文章

01 .WorkManager基础

01 .WorkManager基础

教程基于 v2.9.0

WorkManager

  • [ ] [应用架构:数据层 - 使用 WorkManager 调度任务 - Android 开发者    Android Developers](https://developer.android.com/topic/libraries/architecture/workmanager)
  • [ ] [后台工作概览    Background work    Android Developers](https://developer.android.com/guide/background)

Persistent work 持久性任务

Types of persistent work

  • Immediate: 立即,必须立即开始执行并很快完成的任务。可能会加急
  • Long Running: 长时间运行,可能运行很长时间(可能超过 10 分钟)的任务
  • Deferrable: 可延迟:稍后启动并可以定时运行的计划任务

不同类型的持久性 Work 如何相互关联:

![600](https://developer.android.com/static/images/guide/background/workmanager_main.svg)

同样,下表概述了各种类型的工作。

TypePeriodicityHow to access 如何访问
ImmediateOne timeOneTimeWorkRequest 和 Worker 。如需加快工作速度,请调用 OneTimeWorkRequest 上的 setExpedited() 。
Long RunningOne time or periodic
一次性或周期性
任何 WorkRequest 或 Worker 。在 Worker 中调用 setForeground() 来处理通知。
DeferrableOne time or periodic
一次性或周期性
PeriodicWorkRequest 和 Worker 。

What is WorkManager?

WorkManager 是 Google 推出的 Jetpack 组件, 用于解决 应用在退出或者设备重启后仍需要需要运行任务 的问题.

WorkManager 提供了一个电池友好的 API,封装了 Android 多年演变的后台行为限制。这对于需要执行后台任务的 Android 应用程序来说至关重要。

WorkManager Features

  • 最高向后兼容到 API 14
    • 在运行 API 23 及以上级别的设备上使用 JobScheduler
    • 在运行 API 14-22 的设备上结合使用 BroadcastReceiverAlarmManager
  • 添加网络可用性或充电状态等工作约束
  • 调度一次性或周期性异步任务
  • 监控和管理计划任务
  • 将任务链接起来
  • 确保任务执行,即使应用或设备重启也同样执行任务
  • 遵循低电耗模式等省电功能: Doze

WorkManager 旨在用于可延迟运行(即不需要立即运行)并且在应用退出或设备重启时必须能够可靠运行的任务。例如:

  • 向后端服务发送日志或分析数据
  • 定期将应用数据与服务器同步

WorkManager 不适用于应用进程结束时能够安全终止的运行中后台工作,也不适用于需要立即执行的任务。

添加依赖

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
dependencies {
    val work_version = "2.9.0"

    // (Java only)
    implementation("androidx.work:work-runtime:$work_version")

    // Kotlin + coroutines
    implementation("androidx.work:work-runtime-ktx:$work_version")

    // optional - RxJava2 support
    implementation("androidx.work:work-rxjava2:$work_version")

    // optional - GCMNetworkManager support
    implementation("androidx.work:work-gcm:$work_version")

    // optional - Test helpers
    androidTestImplementation("androidx.work:work-testing:$work_version")

    // optional - Multiprocess support
    implementation "androidx.work:work-multiprocess:$work_version"
}

最新 release 版本查看:release

When use WorkManager?

WorkManager 处理满足各种约束且需要运行的后台工作,无论应用程序进程是否处于活动状态。

当应用程序位于后台时、当应用程序位于前台时或当应用程序在前台启动但转到后台时,可以启动后台工作。

无论应用程序正在做什么,后台工作都应该继续执行,或者在 Android 终止其进程时重新启动。

关于 WorkManager 的一个常见困惑是:它适用于需要在 “ 后台 “ 线程中运行但不需要在进程死亡后继续存在的任务。WorkManager 不是这种情况。对于此用例还有其他解决方案,例如 kotlin 的协程、ThreadPools 或 RxJava 等库,更多关于 Background Task 见:[guide to background processing.](< [后台工作概览    Background work    Android Developers](https://developer.android.com/develop/background-work/background-tasks)。>)

总结一句就是: 需要在后台做一些任务,不管 APP 是否已经死掉都是需要执行的任务,用 WorkManager。

How use WorkManager?

  1. 配置和初始化 WorkManager,可选
  2. 定义一个 Worker,具体的工作在其 doWork() 方法执行
  3. 定义一个 WorkRequest,请求的约束,执行时间,延迟,tag,输入/输出数据等
  4. 交给 WorkManager 去执行,具体什么时候执行,由系统根据约束条件和系统状态,决定一个最佳执行时机

Configuration and Initialization

默认初始化

WorkManager 的默认配置适用于大多数 APP。如果您需要更多地控制 WorkManager 管理和安排工作的方式,您可以通过自行初始化 WorkManager 来自定义 WorkManager 配置。

WorkManager 默认在 InitializationProvider 初始化了

1
2
3
4
5
6
7
8
9
<provider
	android:name="androidx.startup.InitializationProvider"
	android:authorities="${applicationId}.androidx-startup"
	android:exported="false"
	tools:node="merge" >
	<meta-data
		android:name="androidx.work.WorkManagerInitializer"
		android:value="androidx.startup" />
</provider>

具体的初始化逻辑:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public final class WorkManagerInitializer implements Initializer<WorkManager> {
    private static final String TAG = Logger.tagWithPrefix("WrkMgrInitializer");
    @Override
    public WorkManager create(@NonNull Context context) {
        // Initialize WorkManager with the default configuration.
        Logger.get().debug(TAG, "Initializing WorkManager with default configuration.");
        WorkManager.initialize(context, new Configuration.Builder().build());
        return WorkManager.getInstance(context);
    }
    @Override
    public List<Class<? extends androidx.startup.Initializer<?>>> dependencies() {
        return Collections.emptyList();
    }
}

过滤 TAG:WM-

自定义初始化

按需初始化允许您仅在需要该组件时创建 WorkManager,而不是在每次应用程序启动时创建。这样做可以使 WorkManager 脱离关键启动路径,从而提高应用程序启动性能。要使用按需初始化:

Remove the default initializer

要提供您自己的配置,您必须首先删除默认的初始值设定项。为此,请使用合并规则 tools:node="remove" 更新 AndroidManifest.xml

从 WorkManager 2.6 开始,App Startup 在 WorkManager 内部使用。要提供自定义初始值设定项,您需要删除 androidx.startup 节点。

如果您不在应用程序中使用 App Startup,则可以将其完全删除。

1
2
3
4
5
6
 <!-- If you want to disable android.startup completely. -->
 <provider
    android:name="androidx.startup.InitializationProvider"
    android:authorities="${applicationId}.androidx-startup"
    tools:node="remove">
 </provider>

否则,仅删除 WorkManagerInitializer 节点。

1
2
3
4
5
6
7
8
9
10
11
<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.6 的 WorkManager 版本时,请删除 workmanager-init :

1
2
3
4
<provider
    android:name="androidx.work.impl.WorkManagerInitializer"
    android:authorities="${applicationId}.workmanager-init"
    tools:node="remove" />

如果用了 App Startup,检查清单文件是否存在如下配置,如果存在,删除掉:

1
2
3
4
5
6
7
8
9
10
11
<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>

实现 Configuration.Provider

Application 实现 Provider 自动初始化

让您的 Application 类实现 Configuration.Provider 接口,并提供您自己的 Configuration.Provider.getWorkManagerConfiguration 实现。当您需要使用 WorkManager 时,请务必调用方法 WorkManager.getInstance(Context) 。 WorkManager 调用应用程序的自定义 getWorkManagerConfiguration() 方法来发现其 Configuration

注意:如果在初始化 WorkManager 之前调用已弃用的无参数 WorkManager.getInstance() 方法,该方法将引发异常。即使您没有自定义 WorkManager,您也应该始终使用 WorkManager.getInstance(Context) 方法。

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class App : Application(), Configuration.Provider {
    override fun getWorkManagerConfiguration() =
        Configuration.Builder()
            .setWorkerFactory(RenameWorkerFactory())
            .setMinimumLoggingLevel(Log.VERBOSE)
            .build()
}

class RenameWorkerFactory: WorkerFactory() {  
    override fun createWorker(  
        appContext: Context,  
        workerClassName: String,  
        workerParameters: WorkerParameters  
    ): ListenableWorker? {  
        return when(workerClassName) {  
            "RxCleanupWorker" -> CleanupWorker(appContext, workerParameters)  
            else -> null  
        }  
    }  
}

这种方式不需要手动调用 WorkManager.initialize,这是由于 getInstance() 时会判定 context 是否实现了 Configuration.Provider,然后调用 initialize(),属于懒加载的方式,用到的时候再去初始化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// v2.7.1 WorkManagerImpl.java 
public static WorkManager getInstance(Context context) {  
    return WorkManagerImpl.getInstance(context);  
}
public static WorkManagerImpl getInstance(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 {  
                // ... 
            }  
        }  
        return instance;  
    }  
}
手动初始化 WorkManager
  • 清单文件中移除默认的 InitializationProvider,添加自定义的 InitializationProvider
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<provider
	android:name="androidx.startup.InitializationProvider"
	android:authorities="${applicationId}.androidx-startup"
	android:exported="false"
	tools:node="merge">
	<!--移除默认的WorkManagerInitializer-->
	<meta-data
		android:name="androidx.work.WorkManagerInitializer"
		android:value="androidx.startup"
		tools:node="remove" />
	<!--添加自定义的WorkManagerInitializer-->
	<meta-data
		android:name="me.hacket.assistant.samples.google.architecture.appstartup.WorkManagerInitializer"
		android:value="androidx.startup"
		tools:node="merge" />
</provider>
  • 自定义的 Configuration.Provider
1
  • 自定义的 WorkManagerInitializer
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class WorkManagerInitializer : Initializer<Any> {
    override fun create(context: Context) {
        val configuration = Configuration.Builder().build()
//        WorkManager.initialize(context, configuration) // WorkManager is already initialized.  Did you try to initialize it manually without disabling WorkManagerInitializer? See WorkManager#initialize(Context, Configuration) or the class level Javadoc for more information.
        return WorkManager.initialize(
            context,
            MyWorkManagerConfigurationProvider().workManagerConfiguration
        )
    }

    override fun dependencies(): List<Class<out Initializer<*>>> {
        // No dependencies on other libraries.
        return emptyList()
    }
}
自定义 WorkerFactory
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
 * 多进程的WorkerFactory
 */
class MultiProcessWorkerFactory : WorkerFactory() {
    override fun createWorker(
        appContext: Context,
        workerClassName: String,
        workerParameters: WorkerParameters
    ): ListenableWorker? {
        return when (workerClassName) {
            "com.xxx.appwidget.promotion.AppWidgetPromotionMultiProcessWorker" -> {
                AppWidgetPromotionMultiProcessWorker(appContext, workerParameters)
            }
            else -> {
                null
            }
        }
    }
}

初始化设置:

1
2
3
4
5
6
7
8
val builder = Configuration.Builder()  
    .setMinimumLoggingLevel(if (AppContext.isDebug) android.util.Log.VERBOSE else android.util.Log.WARN)  
    .setInitializationExceptionHandler {  
        L.e(msg = "WorkManager initialization failed", it, tag = TAG)  
    }  
    .setWorkerFactory(MultiProcessWorkerFactory())  
    .setDefaultProcessName(currentProcessName)  
WorkManager.initialize(application, builder.build())

使用:DelegatingWorkerFactory

1
2
3
4
5
6
7
8
9
10
11
12
13
private fun initInWidgetProcess(application: Application, currentProcessName: String) {  
    L.d(msg = L.line(">", "initInWidgetProcess"), tag = TAG, false)  
    val factory = DelegatingWorkerFactory()  
    factory.addFactory(MultiProcessWorkerFactory())  
    val builder = Configuration.Builder()  
        .setMinimumLoggingLevel(if (AppContext.isDebug) android.util.Log.VERBOSE else android.util.Log.WARN)  
        .setInitializationExceptionHandler {  
            L.e(msg = "SiWorkManager initialization failed", it, tag = TAG)  
        }  
        .setWorkerFactory(factory)  
        .setDefaultProcessName(currentProcessName)  
    SiWorkManager.initialize(application, builder.build())  
}

WorkManager v 2.1.0 之前的自定义初始化

[自定义 WorkManager 配置和初始化    Background work    Android Developers](https://developer.android.com/develop/background-work/background-tasks/persistent/configuration/custom-configuration#pre-2.1.0)

WorkManager 使用步骤

1、定义 Worker

work 是使用 Worker 类定义的。 doWork() 方法在 WorkManager 提供的后台线程上异步运行。

要创建一些工作供 WorkManager 运行,请扩展 Worker 类并重写 doWork() 方法。例如,要创建上传图像的 Worker ,您可以执行以下操作:

1
2
3
4
5
6
7
8
9
10
11
class UploadWorker(appContext: Context, workerParams: WorkerParameters):
       Worker(appContext, workerParams) {
   override fun doWork(): Result {

       // Do the work here--in this case, upload the images.
       uploadImages()

       // Indicate whether the work finished successfully with the Result
       return Result.success()
   }
}

doWork() 返回的 Result 通知 WorkManager 服务工作是否成功,以及在失败的情况下是否应重试工作:

  • Result.Success() :工作成功完成。
  • Result.Failure() :工作失败。
  • Result.Retry() :工作失败,应根据其重试策略在其他时间尝试 retry policy

2、定义 WorkRequest

WorkRequest 概述

定义工作后,必须使用 WorkManager 服务对其进行调度才能运行。 WorkManager 在如何安排工作方面提供了很大的灵活性。您可以安排它在一段时间间隔内定期 run periodically 运行,也可以安排它仅运行一次 one time

无论您选择如何安排 work,您都将始终使用 WorkRequestWorker 定义了 work 单元,WorkRequest 及其子类定义了它的运行方式和时间。在最简单的情况下,使用 OneTimeWorkRequest

对于一次性 WorkRequest,请使用 OneTimeWorkRequest,对于周期性工作,请使用 PeriodicWorkRequest

1
2
3
val uploadWorkRequest: WorkRequest =
   OneTimeWorkRequestBuilder<UploadWorker>()
       .build()

安排 one-time work (一次性任务)

对于不需要额外配置的简单工作,请使用静态方法 from

1
val myWorkRequest = OneTimeWorkRequest.from(MyWork::class.java)

对于更复杂的工作,您可以使用构建器:

1
2
val oneTimeWorker = OneTimeWorkRequest.Builder(TestWorker::class.java).build()
WorkManager.getInstance().enqueue(oneTimeWorker)

安排 expedited work(加急任务)

expedited work 概述

WorkManager2.7.0 引入了 expedited 工作的概念。这使得 WorkManager 能够执行重要的工作,同时让系统更好地控制对资源的访问。加急工作具有以下特点:

  • Importance: 重要性:expedited work 适合对用户重要或用户发起的任务。
  • Speed: 速度:expedited work 工作最适合立即开始并在几分钟内完成的短期任务。
  • Quotas: 配额:限制前台执行时间的系统级配额决定了 expedited work 是否可以启动。
  • Power Management: 电源管理:电源管理限制(例如 Battery SaverDoze)不太可能影响 expedited 工作, Power management restrictions
  • Latency: 延迟:如果系统当前的工作负载允许,系统会立即执行 expedited work,这意味着它们对延迟敏感,无法安排稍后执行。也就是说如果系统负载不允许,可能会延迟

Expedited work 潜在的用途是在聊天应用程序中,当用户想要发送消息或图像时;同样地,处理付款或订阅流程的应用程序可能也希望使用 expedited work

这是因为这些任务对用户很重要,在后台快速执行,需要立即开始,并且即使用户关闭应用程序也应该继续执行

从 WorkManager2.7 开始,您的应用程序可以调用 setExpedited() 来声明 WorkRequest 应使用加急作业尽快运行。以下代码片段提供了如何使用 setExpedited() 的示例:

1
2
3
4
5
6
val request = OneTimeWorkRequestBuilder<SyncWorker>()
    .setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST)
    .build()

WorkManager.getInstance(context)
    .enqueue(request)

在此示例中,我们初始化 OneTimeWorkRequest 的实例并对其调用 setExpedited() 。该请求随后将成为加急工作。如果配额允许,它将立即开始在后台运行。如果已使用配额,则 OutOfQuotaPolicy 参数指示请求应按 normalnon-expedited work 运行。

向后兼容性和前台服务
  • Android12 之前版本

为了保持 expedited jobs 的向后兼容性,WorkManager 可能会在早于 Android12 的平台版本上运行 foreground serviceforeground service 可以向用户显示通知。Worker 中的 getForegroundInfoAsync() 和 getForegroundInfo() 方法使 WorkManager 能够在您调用 Android12 之前的 setExpedited() 时显示通知。

如果您希望请求任务作为 expedited work 运行,则任何 ListenableWorker 都必须实现 getForegroundInfo 方法。

注意:在旧平台版本上调用 setExpedited 时,未能实现相应的 getForegroundInfo 方法可能会导致运行时崩溃。

  • Android12 及以上版本

当面向 Android12 或更高版本时,您仍然可以通过相应的 setForeground 方法使用前台服务。

注意:setForeground() 在 Android 12 上可能会引发运行时异常,并且如果启动受到限制 Restrictions on starting a foreground service from the background,则可能会引发异常。

Worker 不知道他们正在做的工作是否 expedited。但当 WorkRequest 加急时,工作人员可以在某些版本的 Android 上显示通知。

为了启用此功能,WorkManager 提供了 getForegroundInfoAsync() 方法,您必须实现该方法,以便 WorkManager 可以在必要时显示通知以启动 ForegroundService 。

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
class UploadLogWorker(context: Context, workerParams: WorkerParameters) :
    Worker(context, workerParams) {
    override fun doWork(): Result {
        Log.d("打印线程", Thread.currentThread().name)
        setForegroundAsync(getForegroundInfo())
        return Result.success()
    }
    @SuppressLint("RestrictedApi")
    override fun getForegroundInfoAsync(): ListenableFuture<ForegroundInfo> {
        val future = SettableFuture.create<ForegroundInfo>()
        future.set(getForegroundInfo())
        return future
    }

    fun getForegroundInfo(): ForegroundInfo {
        val notificationManager =
            applicationContext.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager

        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            val channel = NotificationChannel(
                "1",
                "hh",
                NotificationManager.IMPORTANCE_HIGH
            )
            notificationManager.createNotificationChannel(channel)
        }
        val notification = NotificationCompat.Builder(applicationContext, "1")
            .setSmallIcon(R.drawable.ic_launcher_background)
            .setContentTitle(applicationContext.getString(R.string.app_name))
            .setContentText("我是一个上传日志的任务")
            .build()
        return ForegroundInfo(1337, notification)
    }
}

在 Android11 上运行程序,发现打印出了日志,并显示了一个任务通知:

![400](https://raw.githubusercontent.com/hacket/ObsidianOSS/master/obsidian/20240811211224.png)

小结: 在早于 Android 12 的 API 版本中,加急工作都是由前台服务执行的,而从 Android 12 开始,它们将由加急作业 (expedited job) 实现。所以,Android12 上并不会显示通知栏

expedited work use CoroutineWorker

如果您使用 CoroutineWorker ,则必须实现 getForegroundInfo() 。然后将其传递给 doWork() 内的 setForeground() 。这样做将在 12 之前的 Android 版本中创建通知。

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
class ExpeditedWorker(appContext: Context, workerParams: WorkerParameters):
CoroutineWorker(appContext, workerParams) {
	override suspend fun getForegroundInfo(): ForegroundInfo {
	   return ForegroundInfo(NOTIFICATION_ID, createNotification())
	}
	override suspend fun doWork(): Result {
	   TODO()
	}
	private fun createNotification() : Notification {
	   TODO()
	}
}

注意:您应该将 setForeground() 包装在 try/catch 块中以捕获潜在的 IllegalStateException 。当您的应用程序此时无法在前台运行时,可能会发生这些情况。在 Android 12 及更高版本中,您可以使用更详细的 ForegroundServiceStartNotAllowedException 。

Quota policies 配额政策

当您的应用程序达到其执行配额时,您可以控制 expedited work 的情况。要继续,您可以传递 setExpedited()

  • OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST 当系统无法为任务加急处理时,任务变成常规任务
  • OutOfQuotaPolicy.DROP_WORK_REQUEST 如果没有足够的配额,这会导致请求取消,任务删除

系统必须先为其分配执行时间,然后才能运行 expedited work。执行时间不是 unlimited。相反,每个应用程序都会收到一定的执行时间配额。当您的应用程序使用其执行时间并达到其分配的 quota 时,您将无法再执行 expedited work,直到 quota 刷新。这使得 Android 能够更有效地平衡应用程序之间的资源。应用程序可用的执行时间取决于 standby bucket 和进程重要性。

注意: 当您的应用程序位于前台时,quota 不会限制 expedited work 的执行。仅当您的应用程序位于后台或当您的应用程序移至后台时,执行时间配额才适用。 因此,您应该加快您想要在后台继续进行的工作。当您的应用程序位于前台时,您可以继续使用 setForeground() 。

Deferred expedited work

调用作业后,系统会尝试尽快执行给定的 expedited work。但是,与其他类型的作业一样,系统可能会推迟新加急作业的开始,例如在以下情况下:

  • Load: 系统负载过高,当运行的作业过多或系统内存不足时,可能会出现这种情况。
  • Quota: 已超出 expedited work quota 限制。expedited work 使用基于 App Standby Buckets 的配额系统,并限制滚动时间窗口内的最大执行时间。

用于 expedited work 的配额比用于其他类型后台作业的配额更具限制性。

安排 periodic work (周期性任务)

您的应用程序有时可能需要定期运行某些工作。例如,您可能需要定期备份数据、下载应用程序中的新内容或将日志上传到服务器。

以下是如何使用 PeriodicWorkRequest 创建定期执行的 WorkRequest 对象:

1
2
3
4
5
val saveRequest =
       PeriodicWorkRequestBuilder<SaveImageToFileWorker>(1, TimeUnit.HOURS)
    // Additional configuration
	   .build()
// 在此示例中,工作间隔为一小时。

间隔周期定义为重复之间的最短时间。执行工作线程的确切时间取决于您在 WorkRequest 对象中使用的 constraints 以及系统执行的优化。

注意:

可以定义的最小重复间隔为 15 分钟(与 JobScheduler API 相同 JobScheduler API)。

周期性任务需要更加慎重一点. 开启之后如果不注意, 大部分情况下就会一直运行, 这可能带来很不好的用户体验.

灵活的 interval 时间

PeriodicWorkRequest 

设置周期性任务的时候, 需要设置 repeatInterval(重复区间) 和 flexInterval(弹性区间) 参数, 配合注释说明:

![700](https://raw.githubusercontent.com/hacket/ObsidianOSS/master/obsidian/definework-flex-period.png)
1
2
3
4
5
[  弹性区间外  |  弹性区间内 (flex Interval) ][  弹性区间外  |  弹性区间内 (flex Interval) ]...
[  任务不运行  |          任务可运行         ][  任务不运行  |          任务可运行         ]...
\_________________________________________/\________________________________________/...
       第一个区间 (repeat Interval)                 第二个区间 (repeat Interval)        ...(repeat)

  • 要使用弹性周期定义定期工作,请在创建 PeriodicWorkRequest 时传递 flexInterval 以及 repeatInterval 。弹性周期从 repeatInterval - flexInterval 开始,到间隔结束。
  • repeatInterval - flexInterval 期间任务是不会运行的;需要注意如果设置了 flexInterval,安排的 WorkManager 可能就不会立即运行一次
  • repeatInterval 最小值是 15 分钟, 而 flexInterval 的最小值是 5 分钟, 如果 flexInterval 大于 repeatInterval, 也会被修改到和 repeatInterval 一样的值.

以下是可以在每 1 小时周期的最后 15 分钟内运行的定期工作的示例(也就是每小时的最后 45 min→ 1 h 之间 )。

1
2
3
4
val myUploadWork = PeriodicWorkRequestBuilder<SaveImageToFileWorker>(
   1, TimeUnit.HOURS, // repeatInterval (the period cycle)
   15, TimeUnit.MINUTES) // flexInterval
.build()

repeatInterval 必须大于或等于 PeriodicWorkRequest.MIN_PERIODIC_INTERVAL_MILLIS ,flexInterval 必须大于或等于 PeriodicWorkRequest.MIN_PERIODIC_FLEX_MILLIS 。

Constraints 对周期性任务的影响

您可以对 periodic work 应用约束。例如,您可以向工作请求添加约束,以便工作仅在用户设备充电时运行。在这种情况下,即使定义的重复间隔过去了, PeriodicWorkRequest 也不会运行,直到满足此条件。如果在运行间隔内不满足条件,这可能会导致特定的工作运行被延迟,甚至被跳过。

简单说就是到了间隔时间,如果 constraints 不满足也不会运行。

周期性任务注意
  • 默认的周期性任务 enqueue 后,是能立即执行的
  • 如果配置了 flexTimeInterval 的话,就不会立即执行了,需要等 repeatInterval-flexTimeInterval 执行
1
2
3
4
PeriodicWorkRequestBuilder<W>(  
    repeatInterval = 15, TimeUnit.MINUTES, // repeatInterval (the period cycle)  
    flexTimeInterval = 5, TimeUnit.MINUTES  
)
periodic worker 测试

测试代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class LogWorker(appContext: Context, params: WorkerParameters) :
    CoroutineWorker(appContext, params) {
    var i = 1
    override suspend fun doWork(): Result {
        // Do the work here--in this case, upload the images.
        Logger.d(
            "hacket.worker",
            "LogWorker(PeriodicWork) doWork start i=$i, hash=${System.identityHashCode(this)}"
        )

        SystemClock.sleep(10_000L)

        // Indicate whether the task finished successfully with the Result
//        "LogWorker doWork end i=$i, hash=${System.identityHashCode(this)}".logw()
        Logger.w(
            "hacket.worker",
            "LogWorker(PeriodicWork) doWork end i=$i, hash=${System.identityHashCode(this)}"
        )
        return Result.success()
    }
}

测试 log:

1
2
3
4
5
6
7
8
03:38:26.911 D  hacket.worker LogWorker doWork start i=1, hash=164275620  
03:38:36.911 W  hacket.worker LogWorker doWork end i=1, hash=164275620

03:54:27.205 D  WidgetsApp onCreate:  
03:54:27.459 D  hacket.worker LogWorker doWork start i=1, hash=58753849  
03:54:37.463 W  hacket.worker LogWorker doWork end i=1, hash=58753849  
04:09:37.519 D  hacket.worker LogWorker doWork start i=1, hash=68406060  
04:09:47.521 W  hacket.worker LogWorker doWork end i=1, hash=68406060

可以看到,每隔 15min 执行了 1 次任务,每次任务都是一个新的 Worker 实例

更新 Logger 输出代码,输出当前进程信息,对于已经安排的 task,执行的时候也是用最新的 Worker 来输出,例如

1
05:45:14.413 D  WidgetsApp WidgetsApp onCreate, hash=116314074 [2024-07-09 17:45:14 - main - me.hacket.AppWidgets - true]

Work constraints(约束)

约束确保工作被推迟,直到满足最佳条件。 WorkManager 可以使用以下约束。

NetworkType限制您的工作运行所需的 type of network。例如,Wi-Fi ( UNMETERED )。
BatteryNotLow设置为 true 时,如果设备处于低电量模式,您的工作将不会运行。
RequiresCharging设置为 true 时,您的工作将仅在设备充电时运行。
DeviceIdle当设置为 true 时,这要求用户的设备在工作运行之前处于空闲状态。这对于运行批处理操作非常有用,否则可能会对用户设备上活跃运行的其他应用程序产生负面性能影响。
StorageNotLow设置为 true 时,如果用户设备上的存储空间太低,您的工作将不会运行。

要创建一组约束并将其与某些工作关联,请使用 Contraints.Builder() 创建一个 Constraints 实例并将其分配给您的 WorkRequest.Builder() 。

1
2
3
4
5
6
7
8
9
10
11
12
// 设置网络类型
setRequiredNetworkType(networkType: NetworkType)
// 是否运行时电量不要太低
setRequiresBatteryNotLow(requiresBatteryNotLow: Boolean)
// 是否在充电时才运行
setRequiresCharging(requiresCharging: Boolean)
// 是否不太剩余存储空间过低时运行
setRequiresStorageNotLow(requiresStorageNotLow: Boolean)
// 是否在设备空闲时运行, 这个最低版本是 23
setRequiresDeviceIdle(requiresDeviceIdle: Boolean)
// 监听一个本地的 Uri, 第二个参数是否监听 Uri 的子节点. 在 Uri 的内容改变时运行任务, 最低版本是 24
addContentUriTrigger(uri: Uri, triggerForDescendants: Boolean)

示例 1: 例如,以下代码构建一个工作请求,该请求仅在用户的设备正在充电且连接 Wi-Fi 时运行:

1
2
3
4
5
6
7
8
9
val constraints = Constraints.Builder()
   .setRequiredNetworkType(NetworkType.UNMETERED)
   .setRequiresCharging(true)
   .build()

val myWorkRequest: WorkRequest =
   OneTimeWorkRequestBuilder<MyWork>()
       .setConstraints(constraints)
       .build()

当指定多个约束时,只有满足所有约束时您的工作才会运行。如果在您的工作运行时未满足约束条件,WorkManager 将停止您的工作线程。当满足所有约束时,将重试该工作。

示例 2:

1
2
3
4
5
6
7
8
9
10
11
12
13
// Create a Constraints object that defines when the task should run
val constraints = Constraints.Builder()
	.setRequiresDeviceIdle(true) // 设备空闲
	.setRequiresCharging(true) // 充电
	.setRequiresBatteryNotLow(true) // 不是低电量
	.setRequiresStorageNotLow(true) // 不是低存储空间
	.setRequiredNetworkType(NetworkType.NOT_ROAMING) // 不是漫游网络类型
	.build()

// ...then create a OneTimeWorkRequest that uses those constraints
val oneTimeWorkRequest = OneTimeWorkRequestBuilder<UploadWorker>()
	.setConstraints(constraints)
	.build()

Delayed Work(Work 延迟)

如果您的工作没有约束,或者在您的工作排队时满足所有约束,系统可能会选择立即运行该工作。如果您不希望立即运行工作,您可以指定工作在最短初始延迟后开始。

下面是如何将工作设置为在排队后至少运行 10 分钟的示例。

1
2
3
val myWorkRequest = OneTimeWorkRequestBuilder<MyWork>()
   .setInitialDelay(10, TimeUnit.MINUTES)
   .build()

虽然该示例说明了如何为 OneTimeWorkRequest 设置初始延迟,但您也可以为 PeriodicWorkRequest 设置初始延迟。在这种情况下,只有定期工作的第一次运行会被延迟。

注意:worker 执行的确切时间还取决于工作请求中使用的约束以及系统优化。 WorkManager 旨在在这些限制下提供最佳的行为。

Retry and backoff policy(重试和避退政策)

如果您要求 WorkManager 重试您的工作,您可以从您的 Worker 返回 Result.retry() 。然后,您的工作将根据 backoff delay and backoff policy 重新安排。

  • Backoff delay: 退避延迟指定在第一次尝试后重试工作之前等待的最短时间。该值不能小于 10 秒(或 MIN_BACKOFF_MILLIS)。
  • Backoff policy: 退避策略定义退避延迟应如何随着后续重试尝试的时间而增加。 WorkManager 支持 2 种退避策略, LINEAR 和 EXPONENTIAL

每个工作请求都有退避策略和退避延迟。默认策略是 EXPONENTIAL ,延迟 30 秒,但您可以在工作请求配置中覆盖此设置。

以下是自定义退避延迟和策略的示例。

1
2
3
4
5
6
val myWorkRequest = OneTimeWorkRequestBuilder<MyWork>()
   .setBackoffCriteria(
       BackoffPolicy.LINEAR,
       OneTimeWorkRequest.MIN_BACKOFF_MILLIS,
       TimeUnit.MILLISECONDS)
   .build()

在此示例中,最小退避延迟设置为允许的最小值,即 10 秒。由于策略是 LINEAR ,每次新尝试,重试间隔将增加大约 10 秒。例如,如果工作在后续操作后继续返回 Result.retry() ,则以 Result.retry() 结束的第一次运行将在 10 秒后再次尝试,然后是 20、30、40 等。尝试。如果退避策略设置为 EXPONENTIAL ,重试持续时间序列将更接近 20、40、80 等。

注意:退避延迟并不精确,重试之间可能会有几秒的变化,但绝不会小于配置中指定的初始退避延迟。

setBackoffCriteria() 在设置了 setRequiresDeviceIdle(boolean) 时候在 build() 会抛出异 java.lang.IllegalArgumentException: Cannot set backoff criteria on an idle mode job,这是因为 back-off 对这类 job 没意义。

Tag work

每个 work 请求都有一个唯一的标识符 unique identifier,可用于稍后识别该工作,以便取消该工作 cancel 或观察其进度 observe its progress

如果您有一组逻辑上相关的工作,您可能还会发现标记这些工作项很有帮助。标记允许您一起处理一组工作请求。

例如:

以下代码显示了如何向您的作品添加 “ 清理 “ 标签:

1
2
3
val myWorkRequest = OneTimeWorkRequestBuilder<MyWork>()
   .addTag("cleanup")
   .build()

最后,可以将多个标签添加到单个工作请求中。这些标签在内部存储为一组字符串。要获取与 WorkRequest 关联的标签集,您可以使用 WorkInfo.getTags()

从您的 Worker 类中,您可以通过 ListenableWorker.getTags() 检索其标签集。

Assign input data 分配输入数据

您的工作可能需要输入数据才能完成工作。例如,处理上传图像的工作可能需要上传图像的 URI 作为输入。

输入值作为键值对存储在 Data 对象中,并且可以在 WorkRequest 中进行设置。WorkManager 在执行工作时会将输入 Data 传递给您的 Workder。 Worker 类可以通过调用 Worker.getInputData() 访问输入参数。下面的代码显示了如何创建需要输入数据的 Worker 实例以及如何在工作请求中发送它。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// 创建输入
val inputData = Data.Builder()
	.putInt("KEY_FIRST", firstNumber)
	.putInt("KEY_SECOND", secondNumber)
	.build()
val worker = OneTimeWorkRequestBuilder<MathWorker>()
	.setInputData(inputData)
	.build()
WorkManager.getInstance().enqueue(worker)

// Worker 类:
class PlusWorker(context: Context, workerParams: WorkerParameters) : Worker(context, workerParams) {

override fun doWork(): Result {
	// 取出参数
	val first = inputData.getInt("KEY_FIRST", 0)
	val second = inputData.getInt("KEY_SECOND", 0)
	val result = first + second // 1 + 2 = 3
	val output = Data.Builder()
			.putInt("KEY_RESULT", result)
			.build()
	return Result.success(output)
}
}

// 监听返回
WorkManager.getInstance().getWorkInfoByIdLiveData(worker.id)
	.observe(this, Observer { info ->
		if (info != null && info.state.isFinished) {
			// 获取返回结果, 应该是3
			val result = info.outputData.getInt("KEY_RESULT", 0)
		}
	})

类似地, Data 类可用于输出返回值。输入参数和返回值部分更详细地介绍了输入和输出数据:input parameters and returned values

按照设计,Data 对象应该很小,值可以是字符串、基元类型或数组变体。如果需要将更多数据传入和传出工作器,应该将数据放在其他位置,例如 Room 数据库。Data 对象的大小上限为 10 KB。

3、提交 WorkRequest

enqueue() 方法,提交 WorkRequestWorkManager

1
2
3
WorkManager
    .getInstance(myContext)
    .enqueue(uploadWorkRequest)

worker 执行的确切时间取决于 WorkRequest 中使用的 constraints 以及系统优化。 WorkManager 会在这些限制下提供最佳的执行时机。

一旦定义了 Worker 和 WorkRequest ,最后一步就是将您的工作排队。将工作排入队列的最简单方法是调用 WorkManager enqueue() 方法,并传递要运行的 WorkRequest 。

1
2
val myWork: WorkRequest = // ... OneTime or PeriodicWork
WorkManager.getInstance(requireContext()).enqueue(myWork)

将工作排队时要小心,以避免重复。例如,应用程序可能会尝试每 24 小时将其日志上传到后端服务。

如果您不小心,您可能会多次将同一任务排队,即使该作业只需要运行一次。为了实现此目标,您可以将工作安排为独特的工作:unique work

work states 状态

Work states

工作状态 WorkerInfo.State

在工作的整个生命周期内,它会经历多个不同的 State

  1. 如果有尚未完成的前提性工作,则工作处于 BLOCKED State。
  2. 如果工作能够在满足 Constraints 和时机条件后立即运行,则被视为处于 ENQUEUED State。
  3. 当工作器在活跃地执行时,其处于 RUNNING State。
  4. 如果工作器返回 Result.Success(),则被视为处于 SUCCEEDED 状态。这是一种终止 State;只有 OneTimeWorkRequest 可以进入这种 State。
  5. 相反,如果工作器返回 Result.Failure(),则被视为处于 FAILED 状态。这也是一个终止 State;只有 OneTimeWorkRequest 可以进入这种 State。所有依赖工作也会被标记为 FAILED,并且不会运行。
  6. 当您明确取消尚未终止的 WorkRequest 时,它会进入 CANCELLED State。所有依赖工作也会被标记为 CANCELLED,并且不会运行。
One-time work states 一次性工作状态

对于 one-time 工作请求,您的工作以 ENQUEUED 状态开始。

在 ENQUEUED 状态下,只要满足其 Constraints 和初始延迟时间要求,您的工作就有资格运行。从那里它会移动到 RUNNING 状态,然后根据工作结果,它可能会移动到 SUCCEEDED 、 FAILED ,或者可能返回到 ENQUEUED 如果结果是 retry 。在此过程中的任何时刻,工作都可以取消,此时它将转至 CANCELLED 状态。

图 1 说明了一次性工作的生命周期,以及可能将其转变为另一种状态的事件。

![500](https://developer.android.com/static/images/topic/libraries/architecture/workmanager/how-to/one-time-work-flow.png)

SUCCEEDED 、 FAILED 和 CANCELLED 都代表这项工作的最终状态。如果您的工作处于其中任何状态, WorkInfo.State.isFinished() 将返回 true。

Periodic work states 周期性工作状态

成功和失败状态仅适用于 one-time work and chained work。对于 periodic work,只有一种终止状态 CANCELLED 。这是因为定期工作永远不会结束。每次运行后,无论结果如何,都会重新安排时间。下图描绘了周期性工作的凝聚态:

![700](https://developer.android.com/static/images/topic/libraries/architecture/workmanager/how-to/periodic-work-states.png)
Blocked state 阻塞状态

有一个最终状态我们还没有提到,那就是 BLOCKED 。此状态适用于 a series, or chain of work。工作链及其状态图在链接工作中介绍: Chaining work

Unique Work

unique work 是一个强大的概念,它保证您一次只有一个具有特定名称的工作实例。与 IDs 不同,唯一名称是人类可读的,由开发人员指定,而不是由 WorkManager 自动生成。与 tags 不同,唯一名称仅与单个工作实例相关联。

unique work 可以应用于一次性工作和周期性工作。您可以通过调用其中一种方法来创建唯一的工作序列,具体取决于您是安排重复工作还是一次性工作。

这两种方法都接受 3 个参数:

  • uniqueWorkName 用于唯一标识工作请求的 String 。
  • existingWorkPolicy 一个 enum 告诉 WorkManager 如果已经存在具有该唯一名称的未完成工作链该怎么办。有关详细信息,请参阅冲突解决政策 conflict resolution policy
  • work 要安排的 WorkRequest 。

使用 unique work ,我们可以解决前面提到的重复调度问题。

1
2
3
4
5
6
7
8
9
10
11
12
val sendLogsWorkRequest =
   PeriodicWorkRequestBuilder<SendLogsWorker>(24, TimeUnit.HOURS)
	.setConstraints(Constraints.Builder()
	   .setRequiresCharging(true)
	   .build()
	)
	.build()
WorkManager.getInstance(this).enqueueUniquePeriodicWork(
   "sendLogs",
   ExistingPeriodicWorkPolicy.KEEP,
   sendLogsWorkRequest
)

现在,如果代码在 sendLogs 作业已在队列中时运行,则现有作业将被保留,并且不会添加新作业。

如果您需要逐步建立一长串任务,unique work 顺序也很有用。例如,照片编辑应用程序可能允许用户撤消一长串操作。每个撤消操作都可能需要一段时间,但必须以正确的顺序执行。

在这种情况下,应用程序可以创建一个 “ 撤消 “ 链,并根据需要将每个撤消操作附加到链中。有关更多详细信息,请参阅链接工作:Chaining work

Conflict resolution policy 相同任务冲突解决政策

对于 Worker 来说, 可以通过 UUIDTag 来保证其唯一性, 这样在需要的时候就可以避免任务重复执行. 但对于连续的任务链, 如果任务多了, 这样的方式会很繁琐. 于是, WorkerManager 也提供了相应的 API 来保证其唯一性.

当安排 unique work 时,您必须告诉 WorkManager 当发生冲突时要采取什么操作。您可以通过在将工作排队时传递一个枚举来完成此操作。

对于一次性工作,您提供一个 ExistingWorkPolicy ,它支持 4 个选项来处理冲突。

  • REPLACE 此选项取消现有 work,用新的 work 替换已经存在的 work。如果上一个任务处于等待或者未完成的状态, 取消并删除上一个, 执行新的.
  • KEEP 保持现有工作并忽略新工作。如果上一个任务处于等待或者未完成的状态, 什么都不做 (继续等上一个任务执行).
  • APPEND 等现有工作结束再安排新工作:此策略将使您的新工作链接 chained 到现有工作,并在现有工作完成后运行。现有的工作成为新工作的先决条件。如果现有作品变为 CANCELLED 或 FAILED ,则新作品也是 CANCELLED 或 FAILED 。如果您希望无论现有工作的状态如何都运行新工作,请改用 APPEND_OR_REPLACE 。
  • APPEND_OR_REPLACE APPEND_OR_REPLACE 的功能与 APPEND 类似,只是它不依赖于先决条件工作状态。如果现有工作是 CANCELLED 或 FAILED ,则新工作仍会运行。

对于周期工作,您提供一个 ExistingPeriodicWorkPolicy ,它支持 2 个选项: REPLACE 和 KEEP 。这些选项的功能与其 ExistingWorkPolicy 选项相同。

Observing your work 观察你的 work

根据 name、id 和 tag 检查状态

在将工作排队后的任何时候,您都可以通过查询 WorkManager 的 name 、 id 或与其关联的 tag 来检查其状态。

1
2
3
4
5
6
// by id
workManager.getWorkInfoById(syncWorker.id) // ListenableFuture<WorkInfo>
// by name
workManager.getWorkInfosForUniqueWork("sync") // ListenableFuture<List<WorkInfo>>
// by tag
workManager.getWorkInfosByTag("syncTag") // ListenableFuture<List<WorkInfo>>

该查询返回 WorkInfo 对象的 ListenableFuture ,其中包括作品的 id 、其 tags、当前的 State 以及通过 Result.success(outputData) 设置任何 output 数据。

每个方法的 LiveData 变体允许您通过注册侦听器来观察 WorkInfo 的更改。例如,如果您想在某些工作成功完成时向用户显示一条消息,您可以按如下方式设置:

1
2
3
4
5
workManager.getWorkInfoByIdLiveData(syncWorker.id).observe(viewLifecycleOwner) { workInfo ->
   if(workInfo?.state == WorkInfo.State.SUCCEEDED) {
       Snackbar.make(requireView(), R.string.work_completed, Snackbar.LENGTH_SHORT).show()
   }
}

WorkManager 的 getWorkInfosByTagLiveData(tag) 方法被用来观察和获取与特定标签关联的所有工作的状态。返回的是一个 LiveData 列表,可能获取了一些已经完成的工作任务,这是正常现象,因为 WorkInfo 包含作业的最新状态,包括那些已经完成的。为了只获取活动或未完成的工作,你可以过滤 LiveData<List<WorkInfo>> 列表,剔除状态为 SUCCEEDED、FAILED 或 CANCELLED 的 WorkInfo 对象。

1
2
3
4
workManager.getWorkInfosByTagLiveData(tag).observe(this, Observer { workInfos ->
    val activeWorkInfos = workInfos?.filterNot { it.state.isFinished }
    // 处理活动作业的更新...
})
Complex work queries 复杂的工作查询

WorkManager 2.4.0 及更高版本支持使用 WorkQuery 对象对排队作业进行复杂查询。 WorkQuery 支持通过其 tag(s)stateunique work name 的组合来查询工作。

以下示例显示如何找到所有带有标签 syncTag 的工作,该工作处于 FAILED 或 CANCELLED 状态,并且具有唯一的工作名称 preProcesssync

1
2
3
4
5
6
7
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.getWorkInfos(workQuery)

WorkQuery 中的每个组件(tagstatename)都与其他组件一起使用 AND 进行编辑。组件中的每个值都是 OR 编辑的。例如: (name1 OR name2 OR …) AND (tag1 OR tag2 OR …) AND (state1 OR state2 OR …) 。

WorkQuery 也适用于 LiveData 等效项:getWorkInfosLiveData() 。

Cancelling and stopping work 取消和停止 work

取消 Work

如果您不再需要运行之前排队的工作,您可以要求取消它。工作可以通过其 name 、 id 或与其关联的 tag 取消。

1
2
3
4
5
6
7
8
// by id
workManager.cancelWorkById(syncWorker.id)

// by name
workManager.cancelUniqueWork("sync")

// by tag
workManager.cancelAllWorkByTag("syncTag")

在底层,WorkManager 检查工作的 State 。如果工作已经完成,则不会发生任何事情。否则,工作的状态将更改为 CANCELLED 并且工作将来将不会运行。任何依赖于此工作的 WorkRequest 作业也将是 CANCELLED 。

当前 RUNNING 工作收到对 ListenableWorker.onStopped() 的调用。重写此方法以处理任何潜在的清理。有关更多信息,请参阅停止正在运行的 Worker:stop a running worker

注意:cancelAllWorkByTag(String) 取消给定 tag 的所有工作。

Stop a running Worker

您正在运行的 Worker 可能会被 WorkManager 停止,原因有多种:

  • 您明确要求取消它(例如,通过调用 WorkManager.cancelWorkById(UUID) )。
  • 对于 unique work,您显式地将新的 WorkRequest 与 ExistingWorkPolicy of REPLACE 排入队列。旧的 WorkRequest 立即被视为已取消。
  • 您的 work's constraints 不再满足。
  • 系统指示您的应用程序因某种原因停止您的工作。如果超过 10 分钟的执行期限,就会发生这种情况。该 work 计划稍后重试。

在这些情况下,您的 Worker 将停止。

您应该合作中止正在进行的任何 work,并释放您的 Worker 所持有的任何资源。例如,此时您应该关闭数据库和文件的打开句柄。您可以使用两种机制来了解您的 Worker 何时停止。

  • onStopped() 回调 一旦您的 Worker 停止,WorkManager 就会调用 ListenableWorker.onStopped() 。重写此方法以关闭您可能保留的任何资源
  • isStopped() 属性 您可以调用 ListenableWorker.isStopped() 方法来检查您的 worker 是否已经停止。如果您在 Worker 中执行长时间运行或重复性操作,则应经常检查此属性并将其用作尽快停止工作的信号。

注意:WorkManager 会忽略收到 onStop 信号的 Worker 设置的 Result ,因为该 Worker 已被视为已停止。

Chaining work 工作链

WorkManager 允许您创建并排队工作链,该工作链指定多个相关任务并定义它们应按什么顺序运行。当您需要按特定顺序运行多个任务时,此功能特别有用。

要创建工作链,您可以使用 WorkManager.beginWith(OneTimeWorkRequest) 或 WorkManager.beginWith(List<OneTimeWorkRequest>) ,它们各自返回一个 WorkContinuation 实例。

然后, WorkContinuation 可用于使用 then(OneTimeWorkRequest) 或 then(List<OneTimeWorkRequest>) 添加依赖的 OneTimeWorkRequest 实例。

每次调用 WorkContinuation.then(…) 都会返回 WorkContinuation 的新实例。如果您添加 List 个 OneTimeWorkRequest 个实例,这些请求可能会并行运行。

最后,您可以使用 WorkContinuation.enqueue() 方法来 enqueue() 您的 WorkContinuation 链。

让我们看一个例子。在此示例中,配置了 3 个不同的 Worker 作业来运行(可能并行)。然后将这些 Worker 的结果连接起来并传递给缓存 Worker 作业。最后,该作业的输出被传递到上传工作线程,该工作线程将结果上传到远程服务器。

1
2
3
4
5
6
7
8
WorkManager.getInstance(myContext)
   // Candidates to run in parallel
   .beginWith(listOf(plantName1, plantName2, plantName3))
   // Dependent work (only runs after all previous work in chain)
   .then(cache)
   .then(upload)
   // Call enqueue to kick things off
   .enqueue()

Input Mergers 输入合并

当您 chain OneTimeWorkRequest 实例时,父级工作请求的输出将作为输入传递给子级。因此,在上面的示例中, plantName1 、 plantName2 和 plantName3 的输出将作为输入传递给 cache 请求。

为了管理来自多个父工作请求的输入,WorkManager 使用 InputMerger 。

WorkManager 提供了两种不同类型的 InputMerger :

  • OverwritingInputMerger 尝试将所有输入中的所有键添加到输出中。如果发生冲突,它会覆盖先前设置的密钥。
  • ArrayCreatingInputMerger 尝试合并输入,并在必要时创建数组。

如果您有更具体的用例,那么您可以通过子类化 InputMerger 来编写自己的用例。

OverwritingInputMerger 覆盖输入合并

OverwritingInputMerger 是默认的合并方法。如果合并中存在键冲突,则键的最新值将覆盖结果输出数据中的任何先前版本。

例如,如果每个 plant 输入都有一个与其各自变量名称匹配的键( "plantName1" 、 "plantName2" 和 "plantName3" ),则传递给 worker 将拥有三个键值对。

![500](https://developer.android.com/static/images/topic/libraries/architecture/workmanager/how-to/chaining-overwriting-merger-example.png)

如果存在冲突,则最后完成的工作人员 “ 获胜 “,并且其值将传递给 cache 。

![500](https://developer.android.com/static/images/topic/libraries/architecture/workmanager/how-to/chaining-overwriting-merger-conflict.png)

由于您的工作请求是并行运行的,因此您无法保证其运行顺序。在上面的示例中, plantName1 可以保存 "tulip" 或 "elm" 值,具体取决于最后写入的值。如果您有可能发生键冲突并且需要在合并中保留所有输出数据,那么 ArrayCreatingInputMerger 可能是更好的选择。

ArrayCreatingInputMerger 数组创建输入合并

对于上面的示例,假设我们想要保留所有工厂名称 Workers 的输出,我们应该使用 ArrayCreatingInputMerger 。

1
2
3
4
val cache: OneTimeWorkRequest = OneTimeWorkRequestBuilder<PlantWorker>()
   .setInputMerger(ArrayCreatingInputMerger::class)
   .setConstraints(constraints)
   .build()

ArrayCreatingInputMerger 将每个键与一个数组配对。如果每个键都是唯一的,那么您的结果是一系列单元素数组。

![500](https://developer.android.com/static/images/topic/libraries/architecture/workmanager/how-to/chaining-array-merger-example.png)

如果存在任何键冲突,则任何相应的值都会分组到一个数组中。

![500](https://developer.android.com/static/images/topic/libraries/architecture/workmanager/how-to/chaining-array-merger-conflict.png)

Chaining and Work Statuses Chain 和工作状态

只要 OneTimeWorkRequest 链的工作成功完成(即返回 Result.success() ),它们就会按顺序执行。工作请求在运行时可能会失败或被取消,这会对相关工作请求产生下游影响。

当第一个 OneTimeWorkRequest 在工作请求链中排队时,所有后续工作请求都会被阻止,直到第一个工作请求的工作完成。

![500](https://developer.android.com/static/images/topic/libraries/architecture/workmanager/how-to/chaining-enqueued-all-blocked.png)

一旦排队并且满足所有工作约束,第一个工作请求就开始运行。如果根 OneTimeWorkRequest 或 List<OneTimeWorkRequest> 中的工作成功完成(即,它返回 Result.success() ),则下一组相关工作请求将被排队。

![500](https://developer.android.com/static/images/topic/libraries/architecture/workmanager/how-to/chaining-enqueued-in-progress.png)

只要每个工作请求成功完成,相同的模式就会传播到工作请求链的其余部分,直到链中的所有工作完成。虽然这是最简单且通常是首选的情况,但处理错误状态也同样重要。

当工作人员处理您的工作请求时发生错误时,您可以根据您定义的退避策略重试该请求。重试属于链的一部分的请求意味着将使用提供给它的输入数据重试该请求。任何并行运行的工作都不会受到影响。

![500](https://developer.android.com/static/images/topic/libraries/architecture/workmanager/how-to/chaining-enqueued-retry.png)

如果该重试策略未定义或已用尽,或者您达到 OneTimeWorkRequest 返回 Result.failure() 的某种状态,则该工作请求和所有相关工作请求将被标记为 FAILED.

![500](https://developer.android.com/static/images/topic/libraries/architecture/workmanager/how-to/chaining-enqueued-failed.png)

当 OneTimeWorkRequest 被取消时,同样的逻辑也适用。任何依赖的工作请求也被标记为 CANCELLED 并且它们的工作将不会被执行。

![500](https://developer.android.com/static/images/topic/libraries/architecture/workmanager/how-to/chaining-enqueued-cancelled.png)

请注意,如果您要将更多工作请求追加到已失败或已取消工作请求的链中,则新追加的工作请求也将分别标记为 FAILED 或 CANCELLED 。如果您想扩展现有链的工作,请参阅 ExistingWorkPolicy. 中的 APPEND_OR_REPLACE 。

创建工作请求链时,相关工作请求应定义重试策略,以确保工作始终及时完成。失败的工作请求可能会导致不完整的链和/或意外的状态。

示例 1:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// A, B, C 就会按顺序执行, 如果全部返回成功或者某一个返回失败, 那该任务链就会结束.
WorkManager.getInstance()
    .beginWith(workA)
    .then(workB)
    .then(workC)
    .enqueue()

// A, B 一起运行, 虽然这2个的开始顺序不定, 但是 C 一定是在这2个运行后才运行.
WorkManager.getInstance()
    .beginWith(Arrays.asList(workA, workB))
    .then(workC)
    .enqueue()
    
// B 一定会在 A 后面运行, D 也一定会在 C 后面运行, 但是 AB 与 CD 这两条链的运行顺序不定, 但是 E 一定是在 B 和 D 都结束后才运行.
val chain1 = WorkManager.getInstance()
    .beginWith(workA)
    .then(workB)
val chain2 = WorkManager.getInstance()
    .beginWith(workC)
    .then(workD)
val chain3 = WorkContinuation
    .combine(Arrays.asList(chain1, chain2))
    .then(workE)
chain3.enqueue()

示例 2:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// A, B, C 就会按顺序执行, 如果全部返回成功或者某一个返回失败, 那该任务链就会结束.
WorkManager.getInstance()
    .beginWith(workA)
    .then(workB)
    .then(workC)
    .enqueue();

// A, B 一起运行, 虽然这2个的开始顺序不定, 但是 C 一定是在这2个运行后才运行.
WorkManager.getInstance()
    .beginWith(Arrays.asList(workA, workB))
    .then(workC)
    .enqueue();
    
// B 一定会在 A 后面运行, D 也一定会在 C 后面运行, 但是 AB 与 CD 这两条链的运行顺序不定, 但是 E 一定是在 B 和 D 都结束后才运行.
WorkContinuation chain1 = WorkManager.getInstance()
    .beginWith(workA)
    .then(workB);
WorkContinuation chain2 = WorkManager.getInstance()
    .beginWith(workC)
    .then(workD);
WorkContinuation chain3 = WorkContinuation
    .combine(Arrays.asList(chain1, chain2))
    .then(workE);
chain3.enqueue();

Long-running workers 长时间运行的 Workers

WorkManager 为长时间运行的工作人员提供内置支持。在这种情况下,WorkManager 可以向操作系统提供一个信号,表明在执行此工作时,如果可能的话,进程应保持活动状态。这些 Worker 的运行时间可以超过 10 分钟。

此新功能的示例用例包括批量上传或下载(无法分块)、本地处理 ML 模型或对应用程序用户很重要的任务。

在底层,WorkManager 代表您管理和运行前台服务来执行 WorkRequest ,同时还显示可配置的通知。

ListenableWorker 现在支持 setForegroundAsync() API, CoroutineWorker 支持暂停 setForeground() API。这些 API 允许开发人员指定此 WorkRequest 是重要的(从用户角度来看)还是长期运行的。

从 2.3.0-alpha03 开始,WorkManager 还允许您创建 PendingIntent ,它可用于取消工作线程,而无需使用 createCancelPendingIntent() API 注册新的 Android 组件。此方法在与 setForegroundAsync() 或 setForeground() API 一起使用时特别有用,可用于添加通知操作以取消 Worker 。

创建和管理长时间的 Worker

Kotlin 开发人员应该使用 CoroutineWorker 。您可以使用该方法的 suspend 版本 setForeground() 而不是使用 setForegroundAsync() 。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
class DownloadWorker(context: Context, parameters: WorkerParameters) :
   CoroutineWorker(context, parameters) {

   private val notificationManager =
       context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager

   override suspend fun doWork(): Result {
       val inputUrl = inputData.getString(KEY_INPUT_URL) ?: return Result.failure()
       val outputFile = inputData.getString(KEY_OUTPUT_FILE_NAME) ?: return Result.failure()
       // Mark the Worker as important
       val progress = "Starting Download"
       setForeground(createForegroundInfo(progress))
       download(inputUrl, outputFile)
       return Result.success()
   }

   private fun download(inputUrl: String, outputFile: String) {
       // Downloads a file and updates bytes read
       // Calls setForeground() periodically when it needs to update
       // the ongoing Notification
   }
   // Creates an instance of ForegroundInfo which can be used to update the
   // ongoing notification.
   private fun createForegroundInfo(progress: String): ForegroundInfo {
       val id = applicationContext.getString(R.string.notification_channel_id)
       val title = applicationContext.getString(R.string.notification_title)
       val cancel = applicationContext.getString(R.string.cancel_download)
       // This PendingIntent can be used to cancel the worker
       val intent = WorkManager.getInstance(applicationContext)
               .createCancelPendingIntent(getId())

       // Create a Notification channel if necessary
       if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
           createChannel()
       }

       val notification = NotificationCompat.Builder(applicationContext, id)
           .setContentTitle(title)
           .setTicker(title)
           .setContentText(progress)
           .setSmallIcon(R.drawable.ic_work_notification)
           .setOngoing(true)
           // Add the cancel action to the notification which can
           // be used to cancel the worker
           .addAction(android.R.drawable.ic_delete, cancel, intent)
           .build()

       return ForegroundInfo(notificationId, notification)
   }

   @RequiresApi(Build.VERSION_CODES.O)
   private fun createChannel() {
       // Create a Notification channel
   }

   companion object {
       const val KEY_INPUT_URL = "KEY_INPUT_URL"
       const val KEY_OUTPUT_FILE_NAME = "KEY_OUTPUT_FILE_NAME"
   }
}

为长时间运行的 worker 添加 foreground-service 类型

如果您的应用面向 Android 14(API 级别 34)或更高版本,则必须为所有长时间运行的工作程序指定前台服务类型。如果您的应用面向 Android 10(API 级别 29)或更高版本,并且包含需要访问位置的长时间运行的工作线程,请指示该工作线程使用 location 前台服务类型。

如果您的应用面向 Android 11(API 级别 30)或更高版本,并且包含需要访问摄像头或麦克风的长时间运行的工作线程,请分别声明 camera 或 microphone 前台服务类型。

要添加这些前台服务类型,请完成以下部分中描述的步骤。

  • 在应用程序清单中声明工作线程的前台服务类型。在以下示例中,工作人员需要访问位置和麦克风:
1
2
3
4
<service
   android:name="androidx.work.impl.foreground.SystemForegroundService"
   android:foregroundServiceType="location|microphone"
   tools:node="merge" />
  • 在运行时指定前台服务类型: 当您调用 setForeground() 或 setForegroundAsync() 时,请确保指定前台服务类型。
1
2
3
4
5
private fun createForegroundInfo(progress: String): ForegroundInfo {
   // ...
   return ForegroundInfo(NOTIFICATION_ID, notification,
           FOREGROUND_SERVICE_TYPE_LOCATION or
FOREGROUND_SERVICE_TYPE_MICROPHONE) }

注意:从 Android 14(API 级别 34)开始,当您调用 setForeground() 或 setForegroundAsync() 时,系统会根据服务类型检查特定先决条件。有关更多信息,请参阅前台服务指南:Foreground Service guide

Worker 进度

WorkManager 为设置和观察 workers 的中间进度提供一流的支持。如果 workder 在应用程序位于 foreground 运行,则还可以使用返回 WorkInfo 的 LiveData 的 API 向用户显示此信息。

ListenableWorker 现在支持 setProgressAsync() API,这允许它保留中间进度。这些 API 允许开发人员设置 UI 可以观察到的中间进度。进度由 Data 类型表示,它是属性的 serializable 容器(类似于 input 和 output ,并受到相同的限制)。

进度信息只能在 ListenableWorker 运行时观察和更新。在 ListenableWorker 完成执行后设置进度的尝试将被忽略。您还可以使用 getWorkInfoBy…() 或 getWorkInfoBy…LiveData() 方法之一观察进度信息。这些方法返回 WorkInfo 的实例,该实例有一个返回 Data 的新 getProgress() 方法。

Update Progress 更新进度

对于使用 ListenableWorker 或 Worker 的 Java 开发人员, setProgressAsync() API 返回 ListenableFuture<Void> ;考虑到更新过程涉及将进度信息存储在数据库中,更新进度是异步的。在 kotlin 中,您可以使用 CoroutineWorker 对象的 setProgress() 扩展函数来更新进度信息。

此示例显示了一个简单的 ProgressWorker 。 Worker 在启动时将其进度设置为 0,并在完成时将进度值更新为 100。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import android.content.Context
import androidx.work.CoroutineWorker
import androidx.work.Data
import androidx.work.WorkerParameters
import kotlinx.coroutines.delay

class ProgressWorker(context: Context, parameters: WorkerParameters) :
    CoroutineWorker(context, parameters) {

    companion object {
        const val Progress = "Progress"
        private const val delayDuration = 1L
    }

    override suspend fun doWork(): Result {
        val firstUpdate = workDataOf(Progress to 0)
        val lastUpdate = workDataOf(Progress to 100)
        setProgress(firstUpdate)
        delay(delayDuration)
        setProgress(lastUpdate)
        return Result.success()
    }
}

Observing Progress 观察进度

观察进度信息也很简单。您可以使用 getWorkInfoBy…() 或 getWorkInfoBy…LiveData() 方法,并获取对 WorkInfo 的引用。

以下是使用 getWorkInfoByIdLiveData API 的示例。

1
2
3
4
5
6
7
8
9
10
WorkManager.getInstance(applicationContext)
    // requestId is the WorkRequest id
    .getWorkInfoByIdLiveData(requestId)
    .observe(observer, Observer { workInfo: WorkInfo? ->
            if (workInfo != null) {
                val progress = workInfo.progress
                val value = progress.getInt(Progress, 0)
                // Do something with progress information
            }
    })

更新已排队的 work

WorkManager 允许您在将 WorkRequest 排入队列后对其进行更新。这在经常更改 constraints 或需要动态更新其工作人员的大型应用程序中通常是必要的。从 WorkManager 版本 2.8.0 开始, updateWork() API 是执行此操作的方法。

updateWork() 方法允许您即时更改 WorkRequest 的某些方面,而无需经历手动取消并将新的入队的过程。这极大地简化了开发过程。

避免 cancel work

通常,您应该避免取消现有的 WorkRequest 并将新的 WorkRequest enqueue。这样做可能会导致应用程序重复某些任务,并且可能需要您编写大量附加代码。

请考虑以下示例,了解取消 WorkRequest 可能会导致困难的情况:

  • Back-end request: 如果你在 worker 在发送给 server 时取消该 worker,则新的 worker 需要重新开始并计算
  • Scheduling: 如果取消 PeriodicWorkRequest 并且希望新的 PeriodicWorkRequest 按照相同的调度执行,则需要计算时间偏移以确保新的执行时间与之前的工作要求。

updateWork() API 允许您更新工作请求的约束和其他参数,而无需取消和排队新请求。

何时 cancel work

在某些情况下,您应该直接取消 WorkRequest 而不是调用 updateWork() 。当您希望更改已排队工作的基本性质时,您应该这样做。

注意:无法使用 updateWork() 更改 WorkRequest 中 Worker 的类型。例如,如果您已将 OneTimeWorkRequest 排入队列并且希望它定期运行,则必须取消请求并安排新的 PeriodicWorkRequest 。

何时 update work

想象一个照片应用程序每天备份用户的照片。它已将 PeriodicWorkRequest 排入队列来执行此操作。 WorkRequest 具有要求设备充电并连接到 WiFi 的限制。

然而,用户每天仅使用快速充电器为设备充电 20 分钟。在这种情况下,应用程序可能需要更新 WorkRequest 以放宽充电限制,以便即使设备未充满电,它仍然可以上传照片。

在这种情况下,您可以使用 updateWork() 方法来更新工作请求的约束。

如何 update work?

updateWork() 方法提供了一种更新现有 WorkRequest 的简单方法,而无需 cancel() 并将新的 enqueue() 队列。

要使用更新已排队的 work,请按照下列步骤操作:

  • Get the existing ID for enqueued work: 获取您要更新的 WorkRequest 的 ID。您可以使用任何 getWorkInfo API 检索此 ID,或者通过手动保留初始 WorkRequest 中的 ID,以便稍后在将其排入队列之前使用公共属性 WorkRequest.id 进行检索。
  • Create new WorkRequest: 创建一个新的 WorkRequest 并使用 WorkRequest.Builder.setID() 设置其 ID 以匹配现有的 WorkRequest 。
  • Set constraints: 使用 WorkRequest.Builder.setConstraints() 传递 WorkManager 新约束。
  • Call updateWork: 将新的 WorkRequest 传递给 updateWork() 。

Update work example 更新工作示例:演示了如何使用 updateWork() 方法更改用于上传照片的 WorkRequest 的电池限制:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
suspend fun updatePhotoUploadWork() {
    // Get instance of WorkManager.
    val workManager = WorkManager.getInstance(context)

    // Retrieve the work request ID. In this example, the work being updated is unique
    // work so we can retrieve the ID using the unique work name.
    val photoUploadWorkInfoList = workManager.getWorkInfosForUniqueWork(
        PHOTO_UPLOAD_WORK_NAME
    ).await()

    val existingWorkRequestId = photoUploadWorkInfoList.firstOrNull()?.id ?: return

    // Update the constraints of the WorkRequest to not require a charging device.
    val newConstraints = Constraints.Builder()
        // Add other constraints as required here.
        .setRequiresCharging(false)
        .build()

    // Create new WorkRequest from existing Worker, new constraints, and the id of the old WorkRequest.
    val updatedWorkRequest: WorkRequest =
        OneTimeWorkRequestBuilder<MyWorker>()
            .setConstraints(newConstraints)
            .setId(existingWorkRequestId)
            .build()

    // Pass the new WorkRequest to updateWork().
    workManager.updateWork(updatedWorkRequest)
}

updateWork() 返回 ListenableFuture<UpdateResult> 。给定的 UpdateResult 可以具有几个值之一,这些值概述了 WorkManager 是否能够应用您的更改。它还指示何时能够应用更改。

参考: updateWork()UpdateResult

追踪 update 的 generation

每次更新了 WorkRequest 后,generation 都会自动增加 1,这可以让你精准的追踪当前排队的是哪个版本的 WorkRequest。Generations 提供了给你更多的控制当你 osbservingtracingtesting WorkRequest 时。

要生成 WorkRequest ,请按照下列步骤操作:

  • WorkInfo: 调用 WorkManager.getWorkInfoById() 来检索与您的 WorkRequest 对应的 WorkInfo 实例。还有更多的 getWorkInfoXXX ,见 WorkManager reference
  • getGeneration: 在 WorkInfo 实例上调用 getGeneration() 。返回的 Int 对应于 WorkRequest 的生成。

请注意,没有 generation 字段或属性,只有 WorkInfo.getGeneration() 方法。

示例:

1
2
3
4
5
6
7
8
// Get instance of WorkManager.
val workManager = WorkManager.getInstance(context)

// Retrieve WorkInfo instance.
val workInfo = workManager.getWorkInfoById(oldWorkRequestId)

// Call getGeneration to retrieve the generation.
val generation = workInfo.getGeneration()

注意: updateWork() 返回的 UpdateResult 不包括 WorkRequest 的生成。

更新 work policy

以前,更新 periodic work 的推荐解决方案是将 PeriodicWorkRequest 与策略 ExistingPeriodicWorkPolicy.REPLACE 排队。如果有一个待处理的 PeriodicWorkRequest 具有相同的唯一 id ,则新的 work request 将取消并删除它。此 policy 现已弃用,取而代之的是使用 ExistingPeriodicWorkPolicy.UPDATE 的工作流程。

例如,将 enqueueUniquePeriodicWork 与 PeriodicWorkRequest 一起使用时,您可以使用 ExistingPeriodicWorkPolicy.UPDATE 策略初始化新的 PeriodicWorkRequest 。如果存在具有相同唯一名称的待处理 PeriodicWorkRequest ,WorkManager 会将其更新为新规范。按照此工作流程,没有必要使用 updateWork() 。

注意: OneTimeWorkRequest 不存在类似的更新策略。这是因为您可以将 enqueueUniqueWork 方法与 APPEND 或 APPEND_OR_REPLACE 策略一起使用。这样做会创建一个同名的工人链。因此,WorkManager 无法有效地支持它们的 UPDATE 策略,因为无法决定应更新链中的哪些工作线程。

WorkManager 线程和并发

WorkManager 提供了四种不同类型的工作原语:

  • Worker

Worker 是最简单的实现。 WorkManager 自动在后台线程上运行它(也可以覆盖该线程),更多见 [[#Worker]]。

  • CoroutineWorker

CoroutineWorker 是 kotlin 用户的推荐实现。 CoroutineWorker 实例公开了用于后台工作的挂起函数。默认情况下,它们运行在 default Dispatcher ,您可以对其进行自定义,更多见 [[#CoroutineWorker]]。

  • RxWorker

RxWorker 是 RxJava 用户的推荐实现。如果您现有的大量异步代码都是用 RxJava 建模的,则应该使用 RxWorkers。与所有 RxJava 概念一样,您可以自由选择您所选择的线程策略,更多见 [[#RxWorker]]。

  • ListenableWorker

ListenableWorker 是 Worker 、 CoroutineWorker 和 RxWorker 的基类。它适用于 callback-based asynchronous APIs(例如 FusedLocationProviderClient )交互并且不使用 RxJava 的 Java 开发人员,更多见 [[#ListenableWorker]]。

Worker

当您使用 Worker 时,WorkManager 会自动在后台线程上调用 Worker.doWork() 。后台线程来自 WorkManager 的 Configuration 中指定的 Executor。默认情况下,WorkManager 会为您设置一个 Executor ,但您也可以自定义自己的。例如,您可以在应用程序中共享现有的后台执行器,创建单线程 Executor 以确保所有后台工作按顺序执行,甚至指定自定义 Executor 。要自定义 Executor ,请确保手动初始化 WorkManager。

手动配置 WorkManager 时,您可以指定您的 Executor

1
2
3
4
5
6
WorkManager.initialize(
    context,
    Configuration.Builder()
         // Uses a fixed thread pool of size 8 threads.
        .setExecutor(Executors.newFixedThreadPool(8))
        .build())

下面是一个简单的 Worker 示例,它下载网页内容 100 次:

1
2
3
4
5
6
7
8
9
10
11
12
class DownloadWorker(context: Context, params: WorkerParameters) : Worker(context, params) {
    override fun doWork(): ListenableWorker.Result {
        repeat(100) {
            try {
                downloadSynchronously("https://www.google.com")
            } catch (e: IOException) {
                return ListenableWorker.Result.failure()
            }
        }
        return ListenableWorker.Result.success()
    }
}

请注意, Worker.doWork() 是同步调用 - 您应该以阻塞方式完成整个后台工作,并在方法退出时完成它。如果您在 doWork() 中调用异步 API 并返回 Result ,您的回调可能无法正常运行。如果您发现自己处于这种情况,请考虑使用 ListenableWorker (请参阅 ListenableWorker 中的线程)。

当当前运行的 Worker 由于任何原因停止(stopped for any reason)时,它会收到对 Worker.onStopped() 的调用。重写此方法或调用 Worker.isStopped() 来检查代码并在必要时释放资源。当上例中的 Worker 停止时,它可能处于下载项目循环的中间,并且即使已停止,它也会继续这样做。要优化此行为,您可以执行以下操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class DownloadWorker(context: Context, params: WorkerParameters) : Worker(context, params) {

    override fun doWork(): ListenableWorker.Result {
        repeat(100) {
            if (isStopped) {
                break
            }

            try {
                downloadSynchronously("https://www.google.com")
            } catch (e: IOException) {
                return ListenableWorker.Result.failure()
            }

        }

        return ListenableWorker.Result.success()
    }
}

一旦 Worker 停止,您从 Worker.doWork() 返回什么就不再重要了; Result 将被忽略。

CoroutineWorker

对于 kotlin 用户,WorkManager 为协程提供了一流的支持。首先,请在您的 Gradle 文件中包含 work-runtime-ktx in your gradle file 。您应该扩展 CoroutineWorker ,而不是扩展 Worker ,它有 doWork() 的 suspend 版本。例如,如果您想构建一个简单的 CoroutineWorker 来执行一些网络操作:

1
2
3
4
5
6
7
8
9
10
11
class CoroutineDownloadWorker(
    context: Context,
    params: WorkerParameters
) : CoroutineWorker(context, params) {

    override suspend fun doWork(): Result {
        val data = downloadSynchronously("https://www.google.com")
        saveData(data)
        return Result.success()
    }
}

请注意,CoroutineWorker.doWork() 是一个 suspend 函数。与 Worker 不同,此代码不会在 Configuration 中指定的 Executor 上运行。相反,它默认为 Dispatchers.Default 。您可以通过提供自己的 CoroutineContext 来自定义它。在上面的示例中,您可能希望在 Dispatchers.IO 上执行此工作,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
class CoroutineDownloadWorker(
    context: Context,
    params: WorkerParameters
) : CoroutineWorker(context, params) {

    override suspend fun doWork(): Result {
        withContext(Dispatchers.IO) {
            val data = downloadSynchronously("https://www.google.com")
            saveData(data)
            return Result.success()
        }
    }
}

CoroutineWorker 通过取消协程并传播取消信号来自动处理停止。您不需要做任何特殊的事情来处理停工:work stoppages

查看 CoroutineWorker 源码,默认是在 Dispatchers.Default 调度器上运行:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class CoroutineWorker {
	public open val coroutineContext: CoroutineDispatcher = Dispatchers.Default
	public final override fun startWork(): ListenableFuture<Result> {  
	    val coroutineScope = CoroutineScope(coroutineContext + job)  
	    coroutineScope.launch {  
	        try {  
	            val result = doWork()  
	            future.set(result)  
	        } catch (t: Throwable) {  
	            future.setException(t)  
	        }  
	    }  
	    return future  
	}
}

在不同的进程中运行 CoroutineWorker

您还可以使用 RemoteCoroutineWorker ( ListenableWorker 的实现)将 Worker 绑定到特定进程。

RemoteCoroutineWorker 使用您在构建工作请求时作为输入数据的一部分提供的两个额外参数绑定到特定进程: ARGUMENT_CLASS_NAME 和 ARGUMENT_PACKAGE_NAME 。

以下示例演示了构建绑定到特定进程的工作请求:

1
2
3
4
5
6
7
8
9
10
11
12
13
val PACKAGE_NAME = "com.example.background.multiprocess"

val serviceName = RemoteWorkerService::class.java.name
val componentName = ComponentName(PACKAGE_NAME, serviceName)

val data: Data = Data.Builder()
   .putString(ARGUMENT_PACKAGE_NAME, componentName.packageName)
   .putString(ARGUMENT_CLASS_NAME, componentName.className)
   .build()

return OneTimeWorkRequest.Builder(ExampleRemoteCoroutineWorker::class.java)
   .setInputData(data)
   .build()

对于每个 RemoteWorkerService ,您还需要在 AndroidManifest.xml 文件中添加服务定义:

1
2
3
4
5
6
7
8
9
10
11
12
<manifest ... >
    <service
		android:name="androidx.work.multiprocess.RemoteWorkerService"
		android:exported="false"
		android:process=":worker1" />

	<service
		android:name=".RemoteWorkerService2"
		android:exported="false"
		android:process=":worker2" />
    ...
</manifest>

RxWorker

我们提供 WorkManager 和 RxJava 之间的互操作性。首先,请在 Gradle 文件中除了 work-runtime 之外还包含 work-rxjava3 dependency 依赖项。还有一个 work-rxjava2 依赖项支持 RxJava2。

然后,您应该扩展 RxWorker ,而不是扩展 Worker 。最后重写 RxWorker.createWork() 方法以返回指示执行的 Result 的 Single<Result>

1
2
3
4
5
6
7
8
9
10
11
class RxDownloadWorker(
        context: Context,
        params: WorkerParameters
) : RxWorker(context, params) {
    override fun createWork(): Single<Result> {
        return Observable.range(0, 100)
                .flatMap { download("https://www.example.com") }
                .toList()
                .map { Result.success() }
    }
}

请注意, RxWorker.createWork() 是在主线程上调用的,但返回值默认是在后台线程上订阅的。您可以重写 RxWorker.getBackgroundScheduler() 来更改订阅线程。

当 RxWorker 为 onStopped() 时,订阅将被丢弃,因此您不需要以任何特殊方式处理停工。

ListenableWorker

在某些情况下,您可能需要提供自定义线程策略。例如,您可能需要处理基于回调的异步操作。 WorkManager 通过 ListenableWorker 支持此用例。 ListenableWorker 是最基本的 worker API; Worker 、 CoroutineWorker 和 RxWorker 均派生自此类。 ListenableWorker 仅在工作应该开始和停止时发出信号,并将线程完全取决于您。开始工作信号是在主线程上调用的,因此手动转到您选择的后台线程非常重要。

抽象方法 ListenableWorker.startWork() 返回 Result 的 ListenableFuture 。 ListenableFuture 是一个轻量级接口:它是一个 Future ,提供附加侦听器和传播异常的功能。在 startWork 方法中,您应该返回一个 ListenableFuture ,一旦操作完成,您将使用该操作的 Result 来设置它。您可以通过以下两种方式之一创建 ListenableFuture 实例:

如果你想基于异步回调执行一些工作,你可以这样做:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
class CallbackWorker(
	context: Context,
	params: WorkerParameters
) : ListenableWorker(context, params) {
    override fun startWork(): ListenableFuture<Result> {
        return CallbackToFutureAdapter.getFuture { completer ->
            val callback = object : Callback {
                var successes = 0

                override fun onFailure(call: Call, e: IOException) {
                    completer.setException(e)
                }

                override fun onResponse(call: Call, response: Response) {
                    successes++
                    if (successes == 100) {
                        completer.set(Result.success())
                    }
                }
            }

            repeat(100) {
                downloadAsynchronously("https://example.com", callback)
            }

            callback
        }
    }
}

如果你的工作停止了会发生什么?当工作预计停止时, ListenableWorker 的 ListenableFuture 总是被取消。使用 CallbackToFutureAdapter ,您只需添加取消侦听器,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
class CallbackWorker(
	context: Context,
	params: WorkerParameters
) : ListenableWorker(context, params) {
    override fun startWork(): ListenableFuture<Result> {
        return CallbackToFutureAdapter.getFuture { completer ->
            val callback = object : Callback {
                var successes = 0

                override fun onFailure(call: Call, e: IOException) {
                    completer.setException(e)
                }

                override fun onResponse(call: Call, response: Response) {
                    ++successes
                    if (successes == 100) {
                        completer.set(Result.success())
                    }
                }
            }
			completer.addCancellationListener(cancelDownloadsRunnable, executor)
            repeat(100) {
                downloadAsynchronously("https://example.com", callback)
            }
            callback
        }
    }
}

在不同的进程中运行 ListenableWorker

您还可以使用 RemoteListenableWorker ( ListenableWorker 的实现)将工作线程绑定到特定进程。

RemoteListenableWorker 使用您在构建工作请求时作为输入数据的一部分提供的两个额外参数绑定到特定进程: ARGUMENT_CLASS_NAME 和 ARGUMENT_PACKAGE_NAME 。

以下示例演示了构建绑定到特定流程的工作请求:

1
2
3
4
5
6
7
8
9
10
11
12
13
val PACKAGE_NAME = "com.example.background.multiprocess"

val serviceName = RemoteWorkerService::class.java.name
val componentName = ComponentName(PACKAGE_NAME, serviceName)

val data: Data = Data.Builder()
   .putString(ARGUMENT_PACKAGE_NAME, componentName.packageName)
   .putString(ARGUMENT_CLASS_NAME, componentName.className)
   .build()

return OneTimeWorkRequest.Builder(ExampleRemoteListenableWorker::class.java)
   .setInputData(data)
   .build()

迁移 升级到 WorkManager

从 FirebaseJobDispatcher/GCMNetworkManager 升级到 WorkManager

  • [从 Firebase JobDispatcher 迁移到 WorkManager    Background work    Android Developers](https://developer.android.com/develop/background-work/background-tasks/persistent/migrate-from-legacy/firebase)
  • [从 GCMNetworkManager 迁移到 WorkManager    Background work    Android Developers](https://developer.android.com/develop/background-work/background-tasks/persistent/migrate-from-legacy/gcm)

其他

  • 未设置 Constraint 的 Worker,锁屏的情况下,也是会调度的

多进程支持

  • [[03 .WorkManager多进程支持]]

Ref

  • [x] [社区说浅谈 WorkManager 的设计与实现:系统概述 · Issue 66 · qingmei2/blogs · GitHub](https://github.com/qingmei2/blogs/issues/66)
  • [x] [应用架构:数据层 - 使用 WorkManager 调度任务 - Android 开发者    Android Developers](https://developer.android.com/topic/libraries/architecture/workmanager)
  • WorkManager - MAD Skills - YouTube
  • [x] [Android WorkManager with an Example 👷‍♂️by Furkan PaşaoğluDigiGeekMedium](https://medium.com/digigeek/android-work-manager-with-an-example-%EF%B8%8F-63d1b29955d4)
  • [ ] [Introducing WorkManager: How It Works, When to Use It, and How to Use Itby Abdellah Ibn El AzraqMedium](https://bbluecoder.medium.com/introducing-workmanager-how-it-works-when-to-use-it-and-how-to-use-it-2ea98f75ebbc)
  • Demos:AndroidAppWidget
本文由作者按照 CC BY 4.0 进行授权