Android Jetpack系列--7. WorkManager使用详解

相关知识

  • 交换空间:当系统内存资源已被耗尽,但是又有额外的内存资源请求的时候,内存中不活动的页面会被移动到交换空间。交换空间是磁盘上的一块区域,因此其访问速度比物理内存慢。
  • Android基于Linux内核,两者主要差别在于Android系统没有交换空间(Swap space)
  • 于是Android系统引入了OOM( Out Of Memory ) Killer 来解决内存资源被耗尽的问题。
  • 其作用是根据进程所消耗的内存大小以及进程的“visibility state”来决定是否杀死这个进程,从而达到释放内存的目的。
  • Activity Manager会给不同状态下的进程设置相对应的oom_adj 值:
# Define the oom_adj values for the classes of processes that can be
# killed by the kernel.  These are used in ActivityManagerService.
    setprop ro.FOREGROUND_APP_ADJ 0    //前台进程
    setprop ro.VISIBLE_APP_ADJ 1       //可见进程
    setprop ro.SECONDARY_SERVER_ADJ 2  //次要服务
    setprop ro.BACKUP_APP_ADJ 2        //备份进程
    setprop ro.HOME_APP_ADJ 4          //桌面进程
    setprop ro.HIDDEN_APP_MIN_ADJ 7    //后台进程
    setprop ro.CONTENT_PROVIDER_ADJ 14 //内容供应节点
    setprop ro.EMPTY_APP_ADJ 15        //空进程
  • 因此,1是应用占用内存越少,越可能存活下去;2是要合理设计后台任务进程

后台任务

  • Android开发基本都会用到后台任务,通常是不需要用户感知的耗时功能,任务完成后需要及时关闭任务回收资源,若使用不合理则可能造成电量大量消耗;
  • 之前我们处理后台任务一般使用service或线程池,尤其是service又不受Activity生命周期影响,被广泛用于数据缓存,统计及日志上传,消息推送,环境监听,进程保活拉起等,如此过于滥用给用户带来耗电快,被打扰,隱私泄露等问题,于是google在新的Android版本中逐渐增加限制,Doze机制,app Standby等,尤其是Android8.0不允许创建后台服务,无法在清单文件中注册隐式广播接收器; 所以我们所熟知的Servcie已经被弃用了,因为它不再被允许在后台执行长时间的操作,即便这是它最初被设计出来的目的, 除了ForegroundService之外,我们已经没有任何理由再去使用Service了;

Google推荐的不同场景后台任务的处理方案

  1. 需系统触发,不必完成:ThreadPool + Broadcast
  2. 需系统触发,必须完成,可推迟:WorkManager
  3. 需系统触发,必须完成,立即:ForegroundService + Broadcast
  4. 不需系统触发,不必完成:ThreadPool
  5. 不需系统触发,必须完成,可推迟:WorkManager
  6. 不需系统触发,必须完成,立即:ForegroundService

WorkManager简介

  • 一个可兼容,灵活,简单的延迟后台任务;
  • 能根据系统版本,选择不同实现方案,API高于23时采用JobScheduler,以帮助优化电池寿命和批处理作业,而在6.0以下系统版本则可自动切换为AlarmManager+Broadcast Receiver,最终都是交由Executor来执行;

WorkManager优点

  • 兼容性:兼容到api14
  • 可指定约束条件:如有网络才执行
  • 可指定执行次数和定时
  • 多个任务可使用任务链
  • 保证执行:如当前不满足或app挂掉后,下次满足条件再执行
  • 支持省电模式

WorkManager使用

导入依赖
implementation "androidx.work:work-runtime-ktx:2.5.0"
// optional - RxJava2 support
implementation "androidx.work:work-rxjava2:2.5.0"
// optional - Test helpers
androidTestImplementation "androidx.work:work-testing:2.5.0"
初始化配置
  • WorkManager 2.1.0以前的版本
// provide custom configuration
val myConfig = Configuration.Builder()
    .setMinimumLoggingLevel(android.util.Log.INFO)
    .build()

