异步、非阻塞式 Android 启动任务调度库

ilse-orsel-EJ6AruM0jMo-unsplash (1).jpg

1、背景

节前面试的时候被问到 Android 启动任务依赖怎么做调度。当时随口给了一个方案,后来想想觉得有意思就自己花了一天的时间写了一个。这个库已经开源到 Github 上面:

https://github.com/Shouheng88/AndroidStartup

在写这个库之前只是看了下 Jetpack 的 Startup. 毕竟,如果这个库已经非常完善了,那么我就没必要自己再搞一个了。截止目前,在我看来,这个库最大的缺点是,这个库所有的任务都在主线程中触发并执行,而我们为了优化启动的性能通常会将任务放到异步线程中执行。所以,Jetpack 的库充其量只能解决你的任务的依赖关系。

如果要支持异步任务执行,首先要解决的是如何保证任务的先后顺序。最初我也想到了使用并发包里的闭锁的方案,但是这种方案有个问题。即,闭锁执行的时候使用 CAS 以阻塞的方式进行等待,这样会白白浪费线程资源。如果因此占用了 CPU,将会影响到我们其他线程的执行。所以,在我的库中,我使用了非阻塞的事件通知机制。这样在某个任务结束之后会通知所有依赖于它的任务。当一个任务的所有依赖都执行完毕,再执行自己的任务。当然,对于不同参数的线程池,异步任务执行的表现也是不一样的,所以,我也提供了方法用来自定义线程池。

此外,我在开发这个库的时候还用到了注解处理器。你可以通过注解声明自己的任务,然后编译期间会自动发现并拼接你的任务。相对于其他的框架,又多了一个初始化的选择。

2、结构

在开发的时候我将任务调度和启动工具分成了两个独立的模块,这样任务调度工具也可以单独拿出来使用。后来,增加了注解驱动相关的逻辑,就又增加了两个模块。所以,现在各模块及功能如下:

scheduler:           任务调度工具
startup:             启动工具,任务调度工具包装
startup-annotation:  注解定义
startup-compiler:    注解编译器

3、调度器

先来看任务调度器的工作原理。

3.1 任务封装

首先是任务的定义。在我的项目中我使用 ISchedulerJob 定义任务。

interface ISchedulerJob {

    fun threadMode(): ThreadMode

    fun dependencies(): List<Class<out ISchedulerJob>>

    fun run(context: Context)
}

它定义了三个方法,分别是:

  • threadMode() 用来指定执行任务的线程
  • dependencies() 用来指定当前任务依赖的任务
  • run() 你的初始化的任务执行的方法

其次,真实的任务分发的逻辑是通过 Dispatcher 来完成的。根据任务之间的依赖关系,我们可以构建成一拓扑结构。执行任务的时候先执行的任务就是这个拓扑结构的根结点。放在这里就是 dependencies() 方法为空的结点。所以,这里,我们首先要监测拓扑结构是否存在环。然后,只需要找到根结点并从根结点执行任务即可。

3.2 环检测

对于环监测,如果不考虑空间复杂度,我们可以使用 Set 来发现循环依赖:

private fun checkDependencies() {
    val checking = mutableSetOf<Class<out ISchedulerJob>>()
    val checked = mutableSetOf<Class<out ISchedulerJob>>()
    val schedulerMap = mutableMapOf<Class<ISchedulerJob>, ISchedulerJob>()
    schedulerJobs.forEach { schedulerMap[it.javaClass] = it }
    schedulerJobs.forEach { schedulerJob ->
        checkDependenciesReal(schedulerJob, schedulerMap, checking, checked)
    }
}

private fun checkDependenciesReal(
    schedulerJob: ISchedulerJob,
    map: Map<Class<ISchedulerJob>, ISchedulerJob>,
    checking: MutableSet<Class<out ISchedulerJob>>,
    checked: MutableSet<Class<out ISchedulerJob>>
) {
    if (checking.contains(schedulerJob.javaClass)) {
        // Cycle detected.
        throw SchedulerException("Cycle detected for ${schedulerJob.javaClass.name}.")
    }
    if (!checked.contains(schedulerJob.javaClass)) {
        checking.add(schedulerJob.javaClass)
        if (schedulerJob.dependencies().isNotEmpty()) {
            schedulerJob.dependencies().forEach {
                if (!checked.contains(it)) {
                    val job = map[it]
                        ?: throw SchedulerException(String.format("dependency [%s] not found", it.name))
                    checkDependenciesReal(job, map, checking, checked)
                }
            }
        }
        checking.remove(schedulerJob.javaClass)
        checked.add(schedulerJob.javaClass)
    }
}

这里的逻辑和 Jetpack 中的环监测的逻辑差不多。这里用了两个多余的数据结构,分别记录已经检测的和检测中的任务结点,如果发现了一个需要检测的任务正在检测中,则说明存在环。

3.3 任务启动

任务启动之前需要先根据任务间的依赖关系建立数据结构,简单说就是需要知道当前任务有哪些依赖任务和哪些依赖于它的任务。

