社区说|浅谈 WorkManager 的设计与实现:系统概述

反思 系列博客是一种看似 “内卷” ,但却 效果显著 的学习方式,该系列来源和目录请参阅 这儿 。

窘境

作为一名 Android 开发者,即便你没有用过,也必定对 WorkManager 耳熟能详。

自2018年发布以来,作为 Google 官方推出的架构组件,它未像 LiveDataViewModel 相同广泛运用。究其原因,一起来看 官方 开端对 WorkManager 的描述:

WorkManager 用于履行可 推迟异步 的后台使命。它供给了一个 牢靠可调度 的后台使命履行环境,能够处理 即便在运用退出或设备重启后仍需求运转 的使命。

快速提炼重点,咱们得到了什么?

WorkManager,能够处理后台使命,这些使命哪怕手机重启也必定会履行?

看完简介,我的心里毫无波澜,”关机重启仍会履行” 的确很不错,but who cares ? 我底子用不到这些。

它给人的第一印象并不冷艳,甚至能够说 平平无奇 ,不管是相亲市场仍是技术范畴,这都十分致命。

经过一系列的实践和研讨后,回过头再看 WorkManager,笔者以为 WorkManager 是传统 Android 范畴内学院派编程风格的代表作,是沧海遗珠。它为 后台使命的处理和调度 供给了一个优异的处理计划。

即便如此,WorkManager 仍和 Paging 面临着相同的 窘境: 难以推广、默默无闻。简略的项目用不到,杂乱的项目经过若干年的沉积,该范畴早已运用了其它计划,学习和迁移本钱过高,以至不被需求。

——时至今日,社区内除了若干 运用简介源码分析 的博客单篇,咱们仍很难找到其 实战进阶最佳实践 的相关系列。

目的

本文笔者将经过针对 Android 的后台使命办理机制,进行一个系统性的分析和规划。

终究的目的,并非是让读者将 WorkManager 强行引进和运用,而是对 后台使命的办理和调度东西 (后文简称 后台使命库 )有一个清晰的认知——即便从未运用过,在将来的某一天,遇到相似的事务诉求时,也能快速形成一个清晰的思路和计划。

基本概念

想要构建一个优异的后台使命库,需求依靠不断的迭代、优化和扩展,终究成为一个灵敏、完善的东西。

那么 后台使命库 需求供给哪些功用,为什么要规划这些功用?

第一个映入眼帘的概念是:线程调度,望文生义,它是后台异步使命的柱石。

1.线程调度

举个比方,你担任的是一个视频类APP的研发,开端的事务诉求如下:

APP 的日志上报。

需求清晰明晰,显然,日志上报是一个 后台异步使命,在子线程进行 IO 操作: Log 文件本地的读写,以及上传到服务器。

这儿咱们引进 使命履行器(TaskExecutor)的概念:其用于履行后台使命的详细逻辑。一般,咱们运用 线程池 来办理使命的线程,并供给线程的 创立毁掉调度 等功用:

// androidx.work.impl.utils.taskexecutor.WorkManagerTaskExecutor
public class WorkManagerTaskExecutor implements TaskExecutor {
  final Handler mMainThreadHandler = new Handler(Looper.getMainLooper());
  // 1.可切换主线程
  private final Executor mMainThreadExecutor = new Executor() {
    @Override
    public void execute(@NonNull Runnable command) {
        mMainThreadHandler.post(command);
    }
  };
  // 2.可切换后台作业线程
  private final Executor mBackgroundExecutor = Executors.newFixedThreadPool(
                Math.max(2, Math.min(Runtime.getRuntime().availableProcessors() - 1, 4)),
                createDefaultThreadFactory(isTaskExecutor));
}

为前进可读性,本文的示例代码都有 大幅精简 ,读者应尽量防止「只见树木不见森林」,以理解规划理念为主。

这儿咱们为组件供给了最根底的线程调度的才能,便于内部完结 主线程后台线程 的切换。其间咱们为后台线程申请了一个线程池,并依据可用处理器中心数量来设置恰当的线程数(一般一起最多履行的使命数为4),以充分利用设备的性能。

2.使命行列和串行化

接下来咱们规划一个使命行列,确保能够不断接收新的使命,并及时分发给空闲的后台线程履行:

public class ArrayDeque<E> extends AbstractCollection<E> implements Deque<E> {
  public boolean add(E e);
  public boolean offer(E e);
  public E remove();
  public E poll();
}

很经典的一个行列接口,WorkManager 也并未独自造轮子,而是借用了 JavaArrayDeque 类。这是一个十分经典的完结类,在诸多大名鼎鼎的东西库中都有它的身影(比方RxJava),限于篇幅不展开,感兴趣的读者可自行检查源码。

