應(yīng)用程序啟動優(yōu)化新思路 - Kotlin協(xié)程

應(yīng)用程序的啟動速度的重要性不言而喻,各種方案層出不窮端三,為了優(yōu)化十幾毫秒的時間,工程師也是不遺余力。各種框架也是應(yīng)運而生耸弄,Google的Jetpack也包括Startup的項目,對Android應(yīng)用啟動進(jìn)行優(yōu)化卓缰,一些公司也內(nèi)部開發(fā)一些框架计呈,支持任務(wù)初始化的并行執(zhí)行,來提升應(yīng)用啟動的速度征唬。

啟動優(yōu)化涉及到應(yīng)用的許多方面捌显,本文探討的是其中的一個方面,如何簡化任務(wù)初始化的并行執(zhí)行邏輯总寒。

寫在前面

任務(wù)初始化框架扶歪,一般分幾個部分。

  1. 任務(wù)定義
  2. 任務(wù)依賴管理
  3. 任務(wù)并行化執(zhí)行

Kotlin的協(xié)程方案

Kotlin的協(xié)程在管理任務(wù)依賴和并行化執(zhí)行方面非常簡單高效偿乖,在使用這個方案的時候击罪,基本上半天時間哲嘲,就可以把這個方案在項目中落地。而且只用Kotlin的協(xié)程就可以媳禁,不用額外引入框架眠副,使用原生的協(xié)程方案代碼量更小,且使用者完全可控竣稽,這個很關(guān)鍵囱怕。

下面把這個方案說明一下:

Task的定義

首先我們先簡單定義一下Task,作為所有初始化任務(wù)的基類毫别。

abstract class Task(val name: String, val mainThread: Boolean) {
    // deps表示依賴于的task的列表
    val deps: List<String> = mutableListOf()
    
    // depsOn被依賴的task列表娃弓,拓?fù)涞呐判虻臅r候需要用到
    val depsOn: List<String> = mutableListOf()

    // 依賴的任務(wù)為完成的數(shù)量,拓?fù)涞呐判虻臅r候需要用到
    open var dependTaskCount = 0

    // 具體初始化的函數(shù)岛宦,子類需要重寫
    abstract fun startUp(context: Context)
}

初始化Task

這部分負(fù)責(zé)初始化Task台丛,返回Task的列表。

fun collectTasks(): List<Task> = { 
    // collect tasks
}

這部分有兩種實現(xiàn)方案砾肺。

  • 通過注解的方式來實現(xiàn)Task的定義和收集挽霉。
  • 手動進(jìn)行Task的創(chuàng)建,并建立各個任務(wù)的依賴關(guān)系变汪。

這兩種方式可以按需選擇侠坎,我們項目中選擇了使用第二種方式,原因是我們的任務(wù)關(guān)系相對簡單裙盾,并可以在這里統(tǒng)一的地方查看任務(wù)依賴關(guān)系实胸。但是缺點是必須存在工程依賴。

第一種方式是設(shè)計框架進(jìn)行復(fù)用的不二選擇番官,但是對于這種方案庐完,我也建議可以有統(tǒng)一的地方來配置任務(wù)之間的依賴關(guān)系的配置。第二種方式特別適合最開始的時候徘熔,把任務(wù)初始化串行執(zhí)行改為并行假褪,代碼稍加改造即可實現(xiàn)。

任務(wù)調(diào)度

這部分是關(guān)鍵部分近顷,要解決三個問題。

  1. 在Application的onCreate返回前把所有的同步和異步任務(wù)都執(zhí)行完成宁否,并且支持可以啟動協(xié)程窒升。runBlocking()這個函數(shù)是絕佳選擇。
  2. 啟動任務(wù)并行執(zhí)行慕匠,支持在主線程和后臺線程進(jìn)行執(zhí)行饱须。這部分的解決方案是CoroutineScope.launch()
  3. 任務(wù)依賴,假設(shè)任務(wù)A依賴任務(wù)B台谊,啟動A的時候蓉媳,必須保證B執(zhí)行完成譬挚。需要執(zhí)行Job B的join
val jobA = launch { 
    jobB.join() // 執(zhí)行taskA之前酪呻,先執(zhí)行jobB的join减宣,保證任務(wù)的依賴關(guān)系。
    
    // 具體執(zhí)行TaskA的任務(wù)玩荠。
    taskA().startUp()
}

