Android Jetpack-WorkManager学习与实践

最近项目有一个需求是上报日志文件,并且在Android 14上AlarmManager使用需要主动申请权限,相关功能建议迁移到WorkManager来实现,于是学习和实践一下。

一、WorkManager简介(官网)

WorkManager 是适合用于持久性工作 的推荐解决方案。如果工作始终要通过应用重启和系统重新启动 来调度,便是持久性的工作 。由于大多数后台处理操作都是通过持久性工作完成的,因此 WorkManager 是适用于后台处理操作的主要推荐 API。

1、持久性工作的类型

WorkManager 可处理三种类型的持久性工作:

  • 立即执行:必须立即开始且很快就完成的任务,可以加急。
  • 长时间运行:运行时间可能较长(有可能超过 10 分钟)的任务。
  • 可延期执行:延期开始并且可以定期运行的预定任务。

下图大致表明了不同类型的持久性工作彼此之间的关系:

下表大致列出了各种工作类型:

类型 周期 使用方式
立即执行 一次性 OneTimeWorkRequestWorker。如需处理加急工作,请对 OneTimeWorkRequest 调用 setExpedited()
长时间运行 一次性或定期 任意 WorkRequestWorker。在工作器中调用 setForeground() 来处理通知。
可延期执行 一次性或定期 PeriodicWorkRequestWorker

2、WorkManager的特性

(1)更为简单且一致的 API

自己不用搞版本适配了

(2)工作约束(Constraints)

使用工作约束明确定义工作运行的最佳条件。例如,仅在设备采用不按流量计费的网络 连接时、当设备处于空闲状态 或者有足够的电量时运行。

(3)强大的调度(Scheduler)
WorkManager 允许您使用灵活的调度窗口调度工作,以运行一次性或重复工作。您还可以对工作进行标记或命名,以便调度唯一的、可替换的工作以及监控或取消工作组。

已调度的工作存储在内部托管的 SQLite 数据库中,由 WorkManager 负责确保该工作持续进行,并在设备重新启动后重新调度

此外,WorkManager 遵循低电耗模式等省电功能和最佳做法,因此您在这方面无需担心。

(4)加急工作(Expedited)

您可以使用 WorkManager 调度需在后台立即执行的工作。您应该使用加急工作来处理对用户来说很重要且会在几分钟内完成的任务

(5)灵活的重试策略(Retry)

有时工作会失败。WorkManager 提供了灵活的重试策略,包括可配置的指数退避策略(exponential backoff policy)。

(6)工作链(Chaining)

对于复杂的相关工作,您可以使用直观的接口将各个工作任务串联起来,这样您便可以控制哪些部分依序运行,哪些部分并行运行。下面是官网的示例代码:

kotlin 复制代码
val continuation = WorkManager.getInstance(context)
    .beginUniqueWork(
        Constants.IMAGE_MANIPULATION_WORK_NAME,     //自定义常量标记
        ExistingWorkPolicy.REPLACE,     //当前工作策略为替换
        OneTimeWorkRequest.from(CleanupWorker::class.java)    //只执行一次的WorkRequest
    ).then(OneTimeWorkRequest.from(WaterColorFilterWorker::class.java))   //通过then连接,数据也可以按链条传递
    .then(OneTimeWorkRequest.from(GrayScaleFilterWorker::class.java))
    .then(OneTimeWorkRequest.from(BlurEffectFilterWorker::class.java))
    .then(
        if (save) {
            workRequest<SaveImageToGalleryWorker>(tag = Constants.TAG_OUTPUT)
        } else /* upload */ {
            workRequest<UploadWorker>(tag = Constants.TAG_OUTPUT)
        }
    )

对于每项工作任务,您可以定义工作的输入和输出数据。将工作串联在一起时,WorkManager自动将输出数据从一个工作任务传递给下一个工作任务

(7)内置线程互操作性
WorkManager 无缝集成 CoroutinesRxJava,让您可以插入自己的异步 API,非常灵活。

3、使用 WorkManager 保证工作可靠性

WorkManager 适用于需要可靠运行的工作,即使用户导航离开屏幕、退出应用或重启设备也不影响工作的执行。例如:

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

WorkManager 不适用于那些可在应用进程结束时安全终止的进程内后台工作。它也并非对所有需要立即执行的工作都适用的通用解决方案。

4、与其他 API 的关系

虽然协程是适合某些用例的推荐解决方案,但您不应将其用于持久性工作。请务必注意,协程是一个并发框架,而 WorkManager 是一个持久性工作库。同样,AlarmManager 仅适合用于时钟或日历。

API 推荐使用场景 与 WorkManager 的关系
Coroutines 所有不需要持久的异步工作。 协程是在 Kotlin 中退出主线程的标准方式。不过,它们在应用关闭后会释放内存。对于持久性工作,请使用 WorkManager
AlarmManager 仅限闹钟。 WorkManager 不同,AlarmManager 会使设备从低电耗模式中唤醒。因此,它在电源和资源管理方面来讲并不高效。AlarmManager 仅适合用于精确闹钟或通知(例如日历活动)场景,而不适用于后台工作。

二、使用入门(官网)

1、导包

如果项目没有使用Gradle8不要升级到2.8及以上,后面博客中的代码都是2.7.1版本。

kotlin 复制代码
dependencies {
    val work_version = "2.8.1"

    // (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"
}

2、定义Worker

继承 Worker重写doWork() 方法。doWork() 方法在 WorkManager 提供的后台线程上异步运行。例下面的代码以创建上传图像的 Worker为例:

kotlin 复制代码
class UploadWorker(appContext: Context, workerParams: WorkerParameters):
       Worker(appContext, workerParams) {    //继承Worker
   override fun doWork(): Result {    //重写doWorker方法

       // Do the work here--in this case, upload the images.
       uploadImages()    //在子线程上传图像

       // Indicate whether the work finished successfully with the Result
       return Result.success()   //返回值必须是一个Result对象
   }
}

doWork() 返回的 Result 会通知 WorkManager 服务工作是否成功、失败或是应重试。

  • Result.success():工作成功完成。
  • Result.failure():工作失败。
  • Result.retry():工作失败,应根据其重试政策在其他时间尝试。

3、创建 WorkRequest

定义Worker后,必须使用 WorkManager 服务进行调度该工作才能运行。对于如何调度工作,WorkManager 提供了很大的灵活性。你可以将其安排为在某段时间内定期运行,也可以将其安排为仅运行一次。

不论以何种方式调度工作,都需要使用 WorkRequestWorker 定义工作单元,WorkRequest(及其子类)则定义工作的运行方式和时间(WorkManager库的使用核心思路) 。在最简单的情况下,你可以使用 OneTimeWorkRequest,如以下示例所示:

kotlin 复制代码
val uploadWorkRequest: WorkRequest =
   OneTimeWorkRequestBuilder<UploadWorker>()   //只执行一次的WorkRequest
       .build()

4、将 WorkRequest 提交给系统

接下来最后一步,需要使用enqueue() 方法将 WorkRequest 提交到 WorkManager

kotlin 复制代码
WorkManager
    .getInstance(myContext)
    .enqueue(uploadWorkRequest)

执行WorkRequest的确切时间取决于 WorkRequest 中使用的约束和系统优化方式。WorkManager 经过设计,能够在满足这些约束的情况下提供最佳行为。

三、WorkRequest的具体使用

WorkRequest 本身是抽象基类。该类有两个派生实现,可用于创建 OneTimeWorkRequestPeriodicWorkRequest 请求。顾名思义,OneTimeWorkRequest 适用于调度执行一次性的工作,而 PeriodicWorkRequest 则更适合调度以一定间隔重复执行的工作。

1、OneTimeWorkRequest一次性工作

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

kotlin 复制代码
val myWorkRequest = OneTimeWorkRequest.from(MyWork::class.java)

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

kotlin 复制代码
val uploadWorkRequest: WorkRequest =
   OneTimeWorkRequestBuilder<MyWork>()
       // Additional configuration
       .build()

2、调度加急工作

WorkManager 2.7.0 引入了加急工作的概念。这使 WorkManager 能够执行重要工作,同时使系统能够更好地控制对资源的访问权限。

加急工作具有以下特征:

  • 重要性:加急工作适用于对用户很重要或由用户启动的任务。
  • 速度 :加急工作最适合那些立即启动并在几分钟内完成的简短任务
  • 配额:限制前台执行时间的系统级配额决定了加急作业是否可以启动。
  • 电源管理:电源管理限制(如省电模式和低电耗模式)不太可能影响加急工作。
  • 延迟时间:系统立即执行加急工作,前提是系统的当前工作负载允许执行此操作。这意味着这些工作对延迟时间较为敏感,不能安排到以后执行。

在用户想要发送消息或附加的图片 时,可能会在聊天应用内使用加急工作。同样,处理付款或订阅流程的应用也可能需要使用加急工作。这是因为这些任务对用户很重要,会在后台快速执行,并需要立即开始执行。

看到这里可能会有点懵,不是说WorkManager是一个后台持久性工作的Api库吗?怎么发送消息和付款这种简短的任务也用WorkManager了?还有就是我不用WorkManager手动开启异步线程处理不可以吗,这二种方式的优缺点是什么?看下GPT怎么回答的:

(1)持久性工作和加急工作的区别以及为什么WorkManager可以用于这两种情况?

  1. 持久性工作

    • 持久性工作是指那些需要长时间运行或无法保证在应用的生命周期内完成的任务,通常在后台运行。这些任务可能包括数据同步、定期备份、上传/下载大文件等。
    • WorkManager是用于调度和管理这种类型的工作的理想工具。它确保工作在适当的时候运行,即使应用被杀死或设备重启也能够继续工作
    • 对于持久性工作,WorkManager提供了许多有用的功能,如灵活的重试策略、约束条件(例如在特定网络状态下运行工作)以及数据持久性保持等。
  2. 加急工作

    • 加急工作是指那些需要快速执行并且对用户体验至关重要的任务。这些任务可能包括发送聊天消息、上传图片、处理付款等
    • WorkManager也可以用于加急工作,尤其是在需要跨越应用生命周期或设备重启的情况下。WorkManager确保即使应用在后台或被销毁,这些加急工作也能够被调度和执行(比如无论如何也要让用户发送消息成功吧?)。
    • 加急工作可以通过设置约束条件和加急标志来告诉WorkManager优先执行它们,以确保它们立即开始执行。

总结一下,WorkManager是一个通用的工作调度库,它不仅适用于持久性工作,还适用于加急工作。对于加急工作,您可以使用WorkManager的一些功能来确保它们得到及时执行,以提高用户体验。它的灵活性使其成为处理各种类型后台任务的强大工具,无论是长时间运行的任务还是需要快速执行的任务。

(2)加急工作和开启异步线程处理后台工作有什么区别?

