Android Jetpack系列--7. WorkManage

作者: 今阳说 | 来源:发表于2021-09-02 09:05 被阅读0次

    相关知识

    • 交换空间:当系统内存资源已被耗尽,但是又有额外的内存资源请求的时候,内存中不活动的页面会被移动到交换空间。交换空间是磁盘上的一块区域,因此其访问速度比物理内存慢。
    • 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()关闭可能保留的所有资源。
    

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

    相关文章

      网友评论

        本文标题:Android Jetpack系列--7. WorkManage

        本文链接:https://www.haomeiwen.com/subject/fveqwltx.html