头图

With the improvement of device performance and the development of software ecosystem, more and more Android applications need to perform relatively more complex network, asynchronous and offline tasks. For example, if a user wants to watch a video offline, but does not want to stay in the application interface and wait for the download to complete, it is necessary to make these offline processes run in the background in a certain way. For another example, if you want to share a wonderful Vlog to social media, you will definitely hope that the upload of the video will not affect your continued use of the device. This comes to the topic we share today: using WorkManager to manage background and foreground work.

If you prefer to see this through video, check it out here:

https://www.bilibili.com/video/BV16S4y1r7o9/?aid=724120861&cid=509493268&page=1

△ Modern WorkManager API released

This article will focus on discussing the API and usage of WorkManager, to help you gain an in-depth understanding of its operating mechanism and how it is used in actual development. There will be another article on how to better use WorkManager in Android Studio soon, so stay tuned.

WorkManager base API

Since its first stable release, WorkManager has provided some basic APIs that help you define jobs, queue them, execute them in sequence, and notify your application when the job is complete. Classified by function, these basic APIs include:

delayed execution of

In the original version, these jobs could only be defined as deferred execution, that is, they would be deferred before starting execution after they were defined. Through this deferred execution strategy, some tasks that are not urgent or low priority will be postponed.

The delayed execution of WorkManager will fully take into account the low power consumption state of the device and the standby storage partition of the application, so you don't have to think about the specific time when the work needs to be executed, it can be taken into account by WorkManager.

work constraint

WorkManager supports setting constraints on a given job run, Constraint ensures that jobs are delayed until they run when optimal conditions are met. For example, only run when the device is on a non-metered connection, when the device is idle, or has enough power. You can concentrate on developing other functions of the application and leave the checking of working conditions to WorkManager.

dependencies between

We know that there may be dependencies between jobs. For example, if you are developing a video editing application, users may need to share to social media after editing, so your application needs to render several video clips in sequence, and then upload them together to the video service. This process is sequential, that is, the upload work depends on the completion of the rendering work.

To give another example, after your application finishes synchronizing data with the backend, you may want the local log files generated during the synchronization process to be cleaned up in time, or to populate the local database with new data from the backend. You can then request the WorkManager to perform these jobs sequentially or in parallel, allowing seamless transitions between jobs. And WorkManager will run subsequent Worker after making sure all given conditions are met.

Work performed multiple times

Many applications with the function of synchronizing with the server have the following characteristics: the synchronization between the application and the back-end server is often not one-time, it may need to be performed multiple times. For example, when your application provides online editing services, it must frequently synchronize local editing data to the cloud, which results in work that is performed regularly.

working status

Since you can check the status of a job at any time, the entire lifecycle is transparent to jobs that are executed on a regular basis. You can tell if a job is queued, running, blocked, or completed.

WorkManager Modern API

The base API described above was already available when we released the first stable version of WorkManager. When we first talked about WorkManager at the Android Developer Summit, we saw it as a library for managing deferrable background work. Today, from a ground-level perspective, this view still holds true. But then we added more new features and made the API more compliant with modern specifications.

executes immediately

Now, when your app is in the foreground, you can request that some work be performed immediately. Then even if the app is put in the background, the work is not interrupted, but continues. Therefore, even if the user switches to another application, your application can still continue to implement a series of tasks such as adding filters to photos, saving them locally, and uploading them.

For developers of large applications, they need to invest more resources and efforts in optimizing resource usage. But WorkManager can greatly reduce their burden with a good resource allocation strategy.

Multiprocess API

Thanks to the use of a new multi-process library to handle work, WorkManager introduces new APIs and low-level optimizations to help large applications schedule and execute work more efficiently. This is thanks to the new WorkManager, which can be scheduled and processed more efficiently in a separate process.

Hardened working test API

Testing is a very important part of an application before it is released to the store or distributed to users. So we've added APIs to help you test a single worker or a group of workers with dependencies.

tool improvements

Along with the release of the library, we have also improved numerous developer tools. As a developer, you can use Android Studio directly to access detailed debug logs and inspection information.

Start using WorkManager

While these newly introduced APIs and improved tooling provide greater convenience for developers, they are also prompting us to rethink the best time to use WorkManager. Although the core idea of our design of WorkManager is still correct from a technical point of view, the capabilities of WorkManager have greatly exceeded our design expectations for an increasingly complex development ecosystem.

job "persistence" feature

WorkManager can handle any type of work you assign to it, so it has evolved to be a good tool for specialized tasks and trustworthy. WorkManager executes the Worker you define in the global scope, which means that as long as your application is still running, whether the device orientation changes, or the Activity is recycled, etc., your work will always be retained. However, this alone can't be called "persistence", so WorkManager also uses the Room database under the hood to ensure that when the process is terminated or the device is restarted, your work can still be executed, and possibly from where it was interrupted. Go ahead.