加急工作(Urgent Work)和手动开启异步线程处理后台工作是两种不同的方法,它们在处理后台工作时有一些重要的区别。下面是它们之间的主要区别:

  1. 工作调度和管理

    • 加急工作:使用WorkManager等工作调度库可以轻松地调度和管理工作。这些库提供了一套强大的API,允许您定义工作、设置约束条件(例如在特定网络状态下运行)以及处理工作的重试和错误情况。加急工作还可以在应用的整个生命周期内执行,即使应用被销毁或设备重启也能够继续工作。
    • 异步线程:手动开启异步线程处理后台工作需要更多的代码来管理线程的生命周期和任务的执行。您需要处理线程的创建、管理和销毁,并且需要考虑到一些设备性能和稳定性问题,例如线程泄漏和应用崩溃。
  2. 系统资源管理

    • 加急工作:工作调度库通常会考虑系统资源管理,以确保工作不会对设备性能和电池寿命产生负面影响。它们可以更好地协调和平衡后台工作,以避免过度占用CPU、内存和网络资源。
    • 异步线程:手动管理线程时,您需要自己注意资源管理,以防止应用消耗过多的系统资源,这可能会导致应用变得不稳定或影响用户体验。
  3. 可扩展性和灵活性

    • 加急工作:工作调度库提供了丰富的功能和配置选项,使其具有较高的可扩展性和灵活性。您可以定义工作链、设置不同的约束条件、设置重试策略等,以满足不同的需求。
    • 异步线程:手动管理线程的可扩展性和灵活性较低,因为您需要编写更多的自定义代码来处理不同情况。

综上所述,工作调度库(如WorkManager)通常是处理后台工作的首选方法,尤其是对于需要跨越应用生命周期或设备重启的任务。它们提供了更好的资源管理、可维护性和可扩展性,同时减少了开发人员需要编写的样板代码。手动开启异步线程处理后台工作可能更适用于一些简单的、不需要复杂调度和管理的任务。选择哪种方法取决于您的项目需求和复杂性。

(3)加急工作的配额是如何管理的? App都滥用加急怎么办?

系统必须先为加急工作分配应用执行时间 ,然后才能运行工作。执行时间并非无限制,而是受配额限制。如果您的应用使用其执行时间并达到分配的配额,在配额刷新之前,您无法再执行加急工作。这样,Android 可以更有效地在应用之间平衡资源。

每个应用均有自己的前台执行时间配额。可用的执行时间取决于待机模式存储分区和进程的重要性。

注意:当您的应用在前台运行时,配额不会限制加急工作的执行 。仅当应用在后台运行或移至后台时,执行时间配额才适用。因此,您应加急想在后台继续的工作。当应用在前台运行时,您可以继续使用 setForeground()

(4)思考和总结

通过以上的描述可以看出,不要狭隘的理解为WorkManager就是一个在后台不管是应用或是手机重启都能保证工作顺利执行的库,由于加急工作的加持,WorkManager是一个处理后台工作的首选方法。但也要注意需求的复杂性,如果用异步协程就能完成,那也没必要使用WorkManager了。后面会举例一些场景。

3、执行加急工作

WorkManager 2.7 开始,可以调用 setExpedited() 来声明 WorkRequest 使用加急作业来尽可能快的开始运行。以下代码段是一个使用 setExpedited() 的简单示例:

kotlin 复制代码
private fun testWorkManager() {
    val request = OneTimeWorkRequestBuilder<UploadWorker>()
        .setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST)  //OutOfQuotaPolicy定义加急工作超出配额的策略
        .build()

    WorkManager.getInstance(this)
        .enqueue(request)
}

class UploadWorker(appContext: Context, workerParams: WorkerParameters) :
    CoroutineWorker(appContext, workerParams) {    //继承Worker
    override suspend fun doWork(): Result {    //重写doWorker方法
        uploadImages()    //在子线程上传图像
        return Result.success()   //返回值必须是一个Result对象
    }

    private fun uploadImages() {
        //TODO 上传图片
    }
}

此请求就会变成加急工作。如果配额允许,它将立即开始在后台运行。

Android 12 之前,工作器中的 getForegroundInfoAsync()getForegroundInfo() 方法可让 WorkManager 在您调用 setExpedited() 时显示通知。

如果您想要请求任务作为加急作业运行,则所有的 ListenableWorker 都必须实现 getForegroundInfo 方法。否则可能会崩溃。如果使用的是CoroutineWorker,创建通知然后将它传递给doWork中的setForeground()即可,不需要实现getForegroundInfo() 方法。这样做会在Android 12之前的版本中创建通知。下面是一个完善了官网上传图片的一个小Demo:

①一个简单的通知的工具类NotificationUtils

kotlin 复制代码
//前台通知的Channel
private lateinit var mForegroundNotificationChannel: NotificationChannel

//普通通知的Channel
private lateinit var mNormalNotificationChannel: NotificationChannel

private val mNotificationManager by lazy {
    appContext.getSystemService(NotificationManager::class.java)
}

//通知的构建器
fun getNotificationManager(): NotificationManager = mNotificationManager

//创建前台通知
@RequiresApi(Build.VERSION_CODES.O)
fun createForegroundNotification(
    block: NotificationCompat.Builder.() -> NotificationCompat.Builder
): ForegroundInfo {
    val notificationCompat: NotificationCompat.Builder =
        createNotificationCompatBuilder(FOREGROUND_CHANNEL_ID, createForegroundChannel(), block)
    // 返回前台通知信息
    return ForegroundInfo(FOREGROUND_NOTIFICATION_ID_START, notificationCompat.build())
}

