反思 系列博客是一种看似 “内卷” ,但却 效果显著 的学习方式,该系列来源和目录请参阅 这儿 。
窘境
作为一名 Android
开发者,即便你没有用过,也必定对 WorkManager
耳熟能详。
自2018年发布以来,作为 Google
官方推出的架构组件,它未像 LiveData
、ViewModel
相同广泛运用。究其原因,一起来看 官方 开端对 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
也并未独自造轮子,而是借用了 Java
的 ArrayDeque
类。这是一个十分经典的完结类,在诸多大名鼎鼎的东西库中都有它的身影(比方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
)等几种:
请留意,上文中的日志上报咱们界说成了 一次性作业,即只履行一次,履行结束即永久结束。
实际上,一次性作业 并不能涵盖所有的事务场景,举例来说,作为一个视频类的 APP
,咱们需 守时 对用户的播映进展进行一次记载或上报,确保用户即便杀掉APP
,下次仍在终究记载的播映方位继续播映。
这儿咱们引进了 守时使命 的概念,它只要一个终止状况 CANCELLED
。这是因为守时作业永远不会结束。每次运转后,不管成果怎么,系统都会从头对其进行调度。
终究,咱们将后台使命的履行笼统为一个接口,开发者完结 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
的线程池。但随着事务杂乱度的提高,线程池或许会一起履行多个使命,这意味着部分晚入列、或优先级低的使命,会经常性等候前面的使命履行结束。
严厉意义上讲,此时,即时使命都转化为了非即时使命,再进一步笼统,所有即时使命都对错即时使命。
万物皆可异步,是异步编程的一个经典概念,该思想在
Handler
、RxJava
或协程
中都有表现。
即时使命被延时履行是合理的吗?关于后台使命而言,是十分合理的,假如开发者有清晰的诉求, 必须 且 当即 履行某段事务逻辑,那么就不应该用 后台使命库,而是直接在内存中调用这块代码。
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
的规划中除了声明惯例的 Insert
、Query
、Update
等,并没有 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
兼容性运用 AlarmManager
或 GcmScheduler
作为补充:
// 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;
}
其次,读者知道,JobScheduler
的 onStartJob
等回调默认运转在主线程,不能直接进行耗时操作。WorkManager
的 Worker
内部则进行了线程调度,默认完结在 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
做不到,其本质是 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学习系统
- 关于文章纠错
- 关于常识付费
- 关于《反思》系列