自己寫個(gè)App 啟動(dòng)任務(wù)框架也不過如此

作者:王晨彥

一、前言

我們在開發(fā)應(yīng)用的時(shí)候立润,一般都會(huì)引入 SDK确镊,而大部分 SDK 都要求我們在 Application 中初始化,當(dāng)我們引入的 SDK 越來越多范删,就會(huì)出現(xiàn) Application 越來越長蕾域,如果 SDK 的初始化任務(wù)相互依賴,還要處理很多條件判斷到旦,這時(shí)旨巷,如果再來個(gè)異步初始化,相信大家都會(huì)崩潰添忘。

有人可能會(huì)說采呐,我都在主線程按順序初始化不就行了,當(dāng)然行搁骑,只要老板不來找你麻煩斧吐。

「小王啊,咱們的 APP 啟動(dòng)時(shí)間怎么這么久仲器?」

開個(gè)玩笑煤率,可見,一個(gè)優(yōu)秀的啟動(dòng)框架對(duì)于 APP 啟動(dòng)性能而言乏冀,是多么的重要蝶糯!

二、為什么不用 Google 的 StartUp?

說到啟動(dòng)框架辆沦,就不得不提 StartUp昼捍,畢竟是 Google 官方出品识虚,現(xiàn)有的啟動(dòng)框架,或多或少都有參考 StartUp妒茬,這里不再詳細(xì)介紹担锤,如果對(duì) StartUp 還不了解,可以參考這篇文章 Jetpack系列之App Startup從入門到出家乍钻。

https://juejin.cn/post/7023643365048582174

StartUp 提供了簡便的依賴任務(wù)初始化功能肛循,但是對(duì)于一個(gè)復(fù)雜項(xiàng)目來說,StartUp 有以下不足:

1. 不支持異步任務(wù)

如果通過 ContentProvider 啟動(dòng)团赁,所有任務(wù)都在主線程執(zhí)行,如果通過接口啟動(dòng)谨履,所有任務(wù)都在同一個(gè)線程執(zhí)行欢摄。

2. 不支持組件化

通過 Class 指定依賴任務(wù),需要引用依賴的模塊笋粟。

3. 不支持多進(jìn)程

無法單獨(dú)配置任務(wù)需要執(zhí)行的進(jìn)程怀挠。

4. 不支持啟動(dòng)優(yōu)先級(jí)

雖然可以通過指定依賴來設(shè)置優(yōu)先級(jí),但是過于復(fù)雜害捕。

三绿淋、一個(gè)合格的啟動(dòng)框架是怎么樣的?

1. 支持異步任務(wù)

減少啟動(dòng)時(shí)間的有效手段尝盼。

2. 支持組件化

其實(shí)就是解耦吞滞,一方面是解耦任務(wù)依賴,另一方面是解耦 app 和 module 的依賴盾沫。

3. 支持任務(wù)依賴

可以簡化我們的任務(wù)調(diào)度裁赠。

4. 支持優(yōu)先級(jí)

在沒有依賴的情況下,允許任務(wù)優(yōu)先執(zhí)行赴精。

5. 支持多進(jìn)程

只在需要的進(jìn)程中執(zhí)行初始化任務(wù)佩捞,可以減輕系統(tǒng)負(fù)載,側(cè)面提升 APP 啟動(dòng)速度蕾哟。

四一忱、收集任務(wù)

如果要做到完全解耦,我們可以使用 APT 收集任務(wù)谭确。

首先定義注解帘营,即任務(wù)的一些屬性。

@Target(AnnotationTarget.CLASS)
@Retention(AnnotationRetention.RUNTIME)
annotation class InitTask(
    /**
     * 任務(wù)名稱逐哈,需唯一
     */
    val name: String,
    /**
     * 是否在后臺(tái)線程執(zhí)行
     */
    val background: Boolean = false,
    /**
     * 優(yōu)先級(jí)仪吧,越小優(yōu)先級(jí)越高
     */
    val priority: Int = PRIORITY_NORM,
    /**
     * 任務(wù)執(zhí)行進(jìn)程,支持主進(jìn)程鞠眉、非主進(jìn)程薯鼠、所有進(jìn)程择诈、:xxx、特定進(jìn)程名
     */
    val process: Array<String> = [PROCESS_ALL],
    /**
     * 依賴的任務(wù)
     */
    val depends: Array<String> = []
)