//刷新前台通知
@RequiresApi(Build.VERSION_CODES.O)
fun refreshForegroundNotification(
    block: NotificationCompat.Builder.() -> NotificationCompat.Builder
) {
    val notificationCompat: NotificationCompat.Builder =
        createNotificationCompatBuilder(FOREGROUND_CHANNEL_ID, createForegroundChannel(), block)
    getNotificationManager().notify(FOREGROUND_NOTIFICATION_ID_FINISH, notificationCompat.build())
}

//创建普通通知
@RequiresApi(Build.VERSION_CODES.O)
fun createNormalNotification(
    block: NotificationCompat.Builder.() -> NotificationCompat.Builder
): Notification {
    val notificationCompat: NotificationCompat.Builder =
        createNotificationCompatBuilder(NORMAL_CHANNEL_ID, createNormalChannel(), block)
    return notificationCompat.build()
}

//创建NotificationCompat.Builder
private fun createNotificationCompatBuilder(
    channelId: String,
    notificationChannel: NotificationChannel,
    block: NotificationCompat.Builder.() -> NotificationCompat.Builder
): NotificationCompat.Builder {
    val builder = NotificationCompat.Builder(appContext, channelId)
    // 创建通知,invoke中自己定义设置
    val notificationCompat: NotificationCompat.Builder = block.invoke(builder)
    //8.0以上创建NotificationChannel
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
        mNotificationManager.createNotificationChannel(notificationChannel)
    }
    return notificationCompat
}

//创建前台通知的Channel
@RequiresApi(Build.VERSION_CODES.O)
private fun createForegroundChannel(): NotificationChannel {
    if (!::mForegroundNotificationChannel.isInitialized) {
        mForegroundNotificationChannel = NotificationChannel(
            FOREGROUND_CHANNEL_ID,
            FOREGROUND_CHANNEL_NAME,
            NotificationManager.IMPORTANCE_HIGH
        ).also {
            mNotificationManager.createNotificationChannel(it)
        }
    }
    return mForegroundNotificationChannel
}

//创建普通通知的Channel
@RequiresApi(Build.VERSION_CODES.O)
private fun createNormalChannel(): NotificationChannel {
    if (!::mNormalNotificationChannel.isInitialized) {
        mNormalNotificationChannel = NotificationChannel(
            NORMAL_CHANNEL_ID,
            NORMAL_CHANNEL_NAME,
            NotificationManager.IMPORTANCE_DEFAULT
        ).also {
            mNotificationManager.createNotificationChannel(it)
        }
    }
    return mNormalNotificationChannel
}


//显示前台通知的CHANNEL_ID和CHANNEL_NAME
const val FOREGROUND_CHANNEL_ID = "FOREGROUND_CHANNEL_ID"
const val FOREGROUND_CHANNEL_NAME = "FOREGROUND_CHANNEL_NAME"

//显示普通通知的CHANNEL_ID和CHANNEL_NAME
const val NORMAL_CHANNEL_ID = "NORMAL_CHANNEL_ID"
const val NORMAL_CHANNEL_NAME = "NORMAL_CHANNEL_NAME"

//Worker打tag
const val WORKER_TAG = "WORKER_TAG"

//Worker携带数据的key
const val INPUT_DATA_KEY = "INPUT_DATA_KEY"
const val OUTPUT_DATA_KEY = "OUTPUT_DATA_KEY"

//notification的id
const val FOREGROUND_NOTIFICATION_ID_START = 100
const val FOREGROUND_NOTIFICATION_ID_FINISH = 101
const val NORMAL_NOTIFICATION_ID = 102

②实现CoroutineWorker的上传的类:

kotlin 复制代码
@RequiresApi(Build.VERSION_CODES.O)
class UploadFileWorker(private val context: Context, params: WorkerParameters) :
    CoroutineWorker(context, params) {

    //Worker执行任务
    override suspend fun doWork(): Result {
        var outData: Data = Data.EMPTY
        val cancel = WorkManager.getInstance(context).createCancelPendingIntent(id)
        showStartForegroundNotification().also {
            //显示开始上传的前台通知
            setForeground(it)
        }
        //上传文件,上传成功返回文件的文件名
        uploadFile(onProgress = {
            //显示普通通知:更新上传进度
            showProgressNormalNotification {
                setSmallIcon(R.drawable.ic_launcher_foreground)
                setContentTitle("正在上传文件:${inputData.getString(INPUT_DATA_KEY)}")
                setContentText("上传进度 $it%")
                setProgress(100, it, false)
                priority = NotificationCompat.PRIORITY_DEFAULT
                addAction(R.drawable.ic_launcher_foreground, "取消", cancel)  //可以取消
            }
        }, onSuccess = {
            //清除上传进度的普通通知
            getNotificationManager().cancel(NORMAL_NOTIFICATION_ID)
            //显示上传成功的前台通知
            showFinishForegroundNotification()
            //传递数据,告诉哪个文件上传成功
            outData = Data.Builder().putString(OUTPUT_DATA_KEY, it).build()
        })
        return Result.success(outData)
    }

    //模拟上传文件
    private suspend fun uploadFile(
        onProgress: (Int) -> Unit, onSuccess: suspend (String) -> Unit
    ) {
        for (i in 0..100) {
            delay(100L)
            if (i != 100) {
                onProgress.invoke(i)
            } else {
                onSuccess.invoke(inputData.getString(INPUT_DATA_KEY) ?: "")
            }
        }
    }

    //开始上传时的前台通知
    private fun showStartForegroundNotification() = createForegroundNotification {
        setContentTitle("上传文件")
        setContentText("开始上传文件:${inputData.getString(INPUT_DATA_KEY)}")
        priority = NotificationCompat.PRIORITY_HIGH
        setSmallIcon(R.drawable.ic_launcher_foreground)
    }

    //上传完成时的前台通知
    private fun showFinishForegroundNotification() = refreshForegroundNotification {
        clearActions()
        setContentTitle("${inputData.getString(INPUT_DATA_KEY)} 上传成功")
        setContentText("上传完成")
        priority = NotificationCompat.PRIORITY_HIGH
        setSmallIcon(R.drawable.ic_launcher_foreground)
        //点击通知的跳转
        setContentIntent(
            PendingIntent.getActivity(
                applicationContext,
                0,
                Intent(applicationContext, MainActivity::class.java),
                PendingIntent.FLAG_IMMUTABLE
            )
        )
    }

    //显示上传进度的普通通知
    @RequiresApi(Build.VERSION_CODES.O)
    private fun showProgressNormalNotification(
        block: NotificationCompat.Builder.() -> NotificationCompat.Builder
    ) {
        getNotificationManager().notify(NORMAL_NOTIFICATION_ID, createNormalNotification(block))
    }
}