// initialize WorkManager
WorkManager.initialize(this, myConfig)
  • WorkManager 2.1.0及更高版本中已经默认使用provider进行初始化,通过ContentProvider初始化在之前的Android Jetpack系列--5. App Startup使用详解中有过介绍,通过查看其aar文件可以看到其AndroidManifest.xml中的provider配置如下
<provider
    android:name="androidx.work.impl.WorkManagerInitializer"
    android:authorities="${applicationId}.workmanager-init"
    android:directBootAware="false"
    android:exported="false"
    android:multiprocess="true"
    tools:targetApi="n" />
  • WorkManagerInitializer源码如下
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
public class WorkManagerInitializer extends ContentProvider {
    @Override
    public boolean onCreate() {
        // Initialize WorkManager with the default configuration.
        WorkManager.initialize(getContext(), new Configuration.Builder().build());
        return true;
    }

    @Nullable
    @Override
    public Cursor query(@NonNull Uri uri,
            @Nullable String[] projection,
            @Nullable String selection,
            @Nullable String[] selectionArgs,
            @Nullable String sortOrder) {
        return null;
    }

    @Nullable
    @Override
    public String getType(@NonNull Uri uri) {
        return null;
    }

    @Nullable
    @Override
    public Uri insert(@NonNull Uri uri, @Nullable ContentValues values) {
        return null;
    }

    @Override
    public int delete(@NonNull Uri uri,
            @Nullable String selection,
            @Nullable String[] selectionArgs) {
        return 0;
    }

    @Override
    public int update(@NonNull Uri uri,
            @Nullable ContentValues values,
            @Nullable String selection,
            @Nullable String[] selectionArgs) {
        return 0;
    }
}
  • 如果想要自定义初始化可以如下操作
//1. AndroidManifest.xml中覆盖其provider,并设置tools:node="remove"
//WorkManager 2.6以前版本
<provider
android:name="androidx.work.impl.WorkManagerInitializer"
android:authorities="${applicationId}.workmanager-init"
tools:node="remove" />
//WorkManager 2.6以后的版本
<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. Application实现Configuration.Provider接口
class MyApplication() : Application(), Configuration.Provider {
     override fun getWorkManagerConfiguration() =
           Configuration.Builder()
                .setMinimumLoggingLevel(android.util.Log.INFO)
                .build()
}
//You do not need to call `WorkManager.initialize()` yourself
//注意这里是实现Provider接口而不是像2.1版本一样手动调用initialize,
//当然如果我就要手动调用WorkManager.initialize也不会有报错,只是官方不推荐
//看看WorkManagerImpl.getInstance方法就知道了
public static @NonNull WorkManagerImpl getInstance(@NonNull Context context) {
    synchronized (sLock) {
        WorkManagerImpl instance = getInstance();
        if (instance == null) {
            Context appContext = context.getApplicationContext();
            //这里如果application没有实现Provider接口就直接抛出异常
            if (appContext instanceof Configuration.Provider) {
                initialize(
                        appContext,
                        ((Configuration.Provider) appContext).getWorkManagerConfiguration());
                instance = getInstance(appContext);
            } else {
                throw new IllegalStateException("WorkManager is not initialized properly.  You "
                        + "have explicitly disabled WorkManagerInitializer in your manifest, "
                        + "have not manually called WorkManager#initialize at this point, and "
                        + "your Application does not implement Configuration.Provider.");
            }
        }
        return instance;
    }
}
自定义Worker
  • 谷歌提供了四种Worker给我们使用,分别为:
    • 自动运行在后台线程的Worker
    • 结合协程的CoroutineWorker
    • 结合RxJava2的RxWorker
    • 以上三个类的基类的ListenableWorker
  • 我使用的是CoroutineWorker,然后重写doWork方法,其代码如下