读者可预见到的是,后台使命的创立和增加的时机是无法控制的,加上 ArrayDeque 规划之初都并未考虑线程同步,因而现在的规划将会有 线程安全问题

因而,后台使命的入列和履行,必须借用一个新的人物确保串行,就像单线程相同。

完结计划十分简略,经过署理和加锁,供给一个TaskExecutor的包装类即可:

public class SerialExecutorImpl implements SerialExecutor {
  // 1. 使命行列
  private final ArrayDeque<Task> mTasks;
  // 2. 后台线程的Executor,即上文中数量为4的线程池
  private final Executor mBackgroundExecutor;
  // 3.锁对象
  @GuardedBy("mLock")
  private Runnable mActive;
  @Override
  public void execute(@NonNull Runnable command) {
      // 不断地入列、出列和使命履行
      synchronized (mLock) {
          mTasks.add(new Task(this, command));
          if (mActive == null) {
            if ((mActive = mTasks.poll()) != null) {
                mExecutor.execute(mActive);
            }
          }
      }
  }
}

3.使命状况、类型和成果回调

下一步,咱们对所重视的使命状况进行一个罗列,不难得出,咱们重视的状况大致有:使命开端(Enqueued)、使命履行中(Running)、使命成功(Successded)、使命失败(Failed)、使命撤销(Cancelled)等几种:

社区说|浅谈 WorkManager 的设计与实现:系统概述

请留意,上文中的日志上报咱们界说成了 一次性作业,即只履行一次,履行结束即永久结束。

实际上,一次性作业 并不能涵盖所有的事务场景,举例来说,作为一个视频类的 APP,咱们需 守时 对用户的播映进展进行一次记载或上报,确保用户即便杀掉APP,下次仍在终究记载的播映方位继续播映。

这儿咱们引进了 守时使命 的概念,它只要一个终止状况 CANCELLED。这是因为守时作业永远不会结束。每次运转后,不管成果怎么,系统都会从头对其进行调度。

社区说|浅谈 WorkManager 的设计与实现:系统概述

终究,咱们将后台使命的履行笼统为一个接口,开发者完结 doWork 接口,完结详细后台事务,如日志上报等,并返回详细的成果:

public abstract class Worker {
    // 后台使命的详细完结,`WorkerManager`内部进行了线程调度,履行在作业线程.
    @WorkerThread
    public abstract @NonNull Result doWork();
}
public abstract static class Result {
    @NonNull
    public static Result success() {
        return new Success();
    }
    @NonNull
    public static Result retry() {
        return new Retry();
    }
    @NonNull
    public static Result failure() {
        return new Failure();
    }
}

耐久化

现在为止,咱们完结了一个简化版、根据内存中行列的后台使命库,接下来咱们将针对 后台使命耐久化 的必要性,进行进一步的评论。

1.非即时使命

第一步咱们需求引进 非即时使命 的概念。

作为互联网的从业者,读者对相似的提示弹窗应该并不生疏:

您手机/PC的新版别已下载结束:「当即安装」、「守时安装」、「稍后提示我」

显然,这是一个常见的功用,用户挑选后,运用或系统的后台需在未来的某个时刻点,晋级或提示用户。假如和前文中的使命类型进行区分,前者咱们能够概括为 即时使命,后者则可称之为 延时使命非即时使命

非即时使命的诉求,将会使咱们现有 后台使命库杂乱度呈指数级提高 ,但这是 必要 的,原因如下:

首要,上文中 守时使命 也属于非即时使命的范畴,尽管该使命是当即履行并等候的,但实际上其真正的事务逻辑,仍是未来的某个时刻点触发;其次,也是最重要的一点,作为一个健壮的后台使命库,和 即时使命 相比,对 非即时使命 供给支撑的优先级要高得多。

——这好像违反直觉,在日常开发中, 即时使命 好像才是主流,但咱们忽视了一个事实,资源并非无限

在文章的开端,咱们构建了基本的线程调度才能,并创立了一个数量为 4 的线程池。但随着事务杂乱度的提高,线程池或许会一起履行多个使命,这意味着部分晚入列、或优先级低的使命,会经常性等候前面的使命履行结束。

严厉意义上讲,此时,即时使命都转化为了非即时使命,再进一步笼统,所有即时使命都对错即时使命

万物皆可异步,是异步编程的一个经典概念,该思想在 HandlerRxJava协程 中都有表现。

即时使命被延时履行是合理的吗?关于后台使命而言,是十分合理的,假如开发者有清晰的诉求, 必须当即 履行某段事务逻辑,那么就不应该用 后台使命库,而是直接在内存中调用这块代码。

2.耐久化存储

当后台使命或许被延时履行,考虑下一个问题,怎么确保使命履行的牢靠性?