③具体调用

kotlin 复制代码
@Route(path = ARouterPath.MainActivity)
class MainActivity : BaseActivity() {

    private val mWorkManager: WorkManager by lazy(LazyThreadSafetyMode.NONE) {
        WorkManager.getInstance(this)
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        //订阅
        createObserver()
        lifecycleScope.launch(Dispatchers.IO) {
            //延迟3秒是方便录制UI
            delay(3000)
            //开始任务
            enqueueUploadWork()
        }
    }

    private fun enqueueUploadWork() {
        //携带数据,比如下面的文件名
        val inputFileName = "dragon.jpg"
        //约束条件:需要联网
        val constraints =
            Constraints.Builder().setRequiredNetworkType(NetworkType.CONNECTED).build()
        //传递数据
        val data = Data.Builder().putString(INPUT_DATA_KEY, inputFileName).build()
        //执行WorkManager任务
        mWorkManager.beginUniqueWork(
            WORKER_TAG,    //Work的名称
            ExistingWorkPolicy.REPLACE,     //工作已经存在就替换
            OneTimeWorkRequestBuilder<UploadFileWorker>()
                .addTag(WORKER_TAG)   //打标签
                .setInputData(data)    //设置数据
                .setConstraints(constraints)   //设置约束条件
                .setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST)  //设置加急工作(配额)
                .build()
        ).enqueue()
    }

    /**
     * 监听Worker的进度
     */
    private fun createObserver() {
        mWorkManager.getWorkInfosByTagLiveData(WORKER_TAG).observe(this) {
            if (it.isNotEmpty()) {
                val workInfo = it[0]    //最新的工作执行状态
                when (workInfo.state) {
                    WorkInfo.State.SUCCEEDED -> {}   //成功
                    WorkInfo.State.BLOCKED -> {}   //被阻塞
                    WorkInfo.State.ENQUEUED -> {}  //进入队列
                    WorkInfo.State.RUNNING -> {}   //正在运行
                    WorkInfo.State.CANCELLED -> {}   //被取消
                    WorkInfo.State.FAILED -> {}   //失败
                }
            }
        }
    }

}

看一下具体的UI效果,注意前台通知和普通通知的处理。

4、PeriodicWorkRequest一定时间间隔重复工作

有时候需要在后台间隔一定时间周期性的做一件事,比如上传日志等,那使用它就再合适不过了。下面是一个简单的例子:

kotlin 复制代码
val saveRequest =
       PeriodicWorkRequestBuilder<SaveImageToFileWorker>(1, TimeUnit.HOURS)
    // Additional configuration
           .build()

上面的例子中就是以一个小时为间隔重复工作。不过工作的具体执行时间还是取决于约束条件和系统资源的占用情况。可以定义的最小的重复时间间隔为15分钟。

创建PeriodicWorkRequest时可以将flexIntervalrepeatInterval一起传递,更灵活的定义重复的间隔时间内的执行情况:

kotlin 复制代码
val myUploadWork = PeriodicWorkRequestBuilder<SaveImageToFileWorker>(
       1, TimeUnit.HOURS, // repeatInterval (the period cycle)
       15, TimeUnit.MINUTES) // flexInterval
    .build()

上面的代码的意思是在1个小时的间隔周期内的最后15分钟开始执行工作,直到当前周期结束。下面是官网的一张示意图:

5、重试和回退策略

如果要求 WorkManager 重试工作,可以 返回Result.retry()。然后,工作将根据回退延迟和回退政策重新安排。

  • 回退延迟是指在第一次尝试重试工作之前等待的最短时间。该值不能小于 10 秒(或 MIN_BACKOFF_MILLIS)。

  • 回退策略定义回退延迟应如何随着后续重试尝试的时间而增加。WorkManager 支持 2 种回退策略, LINEAR以及 EXPONENTIAL。举例,设置回退延迟为1分钟,如果设置回退策略为LINEAR,那么重试时间的增长曲线为1分钟、2分钟、3分钟、4分钟、5分钟......如果设置回退策略为EXPONENTIAL,那么重试时间的增长曲线为1分钟、2分钟、4分钟、8分钟、16分钟......