是不是比線程方案要簡單多了漆腌。

接下來使用偽代碼把整體的邏輯說明一下,緊要的地方有注釋阶冈。這個函數(shù)調(diào)用在Application的onCreate()里面調(diào)用即可闷尿。

fun startUp(context: Context) = runBlocking {
    val taskList: List<Task> = collectTasks()

    // 建立一個map, 通過name可以獲取task
    val taskMap = mutableMapOf<String, Task>().apply {
        taskList.forEach {
            put(it.name, it)
        }
    }
    
    // 初始化拓?fù)渑判虻牡谝慌鷽]有被依賴的task女坑,可以直接執(zhí)行
    val queue: Queue<Task> = LinkedList()
    taskList.filter { it.dependTaskCount == 0 }.forEach(queue::add)

    //建立一個map, 通過name可以獲取協(xié)程的Job
    val jobMap = mutableMapOf<String, Job>()

    while (queue.isNotEmpty()) {
        val curTask = queue.poll()!!
        // 考慮一下填具,為什么如果task需要在main thread之中運行的話,dispatcher要設(shè)置為EmptyCoroutineContext 匆骗?
        val dispatcher = if (curTask.isMainThreadTask) EmptyCoroutineContext else Dispatchers.Default
        jobMap[curTask.name] = launch(dispatcher) {
            for(dep in curTask.deps) {
                withContext(context) { // 這句代碼很重要劳景,不然會有死鎖,想一想為什么绰筛?
                    jobMap[dep]!!.join() // 依賴的任務(wù)必須先執(zhí)行完枢泰,因為這個是拓?fù)渑判驁?zhí)行的,所以jobMap[dep]不可能為空
                }
            }
            //依賴已經(jīng)執(zhí)行完成铝噩,執(zhí)行自身的任務(wù)
            curTask.startUp(context)
        }
        for (taskName in curTask.depsOn) {
            //這是一個依賴于當(dāng)前任務(wù)的后續(xù)任務(wù)
            val followTask = taskMap[taskName]!!

            //如果這個后續(xù)任務(wù)所依賴的未開始任務(wù)數(shù)量為0衡蚂,則安排這個任務(wù)進(jìn)入隊列
            followTask.dependTaskCount--
            if (followTask.dependTaskCount == 0) {
                queue.offer(followTask)
            }
        }
    }
    // 這個地方需要判斷一下,是否所有的任務(wù)都已經(jīng)被安排執(zhí)行了骏庸,如果還有任務(wù)沒有被安排毛甲,說明任務(wù)存在循環(huán)依賴,拋出異常具被。
}

以上代碼是為這篇文章現(xiàn)準(zhǔn)備的玻募,雖是偽代碼,是可以編譯通過的一姿,但是沒有調(diào)試過七咧,可能存在一些小問題,大的思路上沒有問題叮叹。

其實也有些時候艾栋,代碼還可以更簡單。假設(shè)現(xiàn)在有4個任務(wù), A B C D, 其中B依賴于A蛉顽,C依賴于A蝗砾,D依賴于B和C,初始化代碼可以簡單這樣寫。

fun startUp(context: Context) = runBlocking {
    val jobA = launch(taskA.dispather) {
        TaskA().startUp()   
    }
    
    val jobB = launch(taskB.dispather) {
        jobA.join()
        TaskB().startUp()
    }
    
    val jobC = launch(taskC.dispather) {
        jobA.join()
        TaskC().start()
    }
    
    val jobD = launch(taskD.dispather) {
        jobB.join()
        jobC.join()
        TaskD().start()
    }
}

不過不要被我?guī)牡苛福@樣寫法只在非常特定的場景下闲勺,當(dāng)然這樣寫的執(zhí)行效率高,但是如果任務(wù)很多很復(fù)雜的話扣猫,不建議這樣寫菜循。維護(hù)成本略高,除非你覺得你完全可以Hold住苞笨。

這個寫法特別合適從最初的同步任務(wù)初始化改成異步的寫法的最初嘗試债朵,再逐步重構(gòu),最終可以進(jìn)化成協(xié)程版本的異步任務(wù)初始化框架瀑凝。