class MyWorker(appContext: Context, workerParameters: WorkerParameters) :
    CoroutineWorker(appContext, workerParameters) {

    //执行在一个单独的后台线程里
    override suspend fun doWork(): Result {
        LjyLogUtil.d("doWork start")
        delay(5000)//模拟处理任务耗时
        LjyLogUtil.d("doWork end")
        return Result.success()
    }
}
  • doWork方法执行在一个单独的后台线程里
  • doWork的结果有三种,分别为:
    • Result.success():工作成功完成。
    • Result.failure():工作失败。
    • Result.retry():工作失败,根据其重试政策在其他时间尝试。
执行单次任务
  • 单次任务使用OneTimeWorkRequestBuilder创建workRequest,再通过WorkManager对象的enqueue()方法将其提交到WorkManager
class WorkManagerActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_work_manager)
        //执行单次任务
        val workRequest = OneTimeWorkRequestBuilder<MyWorker>().build()
        WorkManager.getInstance(this).enqueue(workRequest)
    }
}
定期循环任务
  • 可用于如定期上传日志,定期缓存预加载的数据,定期备份等
  • 使用PeriodicWorkRequest.Builder创建workRequest
val workRequest2 =
            PeriodicWorkRequest.Builder(MyWorker::class.java, 3, TimeUnit.SECONDS).build()
WorkManager.getInstance(this).enqueue(workRequest2)
  • 还可以定义具有灵活时间段的定期工作,如在每小时的最后 15 分钟内运行定期工作
val workRequest2: WorkRequest = PeriodicWorkRequest.Builder(
        MyWorker::class.java,
        1, TimeUnit.HOURS,
        15, TimeUnit.MINUTES
    ).build()
WorkManager.getInstance(this).enqueue(workRequest2)
设置任务约束条件
  • 如果不满足某个约束,WorkManager将停止工作,并且系统将在满足所有约束后重试工作
val constraints = Constraints.Builder()
            //设备空闲状态时运行
            .setRequiresDeviceIdle(true)
            //特定的网络状态运行
            //NOT_REQUIRED  不需要网络
            //CONNECTED 任何可用网络
            //UNMETERED 需要不计量网络,如WiFi
            //NOT_ROAMING   需要非漫游网络
            //METERED   需要计量网络,如4G
            .setRequiredNetworkType(NetworkType.CONNECTED)
            //电量充足时运行
            .setRequiresBatteryNotLow(true)
            //充电时执行
            .setRequiresCharging(true)
            //存储空间足够时运行
            .setRequiresStorageNotLow(true)
            //指定是否在(Uri指定的)内容更新时执行本次任务
            .addContentUriTrigger(Uri.EMPTY, true)
            .build()
val workRequest = OneTimeWorkRequestBuilder<MyWorker>()
        .setConstraints(constraints)
        .build()
WorkManager.getInstance(this).enqueue(workRequest)
分配输入数据
//1. 传入数据
val inputData = Data.Builder().putString("name", "ljy").build()
val workRequest = OneTimeWorkRequestBuilder<MyWorker>()
        .setInputData(inputData)
        .build()
WorkManager.getInstance(this).enqueue(workRequest)
//2.接收数据
class MyWorker(appContext: Context, workerParameters: WorkerParameters) :
        CoroutineWorker(appContext, workerParameters) {
    override suspend fun doWork(): Result {
        val name = inputData.getString("name")
        LjyLogUtil.d("doWork start:name=$name")
        delay(5000)
        LjyLogUtil.d("doWork end")
        return Result.success()
    }
}
延时执行
val workRequest = OneTimeWorkRequestBuilder<MyWorker>()
        .setInitialDelay(1, TimeUnit.SECONDS)
        .build()
WorkManager.getInstance(this).enqueue(workRequest)
设置tag
  • 可以用于取消工作或观察其进度,或者对任务进行分组
  • 如果有一组在逻辑上相关的工作,对这些工作项进行标记可能也会很有帮助
val workRequest = OneTimeWorkRequestBuilder<MyWorker>()
        .addTag("myWorker")
        .build()