private fun buildDispatcherJobs() {
    roots.clear()

    // Build the map from scheduler class type to dispatcher job.
    val map =  mutableMapOf<Class<ISchedulerJob>, DispatcherJob>()
    schedulerJobs.forEach {
        val dispatcherJob = DispatcherJob(this.globalContext, executor, it)
        map[it.javaClass] = dispatcherJob
    }

    // Fill the parent field for dispatcher job.
    schedulerJobs.forEach { schedulerJob ->
        val dispatcherJob = map[schedulerJob.javaClass]!!
        schedulerJob.dependencies().forEach {
            dispatcherJob.addParent(map[it]!!)
        }
    }

    // Fill the children field for dispatcher job.
    schedulerJobs.forEach { schedulerJob ->
        val dispatcherJob = map[schedulerJob.javaClass]!!
        dispatcherJob.parents().forEach {
            it.addChild(dispatcherJob)
        }
    }

    // Find roots.
    schedulerJobs.filter {
        it.dependencies().isEmpty()
    }.forEach {
        val dispatcherJob = map[it.javaClass]!!
        roots.add(dispatcherJob)
    }
}

这里先对任务做一个封装,将所有的任务包装成 DispatcherJob,然后根据任务的依赖关系找到各任务的父任务,并调用其 addParent() 方法,这里在 DispatcherJob 中会使用一个 AtomicInteger 进行计数,统计其父任务的数量。然后,再通过各任务的父任务维护子任务关系。最后,再根据任务的依赖找到拓扑的根结点。这样,我们就可以从根结点开始执行整个拓扑结构。

3.4 任务通知机制

上面我们提到了 DispatcherJob,启动一个 DispatcherJob 只需要调用它的 execute() 方法,该方法中会根据线程模型做判断,从而选择执行的线程执行任务:

override fun execute() {
    val realJob = {
        // Run the task.
        job.run(context)
        // Handle for children.
        children.forEach { it.notifyJobFinished(this) }
    }

    try {
        if (job.threadMode() == ThreadMode.MAIN) {
            // Cases for main thread.
            if (Thread.currentThread() == Looper.getMainLooper().thread) {
                realJob()
            } else {
                Handler(Looper.getMainLooper()).post { realJob() }
            }
        } else {
            // Cases for background thread.
            executor.execute { realJob() }
        }
    } catch (e: Throwable) {
        throw SchedulerException(e)
    }
}

这里任务的执行逻辑被包装到了一个 lambda 方法中。如果是主线程,可以根据当前线程状态执行执行或者 post 到主线程中执行。如果是异步任务,则将其丢到线程池当中执行。

当一个任务的工作结束之后会获取所有的子任务进行通知,这里用到了 notifyJobFinished() 方法。这个方法也很简单,就是没当一个任务执行完毕,则计数器减 1,当所有的依赖任务都执行完毕的时候,它才开始执行自己的任务,以此来通过事件而不是阻塞的方式进行任务调度:

override fun notifyJobFinished(job: IDispatcherJob) {
    if (waiting.decrementAndGet() == 0) {
        // All dependencies finished, commit the job.
        execute()
    }
}

4、启动器

对于启动器,你有三种选择。使用类似于 Jetpack 的 ContentProvider、自己声明任务或者使用注解 @StartupJob 进行任务声明。

对于内容提供器,原理比较简单,就是再自定义 ContentProvider 的 onCreate() 方法中扫描自定义的 meta-data. ContentProvider 的声明方式有几个问题,第一,ContentProvider 作为四大组件之一,建立过程需要消耗一定性能。此外,默认 ContentProvider 运行在主进程当中,所以,如果你的应用中如果用到了多进程,那么默认的 ContentProvider 不会为你的子进程做初始化,除非你明确指定它的进程。

所以,除了 ContentProvider,你还可以使用手动声明的方式,

AndroidStartup.newInstance(this).jobs(
   CrashHelperInitializeJob(),
   ThirdPartLibrariesInitializeJob(),
   DependentBlockingBackgroundJob(),
   BlockingBackgroundJob()
).launch()

此外,我还特意增加了注解的方式声明任务。使用起来很简单,你只需要在自己的任务上面使用注解声明即可,如:

@StartupJob
class BlockingBackgroundJob : ISchedulerJob {

    override fun threadMode(): ThreadMode = ThreadMode.BACKGROUND

    override fun dependencies(): List<Class<out ISchedulerJob>> = emptyList()

    override fun run(context: Context) {
        Thread.sleep(5_000L) // 5 seconds
        L.d("BlockingBackgroundJob done! ${Thread.currentThread()}")
    }
}

它的工作原理也比较简单,就是当你调用 AndroidStartup 的 scanAnnotations() 方法的时候,它会通过反射调用 JobHunter 的方法获取所有的任务。在编译期间,我们会为这个接口提供实现,并会对所有扫描到的任务进行初始化并在该实现中返回。

总结

以上就是这个库的实现原理。

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

推荐阅读更多精彩内容