name 作為任務(wù)唯一標(biāo)識(shí)出皇,類型為 String 主要是解耦任務(wù)依賴羞芍。

background 即是否后臺(tái)執(zhí)行。

priority 是在主線程郊艘、無依賴場景下的執(zhí)行順序荷科。

process 指定了任務(wù)執(zhí)行的進(jìn)程,支持主進(jìn)程纱注、非主進(jìn)程畏浆、所有進(jìn)程、:xxx狞贱、特定進(jìn)程名刻获。

depends 指定依賴的任務(wù)。

任務(wù)的屬性定義好瞎嬉,還需要一個(gè)執(zhí)行任務(wù)的接口:

interface IInitTask {
    fun execute(application: Application)
}

任務(wù)需要收集的信息已經(jīng)定義好了蝎毡,那么看一下一個(gè)真正的任務(wù)長什么樣。

@InitTask(
    name = "main",
    process = [InitTask.PROCESS_MAIN],
    depends = ["lib"]
)
class MainTask : IInitTask {
    override fun execute(application: Application) {
        SystemClock.sleep(1000)
        Log.e("WCY", "main1 execute")
    }
}

還是比較簡潔清晰的氧枣。

接下來需要通過 Annotation Processor 收集任務(wù)沐兵,然后通過 kotlin poet 寫入文件。

class TaskProcessor : AbstractProcessor() {

    override fun process(annotations: MutableSet<out TypeElement>?, roundEnv: RoundEnvironment): Boolean {
        val taskElements = roundEnv.getElementsAnnotatedWith(InitTask::class.java)
        val taskType = elementUtil.getTypeElement("me.wcy.init.api.IInitTask")

        /**
         * Param type: MutableList<TaskInfo>
         *
         * There's no such type as MutableList at runtime so the library only sees the runtime type.
         * If you need MutableList then you'll need to use a ClassName to create it.
         * [https://github.com/square/kotlinpoet/issues/482]
         */
        val inputMapTypeName =
            ClassName("kotlin.collections", "MutableList").parameterizedBy(TaskInfo::class.asTypeName())

        /**
         * Param name: taskList: MutableList<TaskInfo>
         */
        val groupParamSpec = ParameterSpec.builder(ProcessorUtils.PARAM_NAME, inputMapTypeName).build()

        /**
         * Method: override fun register(taskList: MutableList<TaskInfo>)
         */
        val loadTaskMethodBuilder = FunSpec.builder(ProcessorUtils.METHOD_NAME)
            .addModifiers(KModifier.OVERRIDE)
            .addParameter(groupParamSpec)

        for (element in taskElements) {
            val typeMirror = element.asType()
            val task = element.getAnnotation(InitTask::class.java)
            if (typeUtil.isSubtype(typeMirror, taskType.asType())) {
                val taskCn = (element as TypeElement).asClassName()

                /**
                 * Statement: taskList.add(TaskInfo(name, background, priority, process, depends, task));
                 */
                loadTaskMethodBuilder.addStatement(
                    "%N.add(%T(%S, %L, %L, %L, %L, %T()))",
                    ProcessorUtils.PARAM_NAME,
                    TaskInfo::class.java,
                    task.name,
                    task.background,
                    task.priority,
                    ProcessorUtils.formatArray(task.process),
                    ProcessorUtils.formatArray(task.depends),
                    taskCn
                )
            }
        }

        /**
         * Write to file
         */
        FileSpec.builder(ProcessorUtils.PACKAGE_NAME, "TaskRegister\$$moduleName")
            .addType(
                TypeSpec.classBuilder("TaskRegister\$$moduleName")
                    .addKdoc(ProcessorUtils.JAVADOC)
                    .addSuperinterface(ModuleTaskRegister::class.java)
                    .addFunction(loadTaskMethodBuilder.build())
                    .build()
            )
            .build()
            .writeTo(filer)

        return true
    }
}

看一下生成的文件長什么樣便监。