每个工作请求都有回退策略和回退延迟。默认策略是EXPONENTIAL延迟 30 秒,可以在工作请求配置中覆盖默认设置。下面是一个代码示例:

kotlin 复制代码
val myWorkRequest = OneTimeWorkRequestBuilder<MyWork>()
   .setBackoffCriteria(
       BackoffPolicy.LINEAR,    //回退策略:线性
       OneTimeWorkRequest.MIN_BACKOFF_MILLIS,   //回退延迟时间:
       TimeUnit.MILLISECONDS)
   .build()

6、工作状态

(1)一次性工作的状态

代码中是这样定义的

kotlin 复制代码
public enum State {

    /**
     * Used to indicate that the {@link WorkRequest} is enqueued and eligible to run when its
     * {@link Constraints} are met and resources are available.
     */
    ENQUEUED,    //队列中

    /**
     * Used to indicate that the {@link WorkRequest} is currently being executed.
     */
    RUNNING,     //运行中

    /**
     * Used to indicate that the {@link WorkRequest} has completed in a successful state.  Note
     * that {@link PeriodicWorkRequest}s will never enter this state (they will simply go back
     * to {@link #ENQUEUED} and be eligible to run again).
     */
    SUCCEEDED,     //成功

    /**
     * Used to indicate that the {@link WorkRequest} has completed in a failure state.  All
     * dependent work will also be marked as {@code #FAILED} and will never run.
     */
    FAILED,     //失败

    /**
     * Used to indicate that the {@link WorkRequest} is currently blocked because its
     * prerequisites haven't finished successfully.
     */
    BLOCKED,    //阻塞

    /**
     * Used to indicate that the {@link WorkRequest} has been cancelled and will not execute.
     * All dependent work will also be marked as {@code #CANCELLED} and will not run.
     */
    CANCELLED;      //取消

    /**
     * Returns {@code true} if this State is considered finished.
     *
     * @return {@code true} for {@link #SUCCEEDED}, {@link #FAILED}, and * {@link #CANCELLED}
     *         states
     */
    public boolean isFinished() {
        return (this == SUCCEEDED || this == FAILED || this == CANCELLED);
    }
}

官网有一张图很好的说明了各个状态:

(2)间歇性工作的状态

对于周期性工作,只有一种终止状态 CANCELLED。这是因为定期工作永远不会结束。每次运行后,无论结果如何,都会重新安排下次工作。官网也有一张图如下:

四、管理工作

1、Unique Work(独特的工作)

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

  • WorkManager.enqueueUniqueWork() 一次性工作
  • WorkManager.enqueueUniquePeriodicWork() 用于定期工作

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

  • uniqueWorkName-String类型:用于唯一标识工作请求的A。
  • existingWorkPolicy-enum类型:如果已经存在具有该唯一名称的未完成工作链,则告诉 WorkManager 该怎么做。有关详细信息,请参阅 冲突解决政策。
  • work-WorkRequest类型:具体工作安排。

下面是官网的一个示例:

kotlin 复制代码
val sendLogsWorkRequest =
       PeriodicWorkRequestBuilder<SendLogsWorker>(24, TimeUnit.HOURS)   //间隔24小时周期执行
           .setConstraints(Constraints.Builder()   //约束条件
               .setRequiresCharging(true)
               .build()
            )
           .build()
WorkManager.getInstance(this).enqueueUniquePeriodicWork(   //周期执行的特殊工作
           "sendLogs",       //工作的tag
           ExistingPeriodicWorkPolicy.KEEP,    //如果工作已经存在采用什么策略
           sendLogsWorkRequest      //具体的WorkRequest
)

ExistingWorkPolicy的具体类型:

kotlin 复制代码
public enum ExistingWorkPolicy {

    REPLACE,    //取消现有工作,执行新工作

    KEEP,    //执行现有工作,忽略新工作

    APPEND,    //现有工作执行完成后执行新工作,现有工作成为新工作的先决条件,如果现有工作成为CANCELLED或FAILED,则新工作也成为CANCELLED或FAILED
    
    APPEND_OR_REPLACE,     //现有工作执行完成后执行新工作,现有工作不会成为新工作的先决条件,如果现有工作成为CANCELLED或FAILED,新工作会加入开始执行
}

(1)问题一:beginUniqueWorkenqueueUniqueWorkenqueueUniquePeriodicWork都能开启工作,那它们的区别是什么(GPT答案)?

beginUniqueWork(), enqueueUniqueWork()enqueueUniquePeriodicWork() 都是 WorkManager 的 API。它们都用于排队和触发唯一的后台工作任务,但是它们的行为略有不同。

以下是每个函数的基本行为:

  • beginUniqueWork(): 这个方法会开始一个新的唯一工作任务。如果在开始任务时已经有一个相同名称的任务在运行,则根据你提供的 ExistingWorkPolicy,可以选择要么使当前任务替换现有任务,要么保持现有任务并忽略新任务,或者将新任务附加到现有任务。
  • enqueueUniqueWork(): 该函数与 beginUniqueWork() 相似,但它不需要一个 WorkContinuation 对象。它直接接受一个 OneTimeWorkRequest 对象,并直接将其加入队列。
  • enqueueUniquePeriodicWork(): 该函数用于安排一个唯一的定期工作任务。这意味着它会定期运行,而不是只运行一次。其行为与 beginUniqueWork()enqueueUniqueWork() 类似,但是它接受一个 PeriodicWorkRequest 对象,而不是 OneTimeWorkRequest