WorkManager.getInstance(this).enqueue(workRequest)
重试和退避政策
  • 工作器返回 Result.retry(),系统将根据退避延迟时间和退避政策重新调度工作;
  • 退避延迟时间:指定了首次尝试后重试工作前的最短等待时间;
  • 退避政策: 定义了在后续重试过程中,退避延迟时间随时间以怎样的方式增长,WorkManager 支持 2 个退避政策,即 LINEAR 和 EXPONENTIAL;
  • 每个工作请求都有退避政策和退避延迟时间, 默认政策是 EXPONENTIAL,延迟时间为 10 秒,开发者可以在工作请求配置中替换此默认设置。
val workRequest4: WorkRequest = OneTimeWorkRequest.Builder(MyWorker::class.java)
    .setBackoffCriteria(
        BackoffPolicy.LINEAR,
        OneTimeWorkRequest.MIN_BACKOFF_MILLIS,
        TimeUnit.MILLISECONDS
    )
    .build()
创建任务链
  • 例如先执行任务1,再执行任务2和任务5,任务2执行完后执行任务3,任务4
val request1 = OneTimeWorkRequest.Builder(MyWorker::class.java).build()
val request2 = OneTimeWorkRequest.Builder(MyWorker::class.java).build()
val request3 = OneTimeWorkRequest.Builder(MyWorker::class.java).build()
val request4 = OneTimeWorkRequest.Builder(MyWorker::class.java).build()
val request5 = OneTimeWorkRequest.Builder(MyWorker::class.java).build()
val workConstraints = WorkManager.getInstance(this).beginWith(request1)
workConstraints.then(request2).then(listOf(request3, request4)).enqueue()
workConstraints.then(request5).enqueue()
唯一链
  • 同一时间内队列里不能存在相同名称的任务
    • WorkManager.enqueueUniqueWork():用于一次性工作
    • WorkManager.enqueueUniquePeriodicWork():用于定期工作
  • 应用场景:多次请求接口数据,如下单,更换头像等
  • 例如替换头像要经历本地文件读取,压缩,上传三个任务,下面组成一个串行的任务连,并且设置唯一标识,则代码如下:
val requestLoadFromFile = OneTimeWorkRequest.Builder(MyWorker::class.java).build()
val requestZip = OneTimeWorkRequest.Builder(MyWorker::class.java)
    .setInputData(createInputDataForUri()).build()
val requestSubmitToService = OneTimeWorkRequest.Builder(MyWorker::class.java).build()
WorkManager.getInstance(this).beginUniqueWork(
    "tagChangeImageHeader",
    ExistingWorkPolicy.REPLACE,
    requestLoadFromFile
)
    .then(requestZip)
    .then(requestSubmitToService)
    .enqueue()
  • 其中参数existingWorkPolicy有三种可选:
    • REPLACE:若相同,删除已有的任务,添加现有的任务;
    • KEEP:若相同,让已有的继续执行,不添加新任务;
    • APPEND:若相同,则添加新任务到已有任务链最末端;
Work状态
  • 当WorkManager把任务加入队列后,会为每个WorkRequest对象提供一个LiveData;
  • LiveData持有WorkStatus,通过观察该 LiveData, 我们可以确定任务的当前状态, 并在任务完成后获取所有返回的值;
ENQUEUED,//已加入队列
RUNNING,//运行中
SUCCEEDED,//已成功
FAILED,//已失败
BLOCKED,//已挂起
CANCELLED;//已取消
  • 状态的更改分为一次性任务的状态和周期性任务的状态:
    • 一次性任务状态:初始状态为 ENQUEUED,在满足其 Constraints 和初始延迟计时要求后立即运行,转为 RUNNING,
      根据工作的结果转为 SUCCEEDED、FAILED 状态, 如果结果是Result.retry() ,它可能会回到 ENQUEUED 状态;
      SUCCEEDED、FAILED 和 CANCELLED 均表示此工作的终止状态,WorkInfo.State.isFinished() 都将返回 true;
      在此过程中,随时都可以取消工作,取消后工作将进入 CANCELLED 状态;
    • 定期任务状态:因为是循环执行的,所以终止状态只有一个CANCELLED,其他和一次性任务状态是一样;