public class TaskRegister$sample : ModuleTaskRegister {
  public override fun register(taskList: MutableList<TaskInfo>): Unit {
    taskList.add(TaskInfo("main2", true, 0, arrayOf("PROCESS_ALL"), arrayOf("main1","lib1"),MainTask2()))
    taskList.add(TaskInfo("main3", false, -1000, arrayOf("PROCESS_ALL"), arrayOf(), MainTask3()))
    taskList.add(TaskInfo("main1", false, 0, arrayOf("PROCESS_MAIN"), arrayOf("lib1"), MainTask()))
  }
} 

sample 模塊收集到了3個(gè)任務(wù)扎谎,TaskInfo 對(duì)任務(wù)信息做了聚合。

我們知道 APT 可以生成代碼烧董,但是無法修改字節(jié)碼簿透,也就是說我們在運(yùn)行時(shí)想到拿到注入的任務(wù),還需要將收集的任務(wù)注入到源碼中解藻。

這里可以借助 AutoRegister 幫我們完成注入老充。

https://github.com/luckybilly/AutoRegister

注入前:

internal class FinalTaskRegister {
    val taskList: MutableList<TaskInfo> = mutableListOf()

    init {
        init()
    }

    private fun init() {}

    fun register(register: ModuleTaskRegister) {
        register.register(taskList)
    }
}

將收集到的任務(wù)注入到 init 方法中,注入后的字節(jié)碼:

/* compiled from: FinalTaskRegister.kt */
public final class FinalTaskRegister {
    private final List<TaskInfo> taskList = new ArrayList();

    public FinalTaskRegister() {
        init();
    }

    public final List<TaskInfo> getTaskList() {
        return this.taskList;
    }

    private final void init() {
        register(new TaskRegister$sample_lib());
        register(new TaskRegister$sample());
    }

    public final void register(ModuleTaskRegister register) {
        Intrinsics.checkNotNullParameter(register, "register");
        register.register(this.taskList);
    }
}

我們通過 APT 生成的類已經(jīng)成功的注入到代碼中螟左。

小結(jié)

至此啡浊,我們已經(jīng)完成了任務(wù)的收集,通過 APT 和字節(jié)碼修改是常見的類收集方案胶背,相比反射巷嚣,字節(jié)碼修改沒有任何性能的損失。

后來發(fā)現(xiàn) Google 已經(jīng)推出了新的注解處理框架 ksp钳吟,處理速度更快廷粒,于是果斷嘗試了一把,所以有兩種注解處理可以選擇,GitHub 上有詳細(xì)介紹坝茎。

五涤姊、任務(wù)調(diào)度

任務(wù)調(diào)度是啟動(dòng)框架的核心,大家可能聽到過嗤放。

處理依賴任務(wù)首先要構(gòu)建一個(gè)「有向無環(huán)圖」思喊。

什么是有向無環(huán)圖,看下維基百科的介紹:

在圖論中次酌,如果一個(gè)有向圖從任意頂點(diǎn)出發(fā)無法經(jīng)過若干條邊回到該點(diǎn)恨课,則這個(gè)圖是一個(gè)有向無環(huán)圖(DAG, Directed Acyclic Graph)。

聽起來好像很簡單岳服,那么具體怎么實(shí)現(xiàn)呢剂公,今天我們拋開高級(jí)概念不談,用代碼帶大家實(shí)現(xiàn)任務(wù)的調(diào)度吊宋。

首先纲辽,需要把任務(wù)分為兩類,有依賴的任務(wù)和無依賴的任務(wù)贫母。

有依賴的首先檢查是否有環(huán)文兑,如果有循環(huán)依賴盒刚,直接 throw腺劣,這個(gè)可以套用公式 —— 如何判斷鏈表是否有環(huán)。

如果沒有循環(huán)依賴因块,則收集每個(gè)任務(wù)的被依賴任務(wù)橘原,我們稱之為子任務(wù),用于當(dāng)前任務(wù)執(zhí)行完成后涡上,繼續(xù)執(zhí)行子任務(wù)趾断。

無依賴的最簡單吩愧,直接按照優(yōu)先級(jí)執(zhí)行即可芋酌。