终极处理计划必定是 后台使命耐久化,经过本地文件或许数据库存储后,即便进程被用户杀掉或系统回收,在合适的时机,APP总是能够将使命恢复并重建。

考虑到安全性,WorkManager 终究挑选运用了 Room 数据库,并且规划保护了一个十分杂乱的 Database,简略罗列下中心的 WorkSpec 表:

@Entity(indices = [Index(value = ["schedule_requested_at"]), Index(value = ["last_enqueue_time"])])
data class WorkSpec(
    // 1.使命履行的状况,ENQUEUED/RUNNING/SUCCEEDED/FAILED/CANCELLED
    @JvmField
    @ColumnInfo(name = "state")
    var state: WorkInfo.State = WorkInfo.State.ENQUEUED,
    // 2.Worker的类名,便于反射和日志打印
    @JvmField
    @ColumnInfo(name = "worker_class_name")
    var workerClassName: String,
    // 3.输入参数
    @JvmField
    @ColumnInfo(name = "input")
    var input: Data = Data.EMPTY,
    // 4.输出参数
    @JvmField
    @ColumnInfo(name = "output")
    var output: Data = Data.EMPTY,
    // 5.守时使命
    @JvmField
    @ColumnInfo(name = "initial_delay")
    var initialDelay: Long = 0,
    // 6.守时使命
    @JvmField
    @ColumnInfo(name = "interval_duration")
    var intervalDuration: Long = 0,
    // 7.束缚关系
    @JvmField
    @Embedded
    var constraints: Constraints = Constraints.NONE,
    // ...
)

规划好字段后,接下来咱们规划其操作类 WorkSpecDao :

@Dao
interface WorkSpecDao {
  // ...
  @Insert(onConflict = OnConflictStrategy.IGNORE)
  fun insertWorkSpec(workSpec: WorkSpec)
  @Query("SELECT * FROM workspec WHERE id=:id")
  fun getWorkSpec(id: String): WorkSpec?
  @Query("SELECT id FROM workspec")
  fun getAllWorkSpecIds(): List<String>
  @Query("UPDATE workspec SET state=:state WHERE id=:id")
  fun setState(state: WorkInfo.State, id: String): Int
  @Query("SELECT state FROM workspec WHERE id=:id")
  fun getState(id: String): WorkInfo.State?
  // ...
}

细心的读者会发现,WorkSpecDao 的规划中除了声明惯例的 InsertQueryUpdate 等,并没有 DELETE 类型的操作。

读者经过认真考虑后,可得该规划是合理的——因为有 setState() 可更新使命的状况,已完结或撤销的作业无需删除,而是经过 SQL 句子,灵敏分类按需获取,如:

@Dao
interface WorkSpecDao {
  // ...
  // 获取全部履行中的使命
  @Query("SELECT * FROM workspec WHERE state=RUNNING")
  fun getRunningWork(): List<WorkSpec>
  // 获取全部近期已完结的使命
  @Query(
      "SELECT * FROM workspec WHERE last_enqueue_time >= :startingAt AND state IN COMPLETED_STATES ORDER BY last_enqueue_time DESC"
  )
  fun getRecentlyCompletedWork(startingAt: Long): List<WorkSpec>
  // ...
}

这可称之额定收获,经过 Room 的耐久化存储,在确保了使命能够被稳定履行的一起,还可对所有使命进行备份,然后向开发者供给更多额定的才能。

准确来说,WorkManager 内部的 Dao 供给了 Delete 方法,但并未直接露出给开发者,而是用于内部处理 使命间的抵触 问题,这个后文再提。

优先级办理

下面咱们针对使命的 优先级 进一步进行评论。

尽管上文清晰说了,关于需求当即履行的行为,不应该作为后台使命,而是应该直接履行对应的事务代码块——看起来优先级机制并非刚需。

但实际上,这种机制依然有必定的必要性。

1.束缚条件

提到 束缚条件,熟悉 JobScheduler 的开发者对此不会感到生疏,

束缚条件 概念
NetworkType 束缚运转作业所需的网络类型。例如 Wi-Fi (UNMETERED)。
BatteryNotLow 假如设置为 true,那么当设备处于“电量不足形式”时,作业不会运转。
RequiresCharging 假如设置为 true,那么作业只能在设备充电时运转。
DeviceIdle 假如设置为 true,则要求用户的设备必须处于空闲状况,才能运转作业。在运转批量操作时,此束缚会十分有用;若是不必此束缚,批量操作或许会降低用户设备上正在活跃运转的其他运用的性能。
StorageNotLow 假如设置为 true,那么当用户设备上的存储空间不足时,作业不会运转。

WorkManager 也供给了相似的概念,实际上内部也正是根据 JobScheduler 完结的,但 WorkManager 并非仅仅单纯的署理。