以下是每个函数的使用示例:

kotlin 复制代码
val request = OneTimeWorkRequestBuilder<MyWork>().build()
val requestPeriodic = PeriodicWorkRequestBuilder<MyWork>(1, TimeUnit.HOURS).build()

// beginUniqueWork
WorkManager.getInstance(context).beginUniqueWork(
    "MyUniqueWork", 
    ExistingWorkPolicy.REPLACE, 
    request
).enqueue()

// enqueueUniqueWork
WorkManager.getInstance(context).enqueueUniqueWork(
    "MyUniqueWork", 
    ExistingWorkPolicy.REPLACE, 
    request
)

// enqueueUniquePeriodicWork
WorkManager.getInstance(context).enqueueUniquePeriodicWork(
    "MyUniquePeriodicWork", 
    ExistingPeriodicWorkPolicy.REPLACE, 
    requestPeriodic
)

(2)问题二:如何设计后台的串行、并行Worker?

串行Worker代码:

kotlin 复制代码
class FirstWorker(context: Context, params: WorkerParameters) :
    CoroutineWorker(context, params) {

    //Worker执行任务
    override suspend fun doWork(): Result {
        "Worker:FirstWorker start".logd
        delay(2000)
        val data = Data.Builder().putString("KEY", "This is FirstWorker Data").build()
        //Result.failure会打断串行的Worker链条,所以务必返回Result.success。
        //可以用Data携带的序列化数据来标记Worker的成功与失败,但序列化数据最大不能超过10kb
        //return Result.failure(data)
        return Result.success(data)       <------注意这里只能返回Result.success,具体看上面的注释
    }
}
kotlin 复制代码
class SecondWorker(context: Context, params: WorkerParameters) :
    CoroutineWorker(context, params) {

    //Worker执行任务
    override suspend fun doWork(): Result {
        "Worker:SecondWorker start".logd
        delay(3000)
        "Worker:SecondWorker receive data is "${inputData.getString("KEY")}"".logd
        return Result.success()
    }
}
kotlin 复制代码
class MainActivity : BaseActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        //开始任务
        enqueueUploadWork()
    }

    private fun enqueueUploadWork() {
        // 链式调用
        val firstWorkerRequest = OneTimeWorkRequestBuilder<FirstWorker>().build()
        val secondWorkerRequest = OneTimeWorkRequestBuilder<SecondWorker>().build()

        WorkManager.getInstance(this)
            .beginUniqueWork(WORKER_TAG, ExistingWorkPolicy.REPLACE, firstWorkerRequest)
            .then(secondWorkerRequest)         <------用then链接
            .enqueue()
        "Worker:enqueue".logd
    }

}

输出日志

kotlin 复制代码
09:43:55.940 30197-30197   Worker:enqueue
09:43:56.022 30197-30249   Worker:FirstWorker start    ---|开始
09:43:58.047 30197-30249   Worker:SecondWorker start   ---|间隔2秒
09:44:01.050 30197-30249   Worker:SecondWorker receive data is "This is FirstWorker Data"   ---|间隔3秒

注意整个串行链条上只能传播Result.success不然链条就会被打破。

并行Worker代码就方式很多了,最简单的就是多个enqueue直接运行了,下面看看方式之一:

kotlin 复制代码
private fun enqueueUploadWork() {
    val workManager = WorkManager.getInstance(this)

    // 创建两个 Worker
    val workRequest1 = OneTimeWorkRequestBuilder<FirstWorker>().build()
    val workRequest2 = OneTimeWorkRequestBuilder<SecondWorker>().build()

    // 创建两个 WorkContinuation
    val continuation1 = workManager.beginWith(workRequest1)
    val continuation2 = workManager.beginWith(workRequest2)

    // 合并这两个 WorkContinuation,使它们并行执行
    val combined = WorkContinuation.combine(listOf(continuation1, continuation2))

    // 最后,将合并后的 WorkContinuation 加入队列
    combined.enqueue()

    //监听执行结果
    combined.workInfosLiveData.observe(this) {
        it.forEach { workInfo ->
            when(workInfo.id){
                workRequest1.id->{
                    "Worker:workRequest1:State=${workInfo.state}".logd
                }
                workRequest2.id->{
                    "Worker:workRequest2:State=${workInfo.state}".logd
                }
            }
        }
    }
}

输出日志

kotlin 复制代码
14:11:10.840 11321-11321   Worker:workRequest2:State=ENQUEUED
14:11:10.840 11321-11321   Worker:workRequest1:State=ENQUEUED
14:11:10.841 11321-11321   Worker:workRequest2:State=RUNNING
14:11:10.841 11321-11321   Worker:workRequest1:State=RUNNING
14:11:10.847 11321-11364   Worker:FirstWorker start
14:11:10.848 11321-11368   Worker:SecondWorker start
14:11:12.853 11321-11359   Worker result SUCCESS for Work [ id=f976405e-4385-474f-996b-90329c330cdf, tags={ com.example.testkotlin.utils.FirstWorker } ]
14:11:12.864 11321-11321   Worker:workRequest2:State=RUNNING
14:11:12.864 11321-11321   Worker:workRequest1:State=SUCCEEDED
14:11:13.849 11321-11368   Worker:SecondWorker receive data is "null"     <------注意这里
14:11:13.850 11321-11347   Worker result FAILURE for Work [ id=aeef4f4e-3c57-4a24-8405-97c54a23cd3e, tags={ com.example.testkotlin.utils.SecondWorker } ]
14:11:13.868 11321-11321   Worker:workRequest2:State=FAILED
14:11:13.868 11321-11321   Worker:workRequest1:State=SUCCEEDED