不知道大家是否有疑問:有依賴的任務(wù)什么時(shí)候啟動(dòng)雁佳?

有依賴的任務(wù),依賴鏈的葉子端點(diǎn)一定是一個(gè)無依賴的任務(wù)糖权,因此無依賴的任務(wù)執(zhí)行完成后堵腹,就可以開始執(zhí)行有依賴的任務(wù)星澳。

下面用一個(gè)小例子來介紹:

? A 依賴 B、C

? B 依賴 C

? C 無依賴

樹形結(jié)構(gòu):

1. 分組并梳理子任務(wù)。

  • 有依賴:

A: 無子任務(wù)

B: 子任務(wù): [A]

  • 無依賴:

C: 子任務(wù): [A, B]

2. 執(zhí)行無依賴的任務(wù)C腿堤。

3. 更新已完成的任務(wù): [C]阀坏。

4. 檢查 C 的子任務(wù)是否可以執(zhí)行。

A: 依賴 [B, C]全释,已完成任務(wù)中不包含 B误债,無法啟動(dòng)

B: 依賴 [C],已完成任務(wù)中包含 C寝蹈,可以執(zhí)行

5. 執(zhí)行任務(wù) B。

6. 重復(fù)步驟 3封字,直到所有任務(wù)執(zhí)行完成耍鬓。

下面我們就用代碼來實(shí)現(xiàn):

使用遞歸檢查循環(huán)依賴:

private fun checkCircularDependency(
    chain: List<String>,
    depends: Set<String>,
    taskMap: Map<String, TaskInfo>
) {
    depends.forEach { depend ->
        check(chain.contains(depend).not()) {
            "Found circular dependency chain: $chain -> $depend"
        }
        taskMap[depend]?.let { task ->
            checkCircularDependency(chain + depend, task.depends, taskMap)
        }
    }
}

梳理子任務(wù):

task.depends.forEach {
    val depend = taskMap[it]
    checkNotNull(depend) {
        "Can not find task [$it] which depend by task [${task.name}]"
    }
    depend.children.add(task)
}

執(zhí)行任務(wù):

private fun execute(task: TaskInfo) {
    if (isMatchProgress(task)) {
        val cost = measureTimeMillis {
            kotlin.runCatching {
                (task.task as IInitTask).execute(app)
            }.onFailure {
                Log.e(TAG, "executing task [${task.name}] error", it)
            }
        }
        Log.d(
            TAG, "Execute task [${task.name}] complete in process [$processName] " +
                    "thread [${Thread.currentThread().name}], cost: ${cost}ms"
        )
    } else {
        Log.w( TAG, "Skip task [${task.name}] cause the process [$processName] not match")
    }
    afterExecute(task.name, task.children)
}

如果進(jìn)程不匹配直接跳過牲蜀。

繼續(xù)執(zhí)行下一個(gè)任務(wù):

private fun afterExecute(name: String, children: Set<TaskInfo>) {
    val allowTasks = synchronized(completedTasks) {
        completedTasks.add(name)
        children.filter { completedTasks.containsAll(it.depends) }
    }
    if (ThreadUtils.isInMainThread()) {
        // 如果是主線程,先將異步任務(wù)放入隊(duì)列涣达,再執(zhí)行同步任務(wù)
        allowTasks.filter { it.background }.forEach {
            launch(Dispatchers.Default) { execute(it) }
        }
        allowTasks.filter { it.background.not() }.forEach { execute(it) }
    } else {
        allowTasks.forEach {
            val dispatcher = if (it.background) Dispatchers.Default else Dispatchers.Main
            launch(dispatcher) { execute(it) }
        }
    }
}

如果子任務(wù)的依賴任務(wù)都已經(jīng)執(zhí)行完畢度苔,就可以執(zhí)行了。

最后還需要提供一個(gè)啟動(dòng)任務(wù)的接口寇窑,為了支持多進(jìn)程,這里不能使用 ContentProvider窗市。

小結(jié)

通過層層拆解横漏,將復(fù)雜的依賴梳理清楚,用通俗易懂的方法缎浇,實(shí)現(xiàn)任務(wù)調(diào)度。

