Android应用程序基于单线程架构,它有一个主线程负责处理所有用户输入事件和UI渲染。当 Android 应用程序的 UI 线程阻塞时间过长时,会触发“应用程序未响应”(ANR) 错误。如果应用程序位于前台,系统会向用户显示一个对话框 -
事实上,在主线程上执行阻塞操作是一个关键问题。此类操作可能会导致应用程序无响应 (ANR) 情况,导致应用程序终止,从而提示用户卸载它。
避免主线程上的阻塞操作的重要性怎么强调都不为过。这种做法确保了应用程序的响应能力,提供流畅、不间断的用户体验。因此,为了保持应用程序的完整性和功能性,防止 ANR 的发生,采用非阻塞方法并利用后台线程或异步机制势在必行。
我们有很多解决方案将阻塞操作从主线程卸载到后台线程,包括 Java Threads 、 RxJava framework 、 Coroutines 、 Android services 、 JobSchedulers 、 Executors
等等。
这里的要点是,上述框架无法在进程死亡和应用程序重新启动后继续存在。如果任务在进程终止并重新启动后仍然存在,则称为 persistent
。
但问题是我们为什么要选择Work Manager ?
答案很简单,它使用 JobSchedulers 、 Alarm Manager 和 Broadcast Receivers 等底层 Android 框架进行后台工作,并提供一个干净的 API,其中包含所有必需的功能,例如作业取消、进程死亡后仍然存在、支持较低级别的 API、作业链、作业观察等。
Android 文档称工作管理器是后台持续运行工作的解决方案。
Background work 后台作业:
如果作品满足这两个条件,将其称为Background work
-
• None of the app’s activity is visible
-
• The app is not running any foreground services that started when an acitivty from the app was visible to the user 这意味着应用程序应该在后台运行,并且状态栏中不应该有正在运行的通知。
Background work的类型
WorkManager的美妙之处在于它可以处理大多数场景, 下面罗列了一下使用WorkManager的场景:
-
• 如果工作是立即且持久的,那么您应该使用 WorkManager ,如果需要加急,请使用 setExpidited()
-
• 如果工作需要长时间运行并且需要 10 分钟以上才能完成,请使用 WorkManager 。
-
• 如果工作被推迟意味着工作不需要立即运行,请使用 WorkManager 。
-
• 如果工作是立即且不持久的 → 使用 coroutines 、 RxJava 或 Executors()
WorkManager — 它有助于可靠地调度长时间运行的任务[ oppertunistic and guaranteed ],机会主义意味着一旦满足约束它就会执行任务,而保证意味着它甚至会执行工作设备根据配置的退避策略重新启动后。
部署工作分为三个步骤 -
-
• 创建工人——定义实际工作
-
• 创建工作请求 - 这会添加工作所需的配置和约束。
-
• 开展工作
创建工人——定义实际工作
Work — 需要在后台线程上执行的任务,这将在 Worker 类的 doWork() 方法中定义。
class TestWorker(context: Context, appParameters: WorkerParameters) : Worker(context, appParameters) {
override fun doWork(): Result {
// The work code will go here and this will be called on background thread
// by default.
return Result.success()
}
}
doWork() 默认情况下在后台线程上调用,worker 类使用 Executors 和 Threadpool 来卸载后台线程上的任务,如果您检查图书馆你会看到这个
private @NonNull Executor createDefaultExecutor(boolean isTaskExecutor) {
return Executors.newFixedThreadPool(
// This value is the same as the core pool size for AsyncTask#THREAD_POOL_EXECUTOR.
Math.max(2, Math.min(Runtime.getRuntime().availableProcessors() - 1, 4)),
createDefaultThreadFactory(isTaskExecutor));
}
工作线程池至少有 2 个线程,最多 4 个,工作调度由工作线程本身处理,因此不必创建手动线程并管理它,这是一项复杂且容易出错的任务。
创建工作请求
WorkRequest — 工作 + 约束 + 配置
val workRequest = OneTimeWorkRequestBuilder<TestCoroutineWorker>()
.addTag("Test-Worker") // Tag
.setBackoffCriteria( // Configurations
BackoffPolicy.LINEAR,
TimeUnit.SECONDS.toMillis(10),
TimeUnit.MILLISECONDS
)
.setConstraints(Constraints.Builder() // Constraints
.setRequiresCharging(true)
.build())
.build()
如果工作请求不需要任何额外的配置,那么我们可以使用 Request 类中的 from() API,如下所示 -
val request = OneTimeWorkRequest.from(TestWorker::class.java)
开展工作
WorkManager.getInstance(context).enqueue(request)
一旦满足约束并且资源可用,这将在后台线程上启动工作。
执行工作线程的确切时间取决于 WorkRequest 中使用的约束以及设置。工作管理器旨在在这些限制下提供最佳行为。
如何安排一次性工作和周期性工作?
只需要执行一次的工作称为 OneTimeWork
,而在固定时间后重复自身的工作称为 PeriodicWork
。
对于不需要额外配置的简单一次性工作请求,可以使用 OneTimeWorkRequest.from
,对于需要配置的工作请求,可以使用 OneTimeWorkRequestBuilder
上面定义的每个示例。
val request = OneTimeWorkRequest.from(TestWorker::class.java)
要创建定期工作请求,使用 PeriodicWorkRequestBuilder<>()
val periodicWorkRequest = PeriodicWorkRequestBuilder<TestWorker>(20, TimeUnit.MINUTES)
.addTag("Test-Worker")
.setBackoffCriteria(
BackoffPolicy.LINEAR,
TimeUnit.SECONDS.toMillis(10),
TimeUnit.MILLISECONDS
)
.build()
如果满足约束且系统资源可用,则此工作请求将每 20 分钟执行一次。
⚠️注意:可定义的最小重复间隔为 15 分钟。
怎样才能加快工作请求?
当有一些重要的工作需要立即执行时,可以使用WorkManager的加急设置。从 WorkManager 版本 2.7.0 开始,有了加急的概念。这允许 WorkManager 执行重要任务,同时让系统更好地控制对资源的访问。
加急工作具有 Importance 、 speed 、 Quotas 、 PowerManagement 、 latency 的优点
为了加快工作速度,对工作请求使用 setExpedited()
方法,如下所示
val oneTimeWorkRequest = OneTimeWorkRequestBuilder<TestRxWorker>()
.setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST)
.build()
两个 OutOfQuotaPolicy
-
• DROP_WORK_REQUEST — 如果无法加急则删除请求
-
• RUN_AS_NON_EXPEDITED_WORK_REQUEST — 如果超出配额策略,将以非加急方式运行。
WorkManager如何处理与旧 API 版本的向后兼容性?
WorkManager在底层使用 JobSchedular 、 AlarmManager 和 BroadcastReciever 等系统组件的组合,这些组件在较低级别的 API 中可用。 WorkManager 根据用户的设备 API 级别选择要使用的正确 API。
WorkManager 根据以下标准使用可用的底层作业调度服务:
-
• 对于 API 23+ 使用 JobScheduler
-
• 使用 API 14–22 的自定义 AlarmManager + BroadcastReceiver 实现
输入和输出数据
使用 Data 对象将数据传递给Worker
//1. Creating the data
val data = Data.Builder()
.putString("URL", url)
.build()
//2. Passing data to worker
val oneTimeWorkRequest = OneTimeWorkRequestBuilder<TestRxWorker>()
.setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST)
.setInputData(data) // passing data to worker
.build()
//3. Accessing data inside worker
class TestWorker(context: Context, appParameters: WorkerParameters) :
Worker(context, appParameters) {
override fun doWork(): Result {
val data = inputData.getString("URL")
return Result.success()
}
}
从工作线程返回数据,我们有这个方法 workDataOf() ,它允许创建一对可以传递给结果构造函数的数据。
class TestWorker(context: Context, appParameters: WorkerParameters) :
Worker(context, appParameters) {
override fun doWork(): Result {
val data = inputData.getString("URL")
// Some image modification from server
val resultData = workDataOf("MODIFIED_URL", resultUrl)
return Result.success()
}
}
显示输出数据
为了显示输出数据,首先,必须唯一地标识worker,为此可以使用worker的 id 或 tag ,在以下情况下设置ID和标签:正在创建工作请求如下
val workRequestRx = OneTimeWorkRequestBuilder<TestRxWorker>()
.addTag("Test-Worker")
.setId(UUID.fromString("show-output-test"))
.build()
可以使用 IDs 或 tags 中的任何一个,大多数情况下使用 tags 因为它很容易创建和使用。
WorkManager 通过使用以下方法提供 WorkInfo
WorkManager.getInstance(this).getWorkInfosByTagLiveData("Test-Worker")
WorkManager.getInstance(this).getWorkInfoById(UUID.fromString("show-output-test"))
WorkManager.getInstance(this).getWorkInfosForUniqueWorkLiveData("Test-Worker")
可以使用 getWorkInfosByTagLiveData 方法,如下所示 -
WorkManager.getInstance(this).getWorkInfosByTagLiveData("Test-Worker")
.observe(this) {
when (it[0].state) {
WorkInfo.State.ENQUEUED -> {
Log.d(TAG, "onCreate: enqueued")
}
WorkInfo.State.RUNNING -> {
Log.d(TAG, "onCreate: running")
}
WorkInfo.State.SUCCEEDED -> {
val outputUrl = it[0].outputData.getString("MODIFIED_URL")
// Show the output url
Log.d(TAG, "onCreate: succeeded")
}
WorkInfo.State.FAILED -> {
Log.d(TAG, "onCreate: Failed")
}
WorkInfo.State.BLOCKED -> {
Log.d(TAG, "onCreate: blocked")
}
WorkInfo.State.CANCELLED -> {
Log.d(TAG, "onCreate: cancelled")
}
}
}
确保work的唯一性
由于WorkManager负责持久的后台工作,因此有可能多次运行同一个WorkManager,但是如何确保工作的唯一性呢?为了解决这个问题,有一个 API beginUnqiueWork()
。开始独特的工作需要三个输入 - unique work name 、 ExistingWorkPolicy 和 work request.
// Starts a unique work, if we start again then new work will replace the old one as per policy
WorkManager.getInstance(this@MainActivity).beginUniqueWork(
"UNIQUE_WORK_NAME",
ExistingWorkPolicy.REPLACE,
workRequest
).enqueue()
取消工作
WorkManager可以根据ID、标签和唯一链名取消工作请求。
WorkManager.getInstance(this@MainActivity).cancelAllWork()
WorkManager.getInstance(this@MainActivity).cancelWorkById(id)
WorkManager.getInstance(this@MainActivity).cancelAllWorkByTag(tag)
WorkManager.getInstance(this@MainActivity).cancelUniqueWork(unique_work_name)
安排延迟工作
为了安排延迟的工作,有另一个 API setInitialDelay(duration, TimeUnit)
val workRequest = OneTimeWorkRequestBuilder<TestWorker>()
.addTag("Test-Worker")
.setInitialDelay(500, TimeUnit.SECONDS)
.build()
这将在开始工作之前增加 500 秒的初始延迟。
重试和退避策略
work可以返回不同类型的 Results ,例如 success 、 error 和 retry 。如果是重试返回,那么退避策略就会启动,它告诉 worker 在第一次尝试或失败后重试工作之前必须等待的最短时间,称为 backoff delay 。
Backoff policy 定义退避延迟应如何随着后续重试尝试的时间而增加
-
• LINEAR — 随着时间线性增加,例如[10, 20, 30, 40]
-
• EXPONENTIAL - 以非线性方式增加,如 [10, 20, 40, 80],默认延迟为 30 毫秒。
val workRequest = OneTimeWorkRequestBuilder<TestWorker>()
.addTag("Test-Worker")
.setBackoffCriteria(BackoffPolicy.LINEAR,
MIN_BACK_OFF_MILLIS,
TimeUnit.MILLISECONDS
)
.build()
有哪些不同的工作状态?
正如上面的工作信息对象中看到的,我们有多个状态,了解这些状态是如何链接的。
工作从排队状态开始,一旦满足约束,工作就会进入运行状态。 Succeeded 、 failed 和 cancelled 是最终状态,因此一旦到达这些状态,它就不会移动到任何其他状态。
成功和失败状态仅适用于一次性工作和连锁工作。对于周期性工作,只有一个终止状态,即 cancelled 。因为定期工作永远不会结束。
工作链化
WorkManager提供工作链接并并行运行工作。有 API beingWith() ,它返回一个延续,可以在其中使用 then API 来链接工作。例如:
WorkManager.getInstance(this).beginWith(downloadImageWorker)
.then(processImageWorker)
.then(saveImageWorker)
.enqueue()
这将首先使用 DownloadImageWorker 下载图像,然后通过添加过滤器并使用 ProcessIamgeWorker 减小大小来处理图像,最后使用 SaveImageWorker 将其保存到数据库。
还可以使用工作列表将多个工作排队,如下所示,请注意,一项工作的结果在另一项工作中不需要,因此可以并行运行这些工作。
WorkManager.getInstance(this).enqueue(listOf(workA, workB, workC))
为WorkManager提供自定义配置
这有点棘手,通常不需要,WorkManager可以正确处理所有内容,但如果您想提供自定义配置,例如 WorkFactory 告诉允许的线程数、工作管理器的日志记录级别、JobSchedulars 使用的 jobId 范围那么在这种情况下,可以提供自定义配置,如下所示 -
第 1 步 — 禁用默认初始化
如果您的应用程序中不使用应用程序启动,则可以将其完全删除。或者当使用低于 2.6 的 WorkManager 版本时,请删除 workmanager-init :
<!-- If you want to disable android.startup completely. -->
<provider
android:name="androidx.startup.InitializationProvider"
android:authorities="${applicationId}.androidx-startup"
tools:node="remove">
</provider>
否则,仅删除 WorkManagerInitializer 节点。
<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:在应用程序类中提供配置,如下所示
class MyApplication() : Application(), Configuration.Provider {
override fun getWorkManagerConfiguration(): Configuration {
return if (BuildConfig.DEBUG) {
Configuration.Builder()
.setMinimumLoggingLevel(android.util.Log.DEBUG)
.build()
} else {
Configuration.Builder()
.setMinimumLoggingLevel(android.util.Log.ERROR)
.build()
}
}
}
这将为 DEBUG 和 RELEASE 变体配置日志级别。