状态监听
// by id
WorkManager.getInstance(this).getWorkInfoById(request1.id)
WorkManager.getInstance(this).getWorkInfoByIdLiveData(request1.id)
// by name
WorkManager.getInstance(this).getWorkInfosForUniqueWork("sync");
WorkManager.getInstance(this).getWorkInfosForUniqueWorkLiveData("sync");
// by tag
WorkManager.getInstance(this).getWorkInfosByTag("syncTag")
WorkManager.getInstance(this).getWorkInfosByTagLiveData("syncTag")
WorkQuery
  • WorkManager 2.4.0 及更高版本还支持使用 WorkQuery 对象对已加入队列的作业进行复杂查询,
  • WorkQuery 支持按工作的标记、状态和唯一工作名称的组合进行查询
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.getInstance(this).getWorkInfos(workQuery)
更新进度 和 观察进度
  • Java: 使用Worker.setProgressAsync()更新进度
  • Kotlin:使用 CoroutineWorker.setProgress()更新进度,代码如下
class MyWorker(appContext: Context, workerParameters: WorkerParameters) :
        CoroutineWorker(appContext, workerParameters) {
        
    override suspend fun doWork(): Result {
        val name = inputData.getString("name")
        LjyLogUtil.d("doWork start:name=$name")
        val p0 = workDataOf("progressValue" to 0)
        val p1 = workDataOf("progressValue" to 20)
        val p2 = workDataOf("progressValue" to 40)
        val p3 = workDataOf("progressValue" to 60)
        val p4 = workDataOf("progressValue" to 80)
        val p5 = workDataOf("progressValue" to 100)
        setProgress(p0)
        delay(1000)
        setProgress(p1)
        delay(1000)
        setProgress(p2)
        delay(1000)
        setProgress(p3)
        delay(1000)
        setProgress(p4)
        delay(1000)
        setProgress(p5)
        LjyLogUtil.d("doWork end")
        return Result.success()
    }
}
  • 观察进度:和上面的监听任务状态是一样的,使用 getWorkInfoBy…() 或 getWorkInfoBy…LiveData(),代码如下
val workRequest10 = OneTimeWorkRequestBuilder<MyWorker>().build()
WorkManager.getInstance(this).enqueue(workRequest10)
WorkManager.getInstance(this)
    .getWorkInfoByIdLiveData(workRequest10.id)
    .observe(this, {
        if (it != null) {
            LjyLogUtil.d("workRequest10:state=${it.state}")
            val progress = it.progress;
            val value = progress.getInt("progressValue", 0)
            LjyLogUtil.d("workRequest10:progress=$value")
        }
    })
取消任务
//取消所有任务
WorkManager.getInstance(this).cancelAllWork()
//取消一组带有相同标签的任务
WorkManager.getInstance(this).cancelAllWorkByTag("tagName")
//根据name取消任务
WorkManager.getInstance(this).cancelUniqueWork("uniqueWorkName")
//根据id取消任务
WorkManager.getInstance(this).cancelWorkById(workRequest.id)
任务停止
  • 正在运行的任务可能因为某些原因而停止运行,主要的原因有以下一些:
1. 明确要求取消它,可以调用WorkManager.cancelWorkById(UUID)方法。
2. 如果是唯一任务,将 ExistingWorkPolicy 为 REPLACE 的新 WorkRequest 加入到了队列中时,旧的 WorkRequest 会立即被视为已取消。
3. 添加的任务约束条件不再适合。
4. 系统出于某种原因指示应用停止工作。
5. 当任务停止后,WorkManager 会立即调用 ListenableWorker.onStopped()关闭可能保留的所有资源。

我是今阳,如果想要进阶和了解更多的干货,欢迎关注微信公众号 “今阳说” 接收我的最新文章

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,847评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,208评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,587评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,942评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,332评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,587评论 1 218
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,853评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,568评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,273评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,542评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,033评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,373评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,031评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,073评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,830评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,628评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,537评论 2 269

推荐阅读更多精彩内容