首要,当API版别不足时,WorkManager 兼容性运用 AlarmManagerGcmScheduler 作为补充:

// androidx.work.impl.Schedulers.java
static Scheduler createBestAvailableBackgroundScheduler(
        @NonNull Context context,
        @NonNull WorkManagerImpl workManager) {
    Scheduler scheduler;
    if (Build.VERSION.SDK_INT >= WorkManagerImpl.MIN_JOB_SCHEDULER_API_LEVEL) {
        scheduler = new SystemJobScheduler(context, workManager);
    } else {
        scheduler = tryCreateGcmBasedScheduler(context);
        if (scheduler == null) {
            scheduler = new SystemAlarmScheduler(context);
        }
    }
    return scheduler;
}

其次,读者知道,JobScheduleronStartJob 等回调默认运转在主线程,不能直接进行耗时操作。WorkManagerWorker 内部则进行了线程调度,默认完结在 WorkerThread

// androidx.work.impl.background.systemjob.SystemJobService
public class SystemJobService extends JobService {
    public boolean onStartJob(@NonNull JobParameters params) {
      //...
      mWorkManagerImpl.startWork(...);
    }
}
// androidx.work.impl.WorkManagerImpl
public class WorkManagerImpl extends WorkManager {
  public void startWork(...) {
      mWorkTaskExecutor.executeOnTaskThread(new StartWorkRunnable(...));
  }
}

因为 JobScheduler 是由系统服务中的 JobSchedulerService 完结的,因而其自身的高权限,能够在APP被杀或重启后,依然能够唤起并履行 JobService 及对应的使命。

2.新的保活机制?

Android 官方供给了后台作业的强壮支撑,国内厂商大多数第一时刻却想拿它来做 保活

——举例来说,既然 WorkManager 支撑守时使命,且即便 APP 被杀或许重启都能够确保履行;那么我一个 IM APP,每 10 秒拉取下接口看有没有新消息,趁便发动下 APP 某些页面或许告诉组件,想必也是十分合理的。

社区说|浅谈 WorkManager 的设计与实现:系统概述
实际上,关于 保活 的诉求,WorkManager 做不到,其本质是 JobScheduler 做不到:

首要,随着版别的迭代,Android 系统对后台使命的办理愈发严苛,小于 15 分钟的守时使命已经被强制调整为 15 分钟履行,防止频频的后台守时使命对前台运用的影响,规避了 API 的不合法滥用:

WorkManager : 我把你当兄弟,你竟然想睡我?

其次是笔者的猜测,因为用户安全等相关的考量,国内设备厂商对 JobSchedulerService 等相关都有必定的魔改——比方,当用户手动将 APP 强制封闭,这种操作目的拥有最高优先级,即便是系统服务也不应对其再次发动(厂商白名单在外,如微信和QQ?)。

两点结合,WorkManager 的守时使命受到了严厉的约束,这也意味着相似保活需求其无法满意,其 “不稳定” 性这也是其国内运用较少的主要原因之一。

3.前台服务

终究,咱们评论下 怎么标准地调度系统资源

最经典的场景依然是后台使命的加急,即便有束缚条件,部分后台使命仍需求被 特别加急 ,比方 用户聊地利发送短视频处理付款或订阅流程 等。这些使命对用户很重要,会在后台快速履行,并需求当即开端履行。

系统关于运用的资源分配十分严厉,读者能够经过 这儿 简略了解。

因为使命的履行依赖于系统对资源的分配,因而想要前进履行的优先级,必然需求提高 APP 组件自身的优先级,那么完结计划已经十分显着了:运用 前台服务

当你需求经过调用相似 worker.setExpedited(true) 标记为加急使命时,Worker 内部的详细完结,需开发者额定创立前台告诉,提高优先级的一起,将你后台的使命行为同步给用户。

小结

小结并不是总结,还有更多内容能够扩展,比方:

  • 1、Android 系统的 JobScheduler 使命调度机制的内部原理是什么样的?

  • 2、WorkManager 组件的初始化机制、耐久化机制等,内部的完结细节有哪些需求留意的?

  • 3、WorkManager 中的束缚使命和非束缚使命在履行上有什么不同?

  • 4、WorkManager 还有哪些其它的亮点?

篇幅原因,这些问题笔者将另起一篇进行更深入性的评论,敬请期待。


关于我

Hello,我是 却把清梅嗅 ,假如您觉得文章对您有价值,欢迎 ❤️,也欢迎重视我的 博客 或许 GitHub。

假如您觉得文章还差了那么点东西,也请经过 重视 催促我写出更好的文章——如果哪天我前进了呢?

  • 我的Android学习系统
  • 关于文章纠错
  • 关于常识付费
  • 关于《反思》系列