perform long-running work

WorkManager version 2.3 introduced support for long-running jobs. When we talk about long-running jobs, we mean jobs that run longer than the 10-minute execution window. Typically, a worker's execution window is limited to 10 minutes. In order to implement long-running work, WorkManager bundles the life cycle of the Worker with the life cycle of the foreground service. JobScheduler and the In-Process Scheduler are still aware of the existence of such jobs.

Since the foreground service controls the life cycle of work execution, and the foreground service needs to display notification information to the user, we add related APIs to WorkManager. The user's attention span is limited, so WorkManager provides APIs that allow users to easily stop long-running jobs through notifications. Let's analyze a long-running working example, the code is as follows:

class DownloadWorker(context: Context, parameters: WorkerParameters) : CoroutineWorker(context, parameters) {
    fun notification(progress: String): Notification = TODO()
    // notification 方法根据进度信息生成一条 Android 通知消息。
    suspend fun download(inputUrl: String,
      outputFile: String,
      callback: suspend (progress: String) -> Unit) = TODO()
    // 定义一个用于分块下载的方法
    fun createForegroundInfo(progress: String): ForegroundInfo {
      return ForegroundInfo(id, notification(progress))
    }
 
    override suspend fun doWork(): Result {
      download(inputUrl, outputFile) { progress -> 
        val progress = "Progress $progress %"
        setForeground(createForegroundInfo(progress))
      } // 提供了一个 suspend 标记的 doWork 方法,其中调用下载方法,并显示最新进度信息。
      return Result.success() 
    } //下载完成后,Worker 只需要返回成功即可
}

△ DownloadWorker class

Here is a DownloadWorker class that extends from the CoroutineWorker class. We will define some helper methods in this class to simplify our work. The first is a notification method, which generates an Android notification message based on the given progress information. Next, we need to define a method for downloading in chunks. This method accepts three parameters: the URL of the downloaded file, the local location where the file is saved, and the suspend callback function. This callback is executed every time a chunked download state changes. The information carried in the callback can then be used to generate a notification.

With these helper methods, we can save the ForegroundInfo instance that the WorkManager needs to perform long-running work. ForegroundInfo is constructed by the combination of notification ID and notification instance, please continue to refer to the code example of the CoroutineWorker class above.

In this code, we provide a suspend-marked doWork method, which calls the block download helper method just mentioned. Since each callback happens to provide some up-to-date progress information, we can use this information to build notifications and call the setForeground method to display these notifications to the user. The operation of calling setForeground here is what causes the Worker to run for a long time. After the download is complete, the Worker only needs to return success, and then WorkManager will decouple the execution of the Worker from the foreground service, clean up notification messages, and end related services if necessary. Therefore, our Worker itself does not need to perform service management work.

Terminate work submitted for execution

Users may suddenly change their minds, such as wanting to cancel a job. The notification of a service running in the foreground cannot be simply swiped to cancel. The previous practice was to add an action to this notification message. When the user clicks, a signal will be sent to the WorkManager, thereby terminating a certain job according to the user's intention. You can also terminate by performing expedited work, as described below.

fun notification(progress: String): Notification {
  val intent = WorkManager.getInstance(context)
      .createCancelPendingIntent(getId())
  return NotificationCompat.Builder(applicationContext, id)
      .setContentTitle(title)
      .setContentText(progress)
      // 其他一些操作
      .addAction(android.R.drawable.ic_delete, cancel, intent)
      .build()
}

△ DownloadWorker class derived from CoroutineWorker class

First you need to create a pending Intent, which can easily cancel a job. We need to call the getId method to get the job request ID when the job was created, and then call the createCancelPendingIntent API to create this Intent instance. When this Intent is fired, it sends a signal to the WorkManager to cancel the work, thus achieving the purpose of canceling the work.

The next step is to generate a notification message with a custom action. We use NotificationCompat.Builder to set the notification's title and then add some text. Then call the addAction method to associate the "Cancel" button in the notification with the Intent created in the previous step. So, when the user clicks the "Cancel" button, the Intent will be sent to the foreground service that is currently executing the Worker, thereby terminating it.

performs expedited work

A new foreground service restriction was introduced in Android 12, and foreground services cannot be started when the app is in the background. Therefore, starting from Android 12, calling the setForegroundAsync method will throw a Foreground Service Start Not Allowed Exception (not allowed to start foreground services) exception. This is where WorkManager comes in handy. WorkManager 2.7 version adds support for expedited work, so next will show you how to use WorkManager to terminate the work that has been submitted for execution.