因为多Work(代码中多个beginWith)并发的关系,导致Data的数据不能共享,所以取到的数据为nullData只能在同一任务链的work(例如单一的beginUniqueWork)间传递。

2、更新Worker

有时候需要更新整个Worker应该怎么办? 下面是以修改约束条件为例的实现步骤,注意更新Worker在大于等于2.8.0的版本才支持,下面是代码:

kotlin 复制代码
// 创建OneTimeWorkRequest
val workRequest = OneTimeWorkRequestBuilder<FirstWorker>().build()
//获取已经存在的workRequest的id
val existingWorkRequestId = workRequest.id        <------id
//创建新的约束条件
val newConstraints = Constraints.Builder()
    .setRequiresCharging(false)
    .build()
//使用已存在的id创建WorkRequest
val updatedWorkRequest: WorkRequest =
    OneTimeWorkRequestBuilder<FirstWorker>()
        .setConstraints(newConstraints)
        .setId(existingWorkRequestId)          <------使用之前的id
        .build()
//更新WorkerRequest
workManager.updateWork(updatedWorkRequest)

3、取消和停止Worker

kotlin 复制代码
// by id
workManager.cancelWorkById(syncWorker.id)

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

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

取消和停止的监听和处理

kotlin 复制代码
class FirstWorker(context: Context, params: WorkerParameters) :
    CoroutineWorker(context, params) {                   <------这里有区别

    //Worker执行任务
    override suspend fun doWork(): Result {
        if (isStopped) {
            //Worker被回调cancel等正在停止, 可以释放资源等
        }
        return Result.success()
    }
}


class FirstWorker2(context: Context, params: WorkerParameters) :
    Worker(context, params) {                            <------这里有区别

    //Worker执行任务
    override fun doWork(): Result {
        if (isStopped) {
            //Worker被回调cancel等正在停止, 可以释放资源等
        }
        return Result.success()
    }

    //如果实现Worker,可以重写onStopped方法
    override fun onStopped() {
        super.onStopped()
        //这个方法将在 doWork 方法之后调用,即使 doWork 方法正常返回 success 也会被调用
    }
}

五、实际应用

1、使用WorkerManager做启动优化

以前有手动实现启动器的方案,现在用WorkerManager就简单太多了。

2、上传文件、上传日志、更新数据库、优化服务等

很多可以放在后台的任务都可以丢到后台去了,优化前台的用户使用体验。Service中的很多后台工作也可以放在WorkerManager中了,Service专心处理与前端的交互即可,比如听歌的时候下载歌曲。

3、优化AlarmManager

Android 14精准闹钟AlarmManager需要主动申请权限,当用户不授予这个权限的时候之前通过AlarmManager实现的功能就都不能用了,比如之前通过AlarmManager实现的SocketPing功能就不能用了,现在需要迁移到WorkManager。下面是一个简单代码的例子:

kotlin 复制代码
class PingWorker(context: Context, workerParams: WorkerParameters) :
    CoroutineWorker(context, workerParams) {
    override suspend fun doWork(): Result =
        suspendCancellableCoroutine { continuation ->     //阻塞式协程,挂起
            AlarmPingSender.clientComms?.checkForActivity(object : IMqttActionListener {   //阻塞等待ping的结果
                override fun onSuccess(asyncActionToken: IMqttToken?) {
                    continuation.resume(Result.success())   //有结果恢复协程,回调结果
                }

                override fun onFailure(asyncActionToken: IMqttToken?, exception: Throwable?) {
                    continuation.resume(Result.failure())    //有结果恢复协程,回调结果
                }
            }) ?: kotlin.run {
                continuation.resume(Result.failure())
            }
        }
}

//开启Work
workManager.enqueueUniqueWork(
    PING_JOB,
    ExistingWorkPolicy.KEEP,
    OneTimeWorkRequest
        .Builder(PingWorker::class.java)
        .build()
)

总结

初次学习并实践,如有错误欢迎指正。

参考了以下内容

官网学习资料

Android WorkManager入门与实践

相关推荐
深海呐5 小时前
Android AlertDialog圆角背景不生效的问题
android
ljl_jiaLiang5 小时前
android10 系统定制:增加应用使用数据埋点,应用使用时长统计
android·系统定制
花花鱼5 小时前
android 删除系统原有的debug.keystore,系统运行的时候,重新生成新的debug.keystore,来完成App的运行。
android
落落落sss6 小时前
sharding-jdbc分库分表
android·java·开发语言·数据库·servlet·oracle
消失的旧时光-19438 小时前
kotlin的密封类
android·开发语言·kotlin
服装学院的IT男10 小时前
【Android 13源码分析】WindowContainer窗口层级-4-Layer树
android
CCTV果冻爽11 小时前
Android 源码集成可卸载 APP
android
码农明明11 小时前
Android源码分析:从源头分析View事件的传递
android·操作系统·源码阅读
秋月霜风12 小时前
mariadb主从配置步骤
android·adb·mariadb
Python私教13 小时前
Python ORM 框架 SQLModel 快速入门教程
android·java·python