源碼

https://github.com/wangchenyan/init

另外二蓝,我也在 JitPack 上發(fā)布了 alpha 版本,歡迎大家嘗試:

kapt "com.github.wangchenyan.init:init-compiler:1-alpha.1"
implementation "com.github.wangchenyan.init:init-api:1-alpha.1"

詳細(xì)使用請(qǐng)移步 GitHub踊跟。

https://github.com/wangchenyan/init

總結(jié)

本文以 StartUp 作為引子鸥诽,闡述依賴任務(wù)啟動(dòng)框架還需要具備哪些能力,通過 APT + 字節(jié)碼注入進(jìn)行解耦牡借,支持模塊化,通過一個(gè)簡單的模型來表述任務(wù)調(diào)度具體的實(shí)現(xiàn)方式炬藤。

希望本文能夠讓大家了解依賴任務(wù)啟動(dòng)框架的核心思想碴里,如果你有好的建議,歡迎評(píng)論羹膳。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末帝火,一起剝皮案震驚了整個(gè)濱河市犀填,隨后出現(xiàn)的幾起案子嗓违,更是在濱河造成了極大的恐慌,老刑警劉巖蹂季,帶你破解...
    沈念sama閱讀 211,042評(píng)論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件偿洁,死亡現(xiàn)場離奇詭異,居然都是意外死亡涕滋,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 89,996評(píng)論 2 384
  • 文/潘曉璐 我一進(jìn)店門溯饵,熙熙樓的掌柜王于貴愁眉苦臉地迎上來丰刊,“玉大人,你說我怎么就攤上這事啄巧。” “怎么了熄求?”我有些...
    開封第一講書人閱讀 156,674評(píng)論 0 345
  • 文/不壞的土叔 我叫張陵逗概,是天一觀的道長。 經(jīng)常有香客問我卿城,道長铅搓,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,340評(píng)論 1 283
  • 正文 為了忘掉前任多望,我火速辦了婚禮氢烘,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘椎工。我一直安慰自己蜀踏,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,404評(píng)論 5 384
  • 文/花漫 我一把揭開白布颅痊。 她就那樣靜靜地躺著局待,像睡著了一般吗讶。 火紅的嫁衣襯著肌膚如雪恋捆。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,749評(píng)論 1 289
  • 那天膜毁,我揣著相機(jī)與錄音愤钾,去河邊找鬼。 笑死杂瘸,一個(gè)胖子當(dāng)著我的面吹牛伙菊,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播运翼,決...
    沈念sama閱讀 38,902評(píng)論 3 405
  • 文/蒼蘭香墨 我猛地睜開眼兴枯,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了悠夯?” 一聲冷哼從身側(cè)響起躺坟,我...
    開封第一講書人閱讀 37,662評(píng)論 0 266
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎策彤,沒想到半個(gè)月后匣摘,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體裹刮,經(jīng)...
    沈念sama閱讀 44,110評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,451評(píng)論 2 325
  • 正文 我和宋清朗相戀三年赠叼,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片瞬场。...
    茶點(diǎn)故事閱讀 38,577評(píng)論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡贯被,死狀恐怖妆艘,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情批旺,我是刑警寧澤,帶...
    沈念sama閱讀 34,258評(píng)論 4 328
  • 正文 年R本政府宣布搏熄,位于F島的核電站暇赤,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏契邀。R本人自食惡果不足惜失暴,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,848評(píng)論 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望古戴。 院中可真熱鬧矩肩,春花似錦、人聲如沸黍檩。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,726評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽棵里。三九已至姐呐,卻和暖如春典蝌,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背麦轰。 一陣腳步聲響...
    開封第一講書人閱讀 31,952評(píng)論 1 264
  • 我被黑心中介騙來泰國打工砖织, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人新锈。 一個(gè)月前我還...
    沈念sama閱讀 46,271評(píng)論 2 360
  • 正文 我出身青樓眶熬,卻偏偏與公主長得像,于是被迫代替她去往敵國和親拳缠。 傳聞我的和親對(duì)象是個(gè)殘疾皇子贸弥,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,452評(píng)論 2 348

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