From the user's point of view, rush work is initiated by the user, so it is very important to the user. Even when the application is not in the foreground, these tasks need to be started and executed. For example, a chat application needs to download an attachment in a message, or the application needs to handle the process of paying for subscriptions. In API versions earlier than Android 12, expedited jobs were all performed by foreground services, and starting with Android 12, they will be implemented by expedited jobs.

The system limits the number of expedited jobs in the form of quotas. There are no quota limits for expedited work when the app is in the foreground, but those limits must be respected when the app goes to the background. The size of the quota depends on the application's standby storage partition and process importance (eg, priority). Literally, expedited work is work that needs to be started as soon as possible, which means that this kind of work is quite sensitive to delays, so it does not support setting initial delays or settings for periodic execution. Due to quota constraints, expedited jobs also cannot replace long-running jobs. When your users want to send an important message, WorkManager will try to ensure that the message is sent as quickly as possible.

class SendMessageWorker(context: Context, parameters: WorkerParameters): 
  CoroutineWorker(context, parameters) {
  override suspend fun getForegroundInfo(): ForegroundInfo {
    TODO()
  }
    
  override suspend fun doWork(): Result {
    TODO()
  }
}

△ Expedited work sample code

For example, a case of synchronizing chat app messages uses the Expedited Work API. The SendMessageWorker class extends CoroutineWorker, and its role is to synchronize messages for the chat application from the background. Expedited jobs need to run in the context of some foreground service, much like long-running jobs in pre-Android 12. Therefore, our Worker class also needs to implement the getForegroundInfo interface to facilitate the generation and display of notification messages. But on Android 12, WorkManager will not display other notifications, because the Worker we defined is implemented by expedited jobs. You need to implement a suspend-marked doWork method as usual. It should be noted that when your application consumes the full quota, the expedited job may be interrupted. So it's a good idea for our Worker to keep track of some state so that it can resume running after a rescheduled execution.

val request = OneTimeWorkRequestBuilder<ForegroundWorker>()
    .setExpedited(OutOfQuotaPolicy.DROP_WORK_REQUEST)
    .build()
 
WorkManager.getInstance(context)
    .enqueue(request)

△ setExpedited API sample code

You can schedule expedited work using the setExpedited API, which tells the WorkManager that the user considers a given work request to be important. Since there is a quota limit on the work that can be scheduled, you need to indicate what to do when the app's quota is exhausted, there are two alternatives: one is to turn the expedited request into a regular work request, and the other is to Abandon new work requests when exhausted.

WorkManager multi-process API

Beginning with version 2.5, WorkManager has made several improvements for applications that support multiprocessing. If you need to use the multiprocess API, you need to define the dependencies of the work-multiprocess artifact. The goal of the multiprocess API is to perform extensive initialization operations on redundant or expensive parts of the WorkManager in worker processes. For example, when multiple processes are simultaneously acquiring transaction locks on the unified underlying SQLite database, SQLite contention occurs; and this contention is exactly what we want to reduce through the multi-process API. On the other hand, we also want to make sure that the in-process scheduler is running in the correct process.

To understand which parts of the WorkManager initialization are redundant, we need to understand what it does in the background.

single process initialization

△ The initialization process of a single process

First observe the single-process initialization process. After the application starts, the first thing that the platform calls the Application.onCreate method. Then at some point in the process lifecycle, WorkManager.getInstance is called to initiate the initialization of the WorkManager. When the WorkManager is initialized, we run the ForceStopRunnable. This process is important because at this point WorkManager will check whether the application has been forcibly stopped before, and it will compare the information stored in WorkManager with the information in JobScheduler or AlarmManager to ensure that jobs are accurately scheduled for execution. At the same time, we can also reschedule some of the previously interrupted work, such as some recovery work after a process crash. As we all know, this is very expensive and we need to compare and coordinate state across multiple subsystems, but ideally this operation should only be performed once. Also note that the in-process scheduler only runs in the default process.

Multiprocess initialization

△ Multi-process initialization process

Then let's see what happens if the application has a second process. If the application has a second process, it basically repeats everything that was done in the first process. First the first process is initialized as above, and since this is the primary process, the In-Process Scheduler will also run in it. For the second process, we will repeat the process just now, calling Application.onCreate again, and then reinitializing the WorkManager. This means, we will repeat all the work we did in the first process.

Based on the previous analysis, you may be wondering, why do you need to execute ForceStopRunable again? This is because WorkManager does not know which of these processes has higher priority. If the application is an on-screen keyboard or widget, the main process may not be the same as the default process. Also, there is no in-process scheduler running in secondary processes (because it is not the default process). In fact, the selection of the process where the in-process scheduler is located is very important. Since it is not affected by the limitations of other persistent schedulers, adjusting the process where it is located can significantly improve data throughput. For example, the JobScheduler has a limit of 100 jobs, while the in-process scheduler does not have this limit.

