The relevant knowledge

  • Swap space: When system memory resources are exhausted and additional memory resources are requested, inactive pages in memory are moved to swap space. Swap space is an area of disk that is slower to access than physical memory.
  • Android is based on the Linux kernel. The main difference between Android and Linux is that there is no Swap space.
  • Therefore, OOM(Out Of Memory) Killer is introduced to solve the problem Of Memory resource exhaustion.
  • It determines whether to kill the process according to the memory size consumed by the process and the “visibility state” of the process to release the memory.
  • The Activity Manager sets the corresponding oOM_adj value for different processes in different states:
# Define the oom_adj values for the classes of processes that can be # killed by the kernel. These are used in Activitymanagerservice.setprop ro.foreground_app_adj 0 // Foreground process setProp ro.visible_app_adj 1 // Visible process setProp SECONDARY_SERVER_ADJ 2 // Secondary service setProp ro.BACKUP_APP_ADJ 2 // Backup process setprop ro.HOME_APP_ADJ 4 // Desktop process setprop HIDDEN_APP_MIN_ADJ 7 // background process setprop ro.content_PROVIDer_adj 14 // content provisioning node setprop ro.empty_app_adj 15 // empty processCopy the code
  • Therefore, 1, the less memory an application consumes, the more likely it is to survive. The second is to design the background task process reasonably

Background tasks

  • Android development will basically use background tasks, usually time-consuming functions that do not need user perception. After the task is completed, the task needs to be closed in time to recover resources. If the use is not reasonable, it may cause a large amount of power consumption.
  • In the past, we generally used Service or thread pool to deal with background tasks. Especially, service is not affected by the Activity life cycle and is widely used in data cache, statistics and log upload, message push, environment monitoring, process protection and pull, etc. Such excessive abuse will bring users problems such as fast power consumption, being disturbed, and privacy disclosure. Therefore, Google has gradually added restrictions in the new Android version, such as Doze mechanism, APP Standby, etc. Especially, Android8.0 does not allow the creation of background services, and it cannot register implicit broadcast receivers in manifest files. So Servcie as we know it has been deprecated because it is no longer allowed to perform long-running operations in the background, even though that is what it was originally designed for. There is no longer any reason to use services other than foregroundServices;

Google recommended solutions for processing background tasks in different scenarios

  1. Need to be triggered by the system: ThreadPool + Broadcast
  2. System trigger required, must be completed, can be deferred: WorkManager
  3. The command must be triggered by the system immediately: ForegroundService + Broadcast
  4. No system trigger, no completion: ThreadPool
  5. No system trigger required, must be completed, can be deferred: WorkManager
  6. ForegroundService does not need to be triggered by the system. It must be completed immediately

The WorkManager profile

  • A compatible, flexible, simple delay background task;
  • JobScheduler is used when THE API is higher than 23 to help optimize battery life and batch processing, while AlarmManager+Broadcast Receiver is automatically switched to when the API is lower than 6.0. The Executor ultimately does the execution;

The WorkManager advantages

  • Compatibility: Compatible with API14
  • Constraints can be specified: execute only if there is a network
  • You can specify times and timing
  • Multiple tasks can use task chains
  • Guaranteed execution: For example, if the current condition is not met or the APP hangs, the next time the condition is met
  • Supports power saving mode

The WorkManager using

Import dependence
Implementation "androidx.work: work-run-time KTX :2.5.0" // optional - RxJava2 support implementation "Androidx. Work: work - rxjava2:2.5.0" / / optional - Test helpers androidTestImplementation "Androidx. Work: work - testing: 2.5.0"Copy the code
Initial Configuration
  • WorkManager versions prior to 2.1.0
// provide custom configuration
val myConfig = Configuration.Builder()
    .setMinimumLoggingLevel(android.util.Log.INFO)
    .build()

// initialize WorkManager
WorkManager.initialize(this, myConfig)
Copy the code
  • For WorkManager 2.1.0 and later, the provider is used to initialize the WorkManager by default. The ContentProvider is used to initialize the WorkManager by default. By looking at its AAR file, you can see the following provider configuration in its Androidmanifest.xml
<provider
    android:name="androidx.work.impl.WorkManagerInitializer"
    android:authorities="${applicationId}.workmanager-init"
    android:directBootAware="false"
    android:exported="false"
    android:multiprocess="true"
    tools:targetApi="n" />
