應(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ù)初始化框架扶歪,一般分幾個部分。
- 任務(wù)定義
- 任務(wù)依賴管理
- 任務(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)鍵部分近顷,要解決三個問題。
- 在Application的onCreate返回前把所有的同步和異步任務(wù)都執(zhí)行完成宁否,并且支持可以啟動協(xié)程窒升。
runBlocking()
這個函數(shù)是絕佳選擇。 - 啟動任務(wù)并行執(zhí)行慕匠,支持在主線程和后臺線程進(jìn)行執(zhí)行饱须。這部分的解決方案是
CoroutineScope.launch()
- 任務(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還是很好用的蜂挪。