另外一點需要注意序芦,盡量讓長時間的任務(wù)盡早安排執(zhí)行,這樣可以最大程度的減少事件的最長路徑粤咪,因為這個最長路徑?jīng)Q定總的執(zhí)行時間的長短谚中。

額外的好處

這個設(shè)計帶來的一個額外的好處是,可以在任務(wù)的初始化代碼里面使用suspend函數(shù)寥枝。

One More Thing

一個小Tip宪塔,作為文章的結(jié)尾吧。

如果有一些初始化任務(wù)囊拜,可以在Application的onCreate函數(shù)之后執(zhí)行某筐,但是可能入口比較多,還要防止重復(fù)初始化冠跷,管理起來會比較麻煩南誊。但是這些初始化越早越好,在這種情況下蜜托,可以在Application的onCreate的最后抄囚,啟動一個協(xié)程(MainThread)來進(jìn)行。這樣不影響主界面的啟動時間橄务,任務(wù)會在主界面啟動之后的消息隊列里面立即執(zhí)行幔托。

override fun onCreate() {
    super.onCreate()
    
    startUp(this)

    // other code ......
    
    coroutineScope.launch(Dispatchers.Main) {
        DelayTask("name", true).startUp(context)
    }
}

在一些特定的場景下,這個小Tip還是很好用的蜂挪。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末重挑,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子棠涮,更是在濱河造成了極大的恐慌攒驰,老刑警劉巖,帶你破解...
    沈念sama閱讀 217,826評論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件故爵,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機诬垂,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,968評論 3 395
  • 文/潘曉璐 我一進(jìn)店門劲室,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人结窘,你說我怎么就攤上這事很洋。” “怎么了隧枫?”我有些...
    開封第一講書人閱讀 164,234評論 0 354
  • 文/不壞的土叔 我叫張陵喉磁,是天一觀的道長。 經(jīng)常有香客問我官脓,道長协怒,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,562評論 1 293
  • 正文 為了忘掉前任卑笨,我火速辦了婚禮孕暇,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘赤兴。我一直安慰自己妖滔,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 67,611評論 6 392
  • 文/花漫 我一把揭開白布桶良。 她就那樣靜靜地躺著座舍,像睡著了一般。 火紅的嫁衣襯著肌膚如雪陨帆。 梳的紋絲不亂的頭發(fā)上曲秉,一...
    開封第一講書人閱讀 51,482評論 1 302
  • 那天,我揣著相機與錄音歧譬,去河邊找鬼岸浑。 笑死,一個胖子當(dāng)著我的面吹牛瑰步,可吹牛的內(nèi)容都是我干的矢洲。 我是一名探鬼主播,決...
    沈念sama閱讀 40,271評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼缩焦,長吁一口氣:“原來是場噩夢啊……” “哼读虏!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起袁滥,我...
    開封第一講書人閱讀 39,166評論 0 276
  • 序言:老撾萬榮一對情侶失蹤盖桥,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后题翻,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體揩徊,經(jīng)...
    沈念sama閱讀 45,608評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,814評論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了塑荒。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片熄赡。...
    茶點故事閱讀 39,926評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖齿税,靈堂內(nèi)的尸體忽然破棺而出彼硫,到底是詐尸還是另有隱情,我是刑警寧澤凌箕,帶...
    沈念sama閱讀 35,644評論 5 346
  • 正文 年R本政府宣布拧篮,位于F島的核電站,受9級特大地震影響牵舱,放射性物質(zhì)發(fā)生泄漏串绩。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,249評論 3 329
  • 文/蒙蒙 一仆葡、第九天 我趴在偏房一處隱蔽的房頂上張望赏参。 院中可真熱鬧,春花似錦沿盅、人聲如沸把篓。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,866評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽韧掩。三九已至,卻和暖如春窖铡,著一層夾襖步出監(jiān)牢的瞬間疗锐,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,991評論 1 269
  • 我被黑心中介騙來泰國打工费彼, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留滑臊,地道東北人。 一個月前我還...
    沈念sama閱讀 48,063評論 3 370
  • 正文 我出身青樓箍铲,卻偏偏與公主長得像雇卷,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子颠猴,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,871評論 2 354

推薦閱讀更多精彩內(nèi)容