val config = Configuration.Builder()
    .setDefaultProcessName("com.example.app")
    .build()

△ Specify the default process sample code of the application

defines the main process through WorkManager

Let's see how to define a specified default process. First set the name of the default process according to your wishes, this is usually the package name of the application, once the default process of the application is defined, then the in-process scheduler will run in it. But what about helper processes? Is there a way to prevent the WorkManager from being initialized again in it? It turns out this can be done. In fact, what we really need is to not have to initialize the WorkManager at all.

To achieve this goal, we introduced RemoteWorkManager. This class needs to bind to a specified process (the main process) and use the binding service to forward all work requests from secondary processes to this specified main process. This way, you can completely avoid all the cross-process SQLite contention just mentioned, since there is only one process writing to the underlying SQLite database from start to finish. You can create work requests in a worker process as usual, but you should use RemoteWorkManager instead of WorkManager here. After using RemoteWorkManager, it will be bound to the main process through the binding service, and all work requests will be forwarded, and then stored in a specific queue for execution. You can implement this binding by incorporating the RemoteWorkManager service into your app's Android Manifest RXML.

val request = OneTimeWorkRequestBuilder<DownloadWorker>()
    .build()
 
RemoteWorkManager.getInstance(context)
    .enqueue(request)

△ Using RemoteWorkManager sample code

<!-- AndroidManifest.xml -->
<service
    android:name="androidx.work.multiprocess.RemoteWorkManagerService"
    android:exported="false" />

△ Manifest registration service sample code

Running Worker in Different Processes

We've seen how to avoid contention by defining the master process through the WorkManager, but sometimes you also want to be able to run the Worker in a different process. For example, if you run a machine learning workflow (ML Pipeline) in a secondary process of an application, and the application has a dedicated interface process, then you may need to run different workers in different processes. For example, executing a certain work in isolation in the auxiliary process, so that even if an error occurs in the process and crashes, it will not cause other parts of the application to be paralyzed and exit as a whole, especially to ensure the normal operation of the interface process. To implement Worker execution in different processes, you need to extend the RemoteCoroutineWorker class. This class is similar to CoroutineWorker, after extending you need to implement the doRemoteWork interface yourself.

public class IndexingWorker(
  context: Context,
  parameters: WorkerParameters
): RemoteCoroutineWorker(context, parameters) {
  override suspend fun doRemoteWork(): Result {
    doSomething()
    return Result.success()
  }
}

△ IndexingWorker class sample code

Since this method is executed in a worker process, we still have to define which process the Worker needs to bind to. To do this, we also need to add an entry to the Android Manifest RXML. An application can define multiple RemoteWorker services, each running in a separate process.

<!-- AndroidManifest.xml -->
<service
    android:name="androidx.work.multiprocess.RemoteWorkerService"
    android:exported="false"
    android:process=":background" />

△ Manifest registration service sample code

Here you can see that we have added a new service for a worker process called background. Now that you have defined the service in RXML, you need to further specify the name of the component to bind to in the work request.

val inputData = workDataOf(
  ARGUMENT_PACKAGE_NAME to context.packageName,
  ARGUMENT_CLASS_NAME to RemoteWorkerService::class.java.name
)
 
val request = OneTimeWorkRequestBuilder<RemoteDownloadWorker>()
    .setInputData(inputData)
    .build()
 
WorkManager.getInstance(context).enqueue(request)

△ Put the RemoteWork object into the queue sample code

The component name is a combination of the package name and the class name, and you need to add it to the input data of the work request, and then use this input data to create the work request, so that the WorkManager knows which service to bind to. We put the work into the queue as usual, and when the WorkManager is ready to perform the work, it first finds the bound service as defined in the input data and executes the doRemoteWork method. In this way, all complex and tedious tasks of cross-process communication are handed over to WorkManager to handle.

Summary

WorkManager is the recommended solution for long-running work. It is recommended that you use WorkManager to request and cancel long-running work tasks. Learn how and when to use the Expedited Work API and how to write reliable, high-performance multiprocessing applications in this article. I hope this article was helpful, and the next article will give a brief introduction to the new background task inspector, so stay tuned!

For more resources, see:

You are welcome click here to submit feedback to us, or share your favorite content and found problems. Your feedback is very important to us, thank you for your support!


Android开发者
404 声望2k 粉丝

Android 最新开发技术更新,包括 Kotlin、Android Studio、Jetpack 和 Android 最新系统技术特性分享。更多内容,请关注 官方 Android 开发者文档。