Copy the code
  • The WorkManagerInitializer source code is as follows
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) public class WorkManagerInitializer extends ContentProvider { @Override public boolean onCreate() { // Initialize WorkManager with the default configuration. WorkManager.initialize(getContext(), new Configuration.Builder().build()); return true; } @Nullable @Override public Cursor query(@NonNull Uri uri, @Nullable String[] projection, @Nullable String selection, @Nullable String[] selectionArgs, @Nullable String sortOrder) { return null; } @Nullable @Override public String getType(@NonNull Uri uri) { return null; } @Nullable @Override public Uri insert(@NonNull Uri uri, @Nullable ContentValues values) { return null; } @Override public int delete(@NonNull Uri uri, @Nullable String selection, @Nullable String[] selectionArgs) { return 0; } @Override public int update(@NonNull Uri uri, @Nullable ContentValues values, @Nullable String selection, @Nullable String[] selectionArgs) { return 0; }}Copy the code
  • If you want to customize the initialization, you can do as follows
//1. Androidmanifest.xml overwrites its provider, And set up the tools: node = "remove" 2.6 previous version < / / WorkManager provider android: name = "androidx. Work. Impl. WorkManagerInitializer" Android :authorities="${applicationId}. Workmanager-init "Tools :node="remove" /> //WorkManager after version 2.6 <provider android:name="androidx.startup.InitializationProvider" android:authorities="${applicationId}.androidx-startup" android:exported="false" tools:node="merge"> <! -- If you are using androidx.startup to initialize other components --> <meta-data android:name="androidx.work.WorkManagerInitializer" android:value="androidx.startup" tools:node="remove" /> </provider> //2. Application implements configuration. Provider class MyApplication() : Application(), Configuration.Provider { override fun getWorkManagerConfiguration() = Configuration.Builder() .setMinimumLoggingLevel(android.util.Log.INFO) .build() } //You do not need to call `WorkManager.initialize()` yourself // If I want to call WorkManager manually, I will not get an error. Only the official is not recommended. / / see WorkManagerImpl getInstance method to know the public static @ NonNull WorkManagerImpl getInstance (@ NonNull Context context) { synchronized (sLock) { WorkManagerImpl instance = getInstance(); if (instance == null) { Context appContext = context.getApplicationContext(); If (appContext instanceof configuration. Provider) {initialize(appContext, ((Configuration.Provider) appContext).getWorkManagerConfiguration()); instance = getInstance(appContext); } else { throw new IllegalStateException("WorkManager is not initialized properly. You " + "have explicitly disabled WorkManagerInitializer in your manifest, " + "have not manually called WorkManager#initialize at this point, and " + "your Application does not implement Configuration.Provider."); } } return instance; }}Copy the code
Custom Worker
  • Google provides four types of workers for us to use, which are as follows:
    • The Worker that automatically runs in the background thread
    • CoroutineWorker with coroutines
    • RxWorker combined with RxJava2
    • The ListenableWorker of the base class of the above three classes
  • I used the CoroutineWorker and overrode the doWork method, which looks like this
class MyWorker(appContext: Context, workerParameters: WorkerParameters) : CoroutineWorker(appContext, workerParameters) {override suspend fun doWork(): Result {ljylogutil. d("doWork start") delay(5000) ljylogutil. d("doWork end") return result.success ()}}Copy the code
  • The doWork method executes in a separate background thread
  • There are three results of doWork, which are:
    • Result.success() : Work completed successfully.
    • Result.failure() : Failure to work.
    • Result.retry() : The work fails and is tried at another time according to its retry policy.
Perform a single task
  • A single task using OneTimeWorkRequestBuilder create workRequest, again through the WorkManager object the enqueue () method to submit it to the WorkManager
class WorkManagerActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) {super.oncreate (savedInstanceState) setContentView(r.layout.activity_work_manager) // Perform a single task val workRequest = OneTimeWorkRequestBuilder<MyWorker>().build() WorkManager.getInstance(this).enqueue(workRequest) } }Copy the code
Periodic task
  • It can be used for periodically uploading logs, periodically caching preloaded data, and periodically backing up data
  • Using PeriodicWorkRequest. Create workRequest Builder
val workRequest2 =
            PeriodicWorkRequest.Builder(MyWorker::class.java, 3, TimeUnit.SECONDS).build()
WorkManager.getInstance(this).enqueue(workRequest2)
Copy the code
  • You can also define regular work with flexible time periods, such as running regular work in the last 15 minutes of each hour
val workRequest2: WorkRequest = PeriodicWorkRequest.Builder(
        MyWorker::class.java,
        1, TimeUnit.HOURS,
        15, TimeUnit.MINUTES
    ).build()
WorkManager.getInstance(this).enqueue(workRequest2)
Copy the code
Set task constraints
  • If a constraint is not met, the WorkManager will stop working, and the system will retry after all the constraints are met
Val constraints = constraints.Builder() // Run when the device is idle. SetRequiresDeviceIdle (true) // run in a specific network state //NOT_REQUIRED No network is required //UNMETERED needs unmetering network, such as WiFi //NOT_ROAMING needs non-roaming network //METERED needs metering network, Such as 4 g. SetRequiredNetworkType (NetworkType. CONNECTED) / / sufficient electricity run setRequiresBatteryNotLow (true) / / charging is carried out SetRequiresCharging (true). / / storage space enough to run setRequiresStorageNotLow (true) / / specifies whether the specified (Uri) content updates to perform this task .addContentUriTrigger(Uri.EMPTY, true) .build() val workRequest = OneTimeWorkRequestBuilder<MyWorker>() .setConstraints(constraints) .build() WorkManager.getInstance(this).enqueue(workRequest)Copy the code
Assign input data
Val inputData = data.builder ().putString("name", "ljy").build() val workRequest = OneTimeWorkRequestBuilder<MyWorker>() .setInputData(inputData) .build() WorkManager.getInstance(this).enqueue(workRequest) //2. Class MyWorker(appContext: Context, workerParameters: workerParameters) : CoroutineWorker(appContext, workerParameters) { override suspend fun doWork(): Result { val name = inputData.getString("name") LjyLogUtil.d("doWork start:name=$name") delay(5000) LjyLogUtil.d("doWork  end") return Result.success() } }Copy the code
Delay to perform
val workRequest = OneTimeWorkRequestBuilder<MyWorker>()
        .setInitialDelay(1, TimeUnit.SECONDS)
        .build()
WorkManager.getInstance(this).enqueue(workRequest)
Copy the code
Set the tag
  • It can be used to cancel work or observe its progress, or to group tasks
  • If you have a group of logically related work, it might also be helpful to tag those work items
val workRequest = OneTimeWorkRequestBuilder<MyWorker>()
        .addTag("myWorker")
        .build()
WorkManager.getInstance(this).enqueue(workRequest)
Copy the code
Retry and retreat policies
  • The worker returns result.retry (), and the system will reschedule the work according to the retreat delay time and the retreat policy.
  • Backoff delay: specifies the minimum waiting time after the first attempt and before retry.
  • Fallback policy: Defines how the fallback delay increases over time during subsequent retries. WorkManager supports two fallback policies, LINEAR and EXPONENTIAL.
  • Each work request has a rejection policy and a rejection delay time. The default policy is EXPONENTIAL and the delay time is 10 seconds. Developers can replace this default setting in the work request configuration.
val workRequest4: WorkRequest = OneTimeWorkRequest.Builder(MyWorker::class.java)
    .setBackoffCriteria(
        BackoffPolicy.LINEAR,
        OneTimeWorkRequest.MIN_BACKOFF_MILLIS,
        TimeUnit.MILLISECONDS
    )
    .build()
Copy the code
Create a task chain
  • For example, perform task 1, then task 2, and task 5. Perform task 3 and task 4 after task 2 is complete
val request1 = OneTimeWorkRequest.Builder(MyWorker::class.java).build()
val request2 = OneTimeWorkRequest.Builder(MyWorker::class.java).build()
val request3 = OneTimeWorkRequest.Builder(MyWorker::class.java).build()
val request4 = OneTimeWorkRequest.Builder(MyWorker::class.java).build()
val request5 = OneTimeWorkRequest.Builder(MyWorker::class.java).build()
val workConstraints = WorkManager.getInstance(this).beginWith(request1)
workConstraints.then(request2).then(listOf(request3, request4)).enqueue()
workConstraints.then(request5).enqueue()
Copy the code
The only chain
  • No task with the same name can exist in the queue at the same time
    • The WorkManager. EnqueueUniqueWork () : used for one-time work
    • The WorkManager. EnqueueUniquePeriodicWork () : used to work on a regular basis
  • Application scenario: Multiple requests for interface data, such as the following order, and changing the profile picture
  • For example, replacing the head image requires three tasks: local file reading, compression and uploading. A serial task link is formed and a unique identifier is set. The code is as follows:
val requestLoadFromFile = OneTimeWorkRequest.Builder(MyWorker::class.java).build()
val requestZip = OneTimeWorkRequest.Builder(MyWorker::class.java)
    .setInputData(createInputDataForUri()).build()
val requestSubmitToService = OneTimeWorkRequest.Builder(MyWorker::class.java).build()
WorkManager.getInstance(this).beginUniqueWork(
    "tagChangeImageHeader",
    ExistingWorkPolicy.REPLACE,
    requestLoadFromFile
)
    .then(requestZip)
    .then(requestSubmitToService)
    .enqueue()
Copy the code
  • There are three options for the existingWorkPolicy parameter:
    • REPLACE: If they are the same, delete the existing task and add the existing task.
    • KEEP: If they are the same, let the existing tasks continue and do not add new tasks.
    • APPEND: If they are the same, add a new task to the end of the existing task chain;
The Work state
  • When WorkManager enqueues tasks, it provides a LiveData for each WorkRequest object.
  • LiveData holds WorkStatus. By observing this LiveData, we can determine the current status of the task and obtain all returned values after the task is completed.
ENQUEUED,// joined RUNNING,// SUCCEEDED in RUNNING,// FAILED,// BLOCKED,// suspended; / / has been cancelledCopy the code
  • Status changes are divided into one-time task status and periodic task status:
    • One-off task status: The initial state is ENQUEUED and runs as soon as its Constraints and initial delay timing requirements are satisfied, It transitions to RUNNING, SUCCEEDED, FAILED depending on the Result of the work, and if the Result is result.retry (), it may revert to ENQUEUED. SUCCEEDED, FAILED, and the working State of termination, CANCELLED said WorkInfo. State. IsFinished () returns true. During this process, the work can be CANCELLED at any time, and the work will be CANCELLED.
    • Periodic task status: there is only one CANCELLED state due to the circular execution, the other status is the same as that of one-time task;
State monitoring
// by id
WorkManager.getInstance(this).getWorkInfoById(request1.id)
WorkManager.getInstance(this).getWorkInfoByIdLiveData(request1.id)
// by name
WorkManager.getInstance(this).getWorkInfosForUniqueWork("sync");
WorkManager.getInstance(this).getWorkInfosForUniqueWorkLiveData("sync");
// by tag
WorkManager.getInstance(this).getWorkInfosByTag("syncTag")
WorkManager.getInstance(this).getWorkInfosByTagLiveData("syncTag")
Copy the code
WorkQuery
  • WorkManager 2.4.0 and later also supports complex queries against queued jobs using WorkQuery objects,
  • WorkQuery supports querying by a combination of work tags, status, and unique work names
val workQuery = WorkQuery.Builder
    .fromTags(listOf("syncTag"))
    .addStates(listOf(WorkInfo.State.FAILED, WorkInfo.State.CANCELLED))
    .addUniqueWorkNames(
        listOf("preProcess", "sync")
    )
    .build()
val workInfos: ListenableFuture<List<WorkInfo>> =
    WorkManager.getInstance(this).getWorkInfos(workQuery)
Copy the code
Update progress and observe progress
  • Java: Update progress with worker.setProgressAsync ()
  • Kotlin: use CoroutineWorker setProgress (), update the progress, the code is as follows
class MyWorker(appContext: Context, workerParameters: WorkerParameters) :
        CoroutineWorker(appContext, workerParameters) {
        
    override suspend fun doWork(): Result {
        val name = inputData.getString("name")
        LjyLogUtil.d("doWork start:name=$name")
        val p0 = workDataOf("progressValue" to 0)
        val p1 = workDataOf("progressValue" to 20)
        val p2 = workDataOf("progressValue" to 40)
        val p3 = workDataOf("progressValue" to 60)
        val p4 = workDataOf("progressValue" to 80)
        val p5 = workDataOf("progressValue" to 100)
        setProgress(p0)
        delay(1000)
        setProgress(p1)
        delay(1000)
        setProgress(p2)
        delay(1000)
        setProgress(p3)
        delay(1000)
        setProgress(p4)
        delay(1000)
        setProgress(p5)
        LjyLogUtil.d("doWork end")
        return Result.success()
    }
}
Copy the code
  • To observe the progress: Use getWorkInfoBy… () or getWorkInfoBy… LiveData(), with the following code
val workRequest10 = OneTimeWorkRequestBuilder<MyWorker>().build() WorkManager.getInstance(this).enqueue(workRequest10) WorkManager.getInstance(this) .getWorkInfoByIdLiveData(workRequest10.id) .observe(this, { if (it ! = null) { LjyLogUtil.d("workRequest10:state=${it.state}") val progress = it.progress; val value = progress.getInt("progressValue", 0) LjyLogUtil.d("workRequest10:progress=$value") } })Copy the code
Cancel the task
Workmanager.getinstance (this).cancelallWork () cancels a group of tasks with the same label Workmanager.getinstance (this).cancelAllWorkByTag("tagName") // Cancel the task based on name Workmanager.getinstance (this).cancelUniqueWork("uniqueWorkName") // Cancel the task by ID WorkManager.getInstance(this).cancelWorkById(workRequest.id)Copy the code
Task to stop
  • A running task may stop running for some reasons. The main reasons are as follows:
1. Clear to cancel it, can call the WorkManager. CancelWorkById (UUID) method. 2. If it is the only task, the new WorkRequest whose ExistingWorkPolicy is REPLACE is added to the queue, the old WorkRequest is immediately considered cancelled. 3. The added task constraints no longer apply. 4. The system tells the application to stop working for some reason. 5. When the task is stopped, the WorkManager will immediately call ListenableWorker. OnStopped () close may retain all of the resources.Copy the code

My name is Jinyang. If you want to learn more about jinyang, please pay attention to the wechat public number “Jinyang said” to receive my latest articles