異步峰髓、非阻塞式 Android 啟動任務調度庫

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

1傻寂、背景

節(jié)前面試的時候被問到 Android 啟動任務依賴怎么做調度。當時隨口給了一個方案携兵,后來想想覺得有意思就自己花了一天的時間寫了一個崎逃。這個庫已經(jīng)開源到 Github 上面:

https://github.com/Shouheng88/AndroidStartup

在寫這個庫之前只是看了下 Jetpack 的 Startup. 畢竟,如果這個庫已經(jīng)非常完善了眉孩,那么我就沒必要自己再搞一個了。截止目前勒葱,在我看來浪汪,這個庫最大的缺點是,這個庫所有的任務都在主線程中觸發(fā)并執(zhí)行凛虽,而我們?yōu)榱藘?yōu)化啟動的性能通常會將任務放到異步線程中執(zhí)行死遭。所以,Jetpack 的庫充其量只能解決你的任務的依賴關系凯旋。

如果要支持異步任務執(zhí)行呀潭,首先要解決的是如何保證任務的先后順序。最初我也想到了使用并發(fā)包里的閉鎖的方案至非,但是這種方案有個問題钠署。即,閉鎖執(zhí)行的時候使用 CAS 以阻塞的方式進行等待荒椭,這樣會白白浪費線程資源谐鼎。如果因此占用了 CPU,將會影響到我們其他線程的執(zhí)行趣惠。所以狸棍,在我的庫中身害,我使用了非阻塞的事件通知機制。這樣在某個任務結束之后會通知所有依賴于它的任務草戈。當一個任務的所有依賴都執(zhí)行完畢塌鸯,再執(zhí)行自己的任務。當然唐片,對于不同參數(shù)的線程池丙猬,異步任務執(zhí)行的表現(xiàn)也是不一樣的,所以牵触,我也提供了方法用來自定義線程池淮悼。

此外,我在開發(fā)這個庫的時候還用到了注解處理器揽思。你可以通過注解聲明自己的任務袜腥,然后編譯期間會自動發(fā)現(xiàn)并拼接你的任務。相對于其他的框架钉汗,又多了一個初始化的選擇羹令。

2、結構

在開發(fā)的時候我將任務調度和啟動工具分成了兩個獨立的模塊损痰,這樣任務調度工具也可以單獨拿出來使用福侈。后來,增加了注解驅動相關的邏輯卢未,就又增加了兩個模塊肪凛。所以,現(xiàn)在各模塊及功能如下:

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() 用來指定執(zhí)行任務的線程
  • dependencies() 用來指定當前任務依賴的任務
  • run() 你的初始化的任務執(zhí)行的方法

其次汉匙,真實的任務分發(fā)的邏輯是通過 Dispatcher 來完成的拱烁。根據(jù)任務之間的依賴關系,我們可以構建成一拓撲結構噩翠。執(zhí)行任務的時候先執(zhí)行的任務就是這個拓撲結構的根結點戏自。放在這里就是 dependencies() 方法為空的結點。所以伤锚,這里浦妄,我們首先要監(jiān)測拓撲結構是否存在環(huán)。然后,只需要找到根結點并從根結點執(zhí)行任務即可剂娄。

3.2 環(huán)檢測

對于環(huán)監(jiān)測蠢涝,如果不考慮空間復雜度,我們可以使用 Set 來發(fā)現(xiàn)循環(huán)依賴:

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 中的環(huán)監(jiān)測的邏輯差不多阅懦。這里用了兩個多余的數(shù)據(jù)結構和二,分別記錄已經(jīng)檢測的和檢測中的任務結點,如果發(fā)現(xiàn)了一個需要檢測的任務正在檢測中耳胎,則說明存在環(huán)惯吕。

3.3 任務啟動

任務啟動之前需要先根據(jù)任務間的依賴關系建立數(shù)據(jù)結構,簡單說就是需要知道當前任務有哪些依賴任務和哪些依賴于它的任務怕午。

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,然后根據(jù)任務的依賴關系找到各任務的父任務郁惜,并調用其 addParent() 方法堡距,這里在 DispatcherJob 中會使用一個 AtomicInteger 進行計數(shù),統(tǒng)計其父任務的數(shù)量兆蕉。然后羽戒,再通過各任務的父任務維護子任務關系。最后虎韵,再根據(jù)任務的依賴找到拓撲的根結點易稠。這樣,我們就可以從根結點開始執(zhí)行整個拓撲結構包蓝。

3.4 任務通知機制

