文章

02 .WorkManager应用场景和测试

02 .WorkManager应用场景和测试

WorkManager

如何测试 WorkManager

REQUEST_DIAGNOSTICS

1
adb shell am broadcast -a 'androidx.work.diagnostics.REQUEST_DIAGNOSTICS' -p 'ai.me.hacket.AppWidgets'

Background Task Inspector (API 26)

路径: View → Tool Window → App Inspection → Background Task Inspector

![image.png500](https://raw.githubusercontent.com/hacket/ObsidianOSS/master/obsidian/20240628144309.png)

使用条件:

  • WorkManager 库在 2.5.0 及以上
  • API 26 及以上
  • 不足: 需要 APP 进程存活
![image.png1500](https://raw.githubusercontent.com/hacket/ObsidianOSS/master/obsidian/20240628162626.png)

Task Details

![image.png700](https://raw.githubusercontent.com/hacket/ObsidianOSS/master/obsidian/20240628162719.png)
  • Description: 此部分列出了工作程序类名称,以及分配的 tag 和工作程序的 UUID。
  • Execution: 此部分显示工作线程的约束(如果有)、运行频率和状态,以及哪个类创建了工作线程并将其排队。
  • WorkContinuation: 此部分显示工作人员在工作链中的位置。要查看工作链中另一个工作人员的详细信息,请单击其 UUID。
  • Results: 此部分显示所选工作器的开始时间、重试次数和输出数据。

Cancel workers

要停止当前正在运行或排队的工作线程,请选择该工作线程并从工具栏中单击 “ 取消选定的工作线程 ![20](https://developer.android.com/static/studio/images/app-inspection/task_inspector_stop_button.png) ”。

View Graph View

由于 Workers 可以链接在一起,因此有时将工作人员依赖关系可视化为图表很有用。要查看工作器链的可视化表示,请从表中选择一个工作器,然后单击工具栏中的显示图形视图 ![20](https://developer.android.com/static/studio/images/app-inspection/task_inspector_graph_view.png) 。图中仅绘制了工人。

通过该图表,您可以快速查看工作人员之间的关系并监控他们在复杂的链接关系中的进度。要返回列表视图,请单击 “ 显示列表视图 ![20](https://developer.android.com/static/studio/images/app-inspection/task_inspector_list_view.png) ”。

View and inspect Jobs, Alarms, and Wakelocks

Background Task Inspector 还可以让您检查应用程序的 jobsalarmswakelocks。每种类型的异步任务都显示在检查器选项卡中相应的标题下,让您可以轻松监控其状态和进度。

worker 类似,您可以选择 jobalarmwakelock 以在Task Details面板中检查其详细信息。

开启 Logging

[调试 WorkManager    Background work    Android Developers](https://developer.android.com/develop/background-work/background-tasks/testing/persistent/debug)
  • 自定义 WorkManager 初始化,开启 log
  • 过滤 log tag: WM-

adb dump

使用 adb 获取更多关于 Android 6.0 或更高版本上的 job scheduling 的信息。运行命令 “adb shell dumpsys jobscheduler” 以查看分配给您的 package 的 job 列表。

1
adb shell dumpsys jobscheduler

结果:

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
# ... 单个job信息
JOB #u0a311/156: cebade6 com.zzz/androidx.work.impl.background.systemjob.SystemJobService
    u0a311 tag=*job*/com.zzz/androidx.work.impl.background.systemjob.SystemJobService#156
    Source: uid=u0a311 user=0 pkg=com.zzz
    JobInfo:
      Service: com.zzz/androidx.work.impl.background.systemjob.SystemJobService
      Priority: 300 [DEFAULT]
      Requires: charging=false batteryNotLow=false deviceIdle=false
      Extras: mParcelledData.dataSize=180
      Minimum latency: +23h59m59s975ms
      Backoff: policy=1 initial=+30s0ms
      Has early constraint
    Required constraints: TIMING_DELAY UID_NOT_RESTRICTED [0x80100000]
    Preferred constraints:
    Dynamic constraints:
    Satisfied constraints: DEVICE_NOT_DOZING BACKGROUND_NOT_RESTRICTED TARE_WEALTH WITHIN_QUOTA UID_NOT_RESTRICTED [0xb500000]
    Unsatisfied constraints: TIMING_DELAY [0x80000000]
    Constraint history:
      -1m24s245ms = BACKGROUND_NOT_RESTRICTED [0x400000]
      -1m24s245ms = DEVICE_NOT_DOZING BACKGROUND_NOT_RESTRICTED [0x2400000]
      -1m24s245ms = DEVICE_NOT_DOZING BACKGROUND_NOT_RESTRICTED WITHIN_QUOTA [0x3400000]
      -1m24s245ms = DEVICE_NOT_DOZING BACKGROUND_NOT_RESTRICTED TARE_WEALTH WITHIN_QUOTA [0xb400000]
      -1m24s245ms = DEVICE_NOT_DOZING BACKGROUND_NOT_RESTRICTED TARE_WEALTH WITHIN_QUOTA UID_NOT_RESTRICTED [0xb500000]
    Tracking: TIME QUOTA UID_RESTRICT
    Implicit constraints:
      readyNotDozing: true
      readyNotRestrictedInBg: true
      readyComponentEnabled: true
    Started with foreground flag: false
    Standby bucket: ACTIVE
    Enqueue time: -1m24s245ms
    Run time: earliest=+23h58m35s730ms, latest=none, original latest=none
    Restricted due to: none.
    Ready: false (job=false user=true !restricted=true !pending=true !active=true !backingup=true comp=true)
# ...

Tests 单元测试

  • [使用 WorkManager 进行集成测试    Background work    Android Developers](https://developer.android.com/develop/background-work/background-tasks/testing/persistent/integration-testing)
  • [测试 Worker 实现    Background work    Android Developers](https://developer.android.com/develop/background-work/background-tasks/testing/persistent/worker-impl)

突破 15 min 限制

15 min 限制

frameworks/base/apex/jobscheduler/framework/java/android/app/job/JobInfo.java

1
2
3
4
5
/* Minimum interval for a periodic job, in milliseconds. */
private static final long MIN_PERIOD_MILLIS = 15 * 60 * 1000L;   // 15 minutes

/* Minimum flex for a periodic job, in milliseconds. */
private static final long MIN_FLEX_MILLIS = 5 * 60 * 1000L; // 5 minutes

hook

定制 ROM?

WorkManager 应用

one time work 一次任务

Worker

模拟图片上传的一次性任务:

定义一个 UploadWorker:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class UploadWorker(context: Context, workerParams: WorkerParameters) :
    Worker(context, workerParams) {
    override fun doWork(): Result {
        // Do the work here--in this case, upload the images.
        "WorkManager UploadWorker doWork start".logi()
        uploadImages()

        // Indicate whether the task finished successfully with the Result
        "WorkManager UploadWorker doWork end".logw()
        return Result.success()
    }

    private fun uploadImages() {
        LogUtils.logi("hacket.WorkManager", "uploadImages", "模拟上传图片操作 sleep 10000 start")
        SystemClock.sleep(10000)
        LogUtils.logw("hacket.WorkManager", "uploadImages", "模拟上传图片操作完成 end")
    }
}
1
2
3
4
5
6
7
8
val uploadWorkRequest = OneTimeWorkRequestBuilder<UploadWorker>()
	.addTag("hello_world")
	.setInitialDelay(10, TimeUnit.SECONDS)
	.build()
val uuid = uploadWorkRequest.id
WorkManager
	.getInstance(context)
	.enqueue(uploadWorkRequest)

观察 work 状态,可通过 idtag

1
2
3
4
5
6
7
8
9
10
11
12
13
14
WorkManager.getInstance(this)  
    .getWorkInfoByIdLiveData(uuid)  
    .observe(this) { workInfo ->  
        if (workInfo != null) {  
            // ...
        }  
    }  
  
WorkManager.getInstance(this).getWorkInfosByTagLiveData("hello_world")  
    .observe(this) { workInfos ->  
        workInfos.forEach {  
            // ... 
        }  
    }

CoroutineWork

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
class UploadCoroutineWorker(appContext: Context, params: WorkerParameters) : CoroutineWorker(
    appContext,
    params
) {
    override suspend fun doWork(): Result {
        // Do the work here--in this case, upload the images.
        "[$TAG]UploadWorker doWork start".logi()
        uploadImages()

        // Indicate whether the task finished successfully with the Result
        "[$TAG]UploadWorker doWork end".logw()
        return Result.success()
    }
    private suspend fun uploadImages() {
        // 创建一个拥有固定线程数的线程池
        val threadPool = Executors.newFixedThreadPool(4)
        // 从线程池创建一个CoroutineDispatcher
        val customDispatcher = threadPool.asCoroutineDispatcher()
        // 默认是Dispatchers.default()
        withContext(customDispatcher) {
	        // 切换线程到pool-10-thread-1
            LogUtils.logd(
                "hacket.WorkManager",
                "uploadImages",
                "[$TAG]withContext(IO) 模拟上传图片操作 delay 5000 start"
            )
        }
        delay(5_000L)
        LogUtils.logd("hacket.WorkManager", "uploadImages", "[$TAG]模拟上传图片操作完成 end")
        
    }
}

添加约束条件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
val constraints = Constraints.Builder()
//     .setRequiresDeviceIdle(true) // 设备空闲,设置了该项,就不能设置setBackoffCriteria
	.setRequiresCharging(true) // 充电
	.setRequiresBatteryNotLow(true) // 不是低电量
	.setRequiresStorageNotLow(true) // 不是低存储空间
	.setRequiredNetworkType(NetworkType.NOT_ROAMING) // 不是漫游网络类型
	.build()
val uploadWorkRequest = OneTimeWorkRequestBuilder<UploadCoroutineWorker>()
	.setConstraints(constraints)
	.addTag("hello_world_coroutine")
	.setInitialDelay(10, TimeUnit.SECONDS)
	.build()
lastUuid2 = uploadWorkRequest.id
WorkManager
	.getInstance(context)
	.enqueue(uploadWorkRequest)

RxWorker

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class UploadRxWorker(appContext: Context, workerParams: WorkerParameters) : RxWorker(
    appContext,
    workerParams
) {
    companion object {
        private const val TAG = "rx";
    }

    override fun createWork(): Single<Result> {
        return Single.create { emitter ->
            // Do the work here--in this case, upload the images.
            "[$TAG]UploadWorker doWork start delay 5000ms".logd()
            Thread.sleep(5_000L)

            // Indicate whether the task finished successfully with the Result
            "[$TAG]UploadWorker doWork end".logi()
            emitter.onSuccess(Result.success())
        }.subscribeOn(Schedulers.io())
    }
}

Work chain demos

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
val continuation: WorkContinuation
continuation = WorkManager.getInstance(context)  
    .beginUniqueWork(  
        Constants.IMAGE_MANIPULATION_WORK_NAME,  
        ExistingWorkPolicy.REPLACE,  
        OneTimeWorkRequest.from(CleanupWorker::class.java)  
    ).thenMaybe<WaterColorFilterWorker>(waterColor)  
    .thenMaybe<GrayScaleFilterWorker>(grayScale)  
    .thenMaybe<BlurEffectFilterWorker>(blur)  
    .then(  
        if (save) {  
            workRequest<SaveImageToGalleryWorker>(tag = Constants.TAG_OUTPUT)  
        } else /* upload */ {  
            workRequest<UploadWorker>(tag = Constants.TAG_OUTPUT)  
        }  
    )
/**  
 * Applies a [ListenableWorker] to a [WorkContinuation] in case [apply] is `true`. */private inline fun <reified T : ListenableWorker> WorkContinuation.thenMaybe(  
    apply: Boolean  
): WorkContinuation {  
    return if (apply) {  
        then(workRequest<T>())  
    } else {  
        this  
    }  
}  
  
/**  
 * Creates a [OneTimeWorkRequest] with the given inputData and a [tag] if set. */private inline fun <reified T : ListenableWorker> workRequest(  
    inputData: Data = imageInputData,  
    tag: String? = null  
) =  
    OneTimeWorkRequestBuilder<T>().apply {  
        setInputData(inputData)  
        setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST)  
        if (!tag.isNullOrEmpty()) {  
            addTag(tag)  
        }  
}.build()

architecture-components-samples/WorkManagerSample at main · android/architecture-components-samples · GitHub

多进程 WorkManager 官方 samples

architecture-components-samples/WorkManagerMultiprocessSample at main · android/architecture-components-samples · GitHub

应用场景

Upload log

Uploading logs 🗳 -> Maybe you want to get some user’s log every day at 12.00 am. Work manager is suitable for that

云盘同步功能

直播/语音 APP 的礼物下载

礼物下载,弄个定时 work,每天同步 1~2 次

自有埋点的上报

自己采集的一些埋点,定时上报

图片上传

头像的上传

用户头像上传

图片的处理

Applying filters to images and saving the image 🗂 -> Suppose that you work at an AI Company and you want to filter the image but request-response get fails, work manager is retry it at later time

  1. 图片的 blur worker
  2. 图片的 gray worker
  3. 图片 water worker
  4. 图片上传 worker

一系列 worker 组成 chain

官方 chain 示例:architecture-components-samples/WorkManagerSample at main · android/architecture-components-samples · GitHub

App Widget

App Widget 的周期性更新数据,通过 WorkManager 来更新

Google Play Cubes

周期性任务更新数据

WorkManager 坑

注意

  1. 没有添加 tag 的 schedule woker,每次添加都是新的 worker

Oppo 工程师认为这个 Api 过于耗电,于是屏蔽掉了这个 Api 的功能 (待验证)

详情请参考:OPPO社区

解决(如果存在)

  1. 彻底放弃使用 WorkManager 这个 api,使用 Alarm manager 代替
  2. 部分放弃,可以在我们的代码中检测手机品牌是 Oppo 的话就用 Alarm manager 代替,否则使用 WorkManager 代替,查询 Oppo 手机的 rom 的方式是查询 build.prop 是否有 ro.build.version.opporom 属性

进程杀死执行情况

  • WorkManager 虽然在设计的时候是为了在 App 没运行的时候也能运行 Worker, 但是目前从 Google Issue Tracker 上的信息来看, 以下几种情况杀掉后任务的存活情况是这样的:

    1. 从任务管理器 (最近使用) 关掉: 原生的 Android 上 Worker 仍然会运行, 但是在 某些把这种操作当做强制停止的厂商 或 一些中国厂商 的机型上, Worker 要等到下次打开 App 才会运行.
    2. 重启手机 (Worker 运行中的状态): 重启后 Worker 会继续运行.
    3. App 信息 -> 强制关闭: Worker 会再下次打开 App 的时候运行.
    4. 重启手机 (App 被强制关闭了): Worker 会再下次打开 App 的时候运行.

现状

在谷歌原生手机上,即使你杀死进程,这个 api 也会一直执行你设置的定时任务,而在国产五大厂商的手机上则不然,会跟随进程被杀死。

  • 一加 Ace2V 手机国内版,开启 WorkManager 任务后,app 进程死了,Worker 不会运行了,等进程活了 Worker 继续执行。

查看 APP 进程是否存活

命令:adb shell ps -A | grep com.example.app

如果存在输出就表示该进程存活着

示例:

1
2
3
$adb shell ps -ef | grep me.hacket
u0_a406      12760 11830 0 10:01:01 136:0 00:00:00 install_server-7cce5689 ai.me.hacket.AppWidgets
u0_a406      13413   871 19 10:14:31 ?    00:00:00 ai.me.hacket.AppWidgets

各列的解释:

  1. USERu0_a406
    • 运行此进程的用户。例如,u0_a406 表示用户 406。
    • u0 代表用户空间,a406 代表用户 ID。
  2. PID13413
    • 进程 ID。这是该进程的唯一标识符。例如,13413 是该进程的 ID。
  3. PPID871
    • 父进程 ID。表示哪个进程启动了此进程。例如,871 是该进程的父进程 ID。
  4. PRI19
    • 进程优先级。优先级通常决定了调度程序在多任务环境中如何调度该进程。例如,19 是该进程的优先级。
  5. STIME10:14:31
    • 该进程的启动时间。例如,10:14:31 表示该进程在 10:14:31 启动。
  6. TTY?
    • 终端类型。? 表示该进程没有与特定终端关联。
  7. TIME00:00:00
    • 进程占用的总 CPU 时间。例如,00:00:00 表示该进程几乎没有使用 CPU 时间。
  8. CMDai.me.hacket.AppWidgets
    • 进程命令或进程名称。例如,ai.me.hacket.AppWidgets 是进程名称或应用包名。

操作对进程的影响

操作设备进程存活Worker 任务执行创建新的进程
最近任务列表划掉Pixel 6不存活正常执行创建新的进程
APP Info 界面 Force StopPixel 6不存活不执行不创建新的进程
adb killPixel 6   
     

WorkManager 入队 App 被杀死,任务不执行

任务创建并且入队后,app 被后台清理了,任务不会执行. 但是在 app 重新启动后,只要定时时间已经到达,任务就会在 app 启动的时候立刻执行.

要避开 queue 里面出现 100 个待调度的 job 的 case

需要注意的是队列里面任务(还在等待调度未执行的那种)不能超过 100 个,不然会 crash,这是 workmanager 代码的限制

国内手机 WorkManager 不调度情况

  1. 从任务管理器 (最近使用) 关掉: 原生的 Android 上 Worker 仍然会运行, 但是在 某些把这种操作当做强制停止的厂商 或 一些中国厂商 的机型上 are the Chinese manufacturers (Huawei, Oppo, Xiaomi…) supported? [113676489] - Issue Tracker, Worker 要等到下次打开 App 才会运行.
  2. 重启手机 (Worker 运行中的状态): 重启后 Worker 会继续运行.
  3. App 信息 -> 强制关闭: Worker 会再下次打开 App 的时候运行.
  4. 重启手机 (App 被强制关闭了): Worker 会再下次打开 App 的时候运行.

三星手机 job 未调度

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
  JOB #u0a311/156: cebade6 com.zzz/androidx.work.impl.background.systemjob.SystemJobService
    u0a311 tag=*job*/com.zzz/androidx.work.impl.background.systemjob.SystemJobService#156
    Source: uid=u0a311 user=0 pkg=com.zzz
    JobInfo:
      Service: com.zzz/androidx.work.impl.background.systemjob.SystemJobService
      Priority: 300 [DEFAULT]
      Requires: charging=false batteryNotLow=false deviceIdle=false
      Extras: mParcelledData.dataSize=180
      Minimum latency: +23h59m59s975ms
      Backoff: policy=1 initial=+30s0ms
      Has early constraint
    Required constraints: TIMING_DELAY UID_NOT_RESTRICTED [0x80100000]
    Preferred constraints:
    Dynamic constraints:
    Satisfied constraints: DEVICE_NOT_DOZING BACKGROUND_NOT_RESTRICTED TARE_WEALTH WITHIN_QUOTA UID_NOT_RESTRICTED [0xb500000]
    Unsatisfied constraints: TIMING_DELAY [0x80000000]
    Constraint history:
      -1m24s245ms = BACKGROUND_NOT_RESTRICTED [0x400000]
      -1m24s245ms = DEVICE_NOT_DOZING BACKGROUND_NOT_RESTRICTED [0x2400000]
      -1m24s245ms = DEVICE_NOT_DOZING BACKGROUND_NOT_RESTRICTED WITHIN_QUOTA [0x3400000]
      -1m24s245ms = DEVICE_NOT_DOZING BACKGROUND_NOT_RESTRICTED TARE_WEALTH WITHIN_QUOTA [0xb400000]
      -1m24s245ms = DEVICE_NOT_DOZING BACKGROUND_NOT_RESTRICTED TARE_WEALTH WITHIN_QUOTA UID_NOT_RESTRICTED [0xb500000]
    Tracking: TIME QUOTA UID_RESTRICT
    Implicit constraints:
      readyNotDozing: true
      readyNotRestrictedInBg: true
      readyComponentEnabled: true
    Started with foreground flag: false
    Standby bucket: ACTIVE
    Enqueue time: -1m24s245ms
    Run time: earliest=+23h58m35s730ms, latest=none, original latest=none
    Restricted due to: none.
    Ready: false (job=false user=true !restricted=true !pending=true !active=true !backingup=true comp=true)

用的单次发布:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
private fun queueOneTimeEngageServiceWorker(  
	workerName: String,  
	publishType: String,  
	context: Context  
) {  
	val workRequest =  
		OneTimeWorkRequestBuilder<EngageServiceWorker>()  
			.setInputData(workDataOf(PUBLISH_TYPE to publishType))  
//                .setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST)  
			.build()  
	L.v(TAG, "queueOneTimeEngageServiceWorker() - $workerName - $publishType")  
	WorkManager.getInstance(context)  
		.enqueueUniqueWork(workerName, ExistingWorkPolicy.REPLACE, workRequest)  
}

解决:需要及时发布的,不用 WorkManager,只有需要定时的才用 WorkManager

中国手机不调度

  • WorkManager 不太准时,可能一天只执行一次,全靠系统调度了,除非让用户同意后台启动。
  • 当我们从最近的应用程序托盘中清除该应用程序时,Android 系统会杀死该应用程序。当我们使用 Workmanager 安排任务时,如果用户在不知情的情况下从最近的应用程序中清除了该应用程序,则该应用程序将被终止并且操作无法正常工作。这种情况只发生在中文 ROM 中。
  • 我们遇到的唯一问题是一些中国原始设备制造商将滑动以从 “ 最近 “ 中关闭视为强制停止。发生这种情况时,WorkManager 将在下次应用程序启动时重新安排所有待处理的作业。鉴于这是一种 CDD 违规,考虑到 WorkManager 的客户端库,它无能为力。
  • 如果设备制造商决定修改原生 Android 以强制停止应用程序,WorkManager 将停止工作(JobScheduler、警报、广播接收器等也将停止工作)。没有办法解决这个问题。不幸的是,一些设备制造商会这样做,因此在这种情况下,WorkManager 将停止工作,直到下次启动应用程序为止。
  • 我尝试了很多方法来在应用程序被杀死/从 Android 后堆栈中删除后保持服务在后台运行。我尝试过 AlermManager 和广播接收器、JobScheduler 和 WorkManager。所有这些在某些手机中都可以完美运行,但在某些手机(具有自定义操作系统)中则无法正常工作。我试图找到任何解决方案或替代方法来解决上周的这个问题,但我仍然没有找到任何解决方案。请为此提供一些帮助。
  • are the Chinese manufacturers (Huawei, Oppo, Xiaomi…) supported?
  • java - Work Manager on chinese ROMs like Xiaomi and oppo, when under battery optimization, increase the scheduled delay of work by several hours - Stack Overflow

WorkManager 周期性任务拉起进程

周期性任务,App 杀死后,执行周期性任务时,会拉起 App 进程吗?

会拉起 App 的进程,需要注意的是,Application.onCreate 做了异步任务,但在 Worker 中需要依赖的

本文由作者按照 CC BY 4.0 进行授权