上面我們提到了 DispatcherJob驶社,啟動一個 DispatcherJob 只需要調用它的 execute() 方法,該方法中會根據(jù)線程模型做判斷测萎,從而選擇執(zhí)行的線程執(zhí)行任務:

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)
    }
}

這里任務的執(zhí)行邏輯被包裝到了一個 lambda 方法中亡电。如果是主線程,可以根據(jù)當前線程狀態(tài)執(zhí)行執(zhí)行或者 post 到主線程中執(zhí)行绳泉。如果是異步任務,則將其丟到線程池當中執(zhí)行姆泻。

當一個任務的工作結束之后會獲取所有的子任務進行通知零酪,這里用到了 notifyJobFinished() 方法。這個方法也很簡單拇勃,就是沒當一個任務執(zhí)行完畢四苇,則計數(shù)器減 1,當所有的依賴任務都執(zhí)行完畢的時候方咆,它才開始執(zhí)行自己的任務月腋,以此來通過事件而不是阻塞的方式進行任務調度:

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 的方法獲取所有的任務纫塌。在編譯期間诊县,我們會為這個接口提供實現(xiàn),并會對所有掃描到的任務進行初始化并在該實現(xiàn)中返回措左。

總結

以上就是這個庫的實現(xiàn)原理依痊。

?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市怎披,隨后出現(xiàn)的幾起案子胸嘁,更是在濱河造成了極大的恐慌,老刑警劉巖凉逛,帶你破解...
    沈念sama閱讀 211,042評論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件性宏,死亡現(xiàn)場離奇詭異,居然都是意外死亡状飞,警方通過查閱死者的電腦和手機毫胜,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 89,996評論 2 384
  • 文/潘曉璐 我一進店門书斜,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人酵使,你說我怎么就攤上這事荐吉。” “怎么了凝化?”我有些...
    開封第一講書人閱讀 156,674評論 0 345
  • 文/不壞的土叔 我叫張陵稍坯,是天一觀的道長。 經(jīng)常有香客問我搓劫,道長瞧哟,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,340評論 1 283
  • 正文 為了忘掉前任枪向,我火速辦了婚禮勤揩,結果婚禮上,老公的妹妹穿的比我還像新娘秘蛔。我一直安慰自己陨亡,他們只是感情好,可當我...
    茶點故事閱讀 65,404評論 5 384
  • 文/花漫 我一把揭開白布深员。 她就那樣靜靜地躺著负蠕,像睡著了一般。 火紅的嫁衣襯著肌膚如雪倦畅。 梳的紋絲不亂的頭發(fā)上遮糖,一...
    開封第一講書人閱讀 49,749評論 1 289
  • 那天,我揣著相機與錄音叠赐,去河邊找鬼欲账。 笑死,一個胖子當著我的面吹牛芭概,可吹牛的內容都是我干的赛不。 我是一名探鬼主播,決...
    沈念sama閱讀 38,902評論 3 405
  • 文/蒼蘭香墨 我猛地睜開眼罢洲,長吁一口氣:“原來是場噩夢啊……” “哼踢故!你這毒婦竟也來了?” 一聲冷哼從身側響起惹苗,我...
    開封第一講書人閱讀 37,662評論 0 266
  • 序言:老撾萬榮一對情侶失蹤殿较,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后鸽粉,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體斜脂,經(jīng)...
    沈念sama閱讀 44,110評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡抓艳,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 36,451評論 2 325
  • 正文 我和宋清朗相戀三年触机,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,577評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡儡首,死狀恐怖片任,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情蔬胯,我是刑警寧澤对供,帶...
    沈念sama閱讀 34,258評論 4 328
  • 正文 年R本政府宣布,位于F島的核電站氛濒,受9級特大地震影響产场,放射性物質發(fā)生泄漏。R本人自食惡果不足惜舞竿,卻給世界環(huán)境...
    茶點故事閱讀 39,848評論 3 312
  • 文/蒙蒙 一京景、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧骗奖,春花似錦确徙、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,726評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至仰挣,卻和暖如春伴逸,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背椎木。 一陣腳步聲響...
    開封第一講書人閱讀 31,952評論 1 264
  • 我被黑心中介騙來泰國打工违柏, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人香椎。 一個月前我還...
    沈念sama閱讀 46,271評論 2 360
  • 正文 我出身青樓漱竖,卻偏偏與公主長得像,于是被迫代替她去往敵國和親畜伐。 傳聞我的和親對象是個殘疾皇子馍惹,可洞房花燭夜當晚...
    茶點故事閱讀 43,452評論 2 348

推薦閱讀更多精彩內容