聽說你還不懂依賴任務啟動框架寓涨?帶你擼一個

前言

我們在開發(fā)應用的時候,一般都會引入 SDK氯檐,而大部分 SDK 都要求我們在 Application 中初始化缅茉,當我們引入的 SDK 越來越多,就會出現(xiàn) Application 越來越長男摧,如果 SDK 的初始化任務相互依賴蔬墩,還要處理很多條件判斷,這時耗拓,如果再來個異步初始化拇颅,相信大家都會崩潰。

有人可能會說乔询,我都在主線程按順序初始化不就行了樟插,當然行,只要老板不來找你麻煩

「小王啊,咱們的 APP 啟動時間怎么這么久黄锤?」

開個玩笑搪缨,可見,一個優(yōu)秀的啟動框架對于 APP 啟動性能而言鸵熟,是多么的重要副编!

為什么不用 Google 的 StartUp?

說到啟動框架,就不得不提 StartUp流强,畢竟是 Google 官方出品痹届,現(xiàn)有的啟動框架,或多或少都有參考 StartUp打月,這里不再詳細介紹队腐,如果對 StartUp 還不了解,可以參考這篇文章 Jetpack系列之App Startup從入門到出家

StartUp 提供了簡便的依賴任務初始化功能奏篙,但是對于一個復雜項目來說柴淘,StartUp 有以下不足

  1. 不支持異步任務
    如果通過 ContentProvider 啟動,所有任務都在主線程執(zhí)行秘通,如果通過接口啟動悠就,所有任務都在同一個線程執(zhí)行

  2. 不支持組件化
    通過 Class 指定依賴任務,需要引用依賴的模塊

  3. 不支持多進程
    無法單獨配置任務需要執(zhí)行的進程

  4. 不支持啟動優(yōu)先級
    雖然可以通過指定依賴來設置優(yōu)先級充易,但是過于復雜

一個合格的啟動框架是怎么樣的梗脾?

  1. 支持異步任務
    減少啟動時間的有效手段

  2. 支持組件化
    其實就是解耦,一方面是解耦任務依賴盹靴,另一方面是解耦 app 和 module 的依賴

  3. 支持任務依賴
    可以簡化我們的任務調(diào)度

  4. 支持優(yōu)先級
    在沒有依賴的情況下炸茧,允許任務優(yōu)先執(zhí)行

  5. 支持多進程
    只在需要的進程中執(zhí)行初始化任務,可以減輕系統(tǒng)負載稿静,側(cè)面提升 APP 啟動速度

收集任務

如果要做到完全解耦梭冠,我們可以使用 APT 收集任務

首先定義注解,即任務的一些屬性

@Target(AnnotationTarget.CLASS)
@Retention(AnnotationRetention.RUNTIME)
annotation class InitTask(
    /**
     * 任務名稱改备,需唯一
     */
    val name: String,
    /**
     * 是否在后臺線程執(zhí)行
     */
    val background: Boolean = false,
    /**
     * 優(yōu)先級控漠,越小優(yōu)先級越高
     */
    val priority: Int = PRIORITY_NORM,
    /**
     * 任務執(zhí)行進程,支持主進程悬钳、非主進程盐捷、所有進程、:xxx默勾、特定進程名
     */
    val process: Array<String> = [PROCESS_ALL],
    /**
     * 依賴的任務
     */
    val depends: Array<String> = []
)
  • name 作為任務唯一標識碉渡,類型為 String 主要是解耦任務依賴
  • background 即是否后臺執(zhí)行
  • priority 是在主線程、無依賴場景下的執(zhí)行順序
  • process 指定了任務執(zhí)行的進程母剥,支持主進程滞诺、非主進程形导、所有進程、:xxx习霹、特定進程名
  • depends 指定依賴的任務

任務的屬性定義好朵耕,還需要一個執(zhí)行任務的接口

interface IInitTask {
    fun execute(application: Application)
}

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

@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 收集任務淋叶,然后通過 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個任務阎曹,TaskInfo 對任務信息做了聚合。

我們知道 APT 可以生成代碼爸吮,但是無法修改字節(jié)碼芬膝,也就是說我們在運行時想到拿到注入的任務望门,還需要將收集的任務注入到源碼中形娇。

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

注入前

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

    init {
        init()
    }

    private fun init() {}

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

將收集到的任務注入到 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)完成了任務的收集厨剪,通過 APT 和字節(jié)碼修改是常見的類收集方案哄酝,相比反射,字節(jié)碼修改沒有任何性能的損失祷膳。

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

任務調(diào)度

任務調(diào)度是啟動框架的核心勇皇,大家可能聽到過

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

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

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

聽起來好像很簡單,那么具體怎么實現(xiàn)呢兄淫,今天我們拋開高級概念不談屯远,用代碼帶大家實現(xiàn)任務的調(diào)度。

首先捕虽,需要把任務分為兩類氓润,有依賴的任務和無依賴的任務。

有依賴的首先檢查是否有環(huán)薯鳍,如果有循環(huán)依賴咖气,直接 throw挨措,這個可以套用公式 —— 如何判斷鏈表是否有環(huán)

如果沒有循環(huán)依賴,則收集每個任務的被依賴任務崩溪,我們稱之為子任務浅役,用于當前任務執(zhí)行完成后,繼續(xù)執(zhí)行子任務伶唯。

無依賴的最簡單觉既,直接按照優(yōu)先級執(zhí)行即可。

不知道大家是否有疑問:有依賴的任務什么時候啟動乳幸?

有依賴的任務瞪讼,依賴鏈的葉子端點一定是一個無依賴的任務,因此無依賴的任務執(zhí)行完成后粹断,就可以開始執(zhí)行有依賴的任務符欠。

下面用一個小例子來介紹

  • A 依賴 BC
  • B 依賴 C
  • C 無依賴

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

image.png
  1. 分組并梳理子任務
  • 有依賴
    • A: 無子任務
    • B: 子任務: [A]
  • 無依賴
    • C: 子任務: [A, B]
image.png
  1. 執(zhí)行無依賴的任務C
  2. 更新已完成的任務: [C]
  3. 檢查 C 的子任務是否可以執(zhí)行
  • A: 依賴 [B, C]瓶埋,已完成任務中不包含 B希柿,無法啟動
  • B: 依賴 [C],已完成任務中包含 C养筒,可以執(zhí)行
  1. 執(zhí)行任務 B
  2. 重復步驟 3曾撤,直到所有任務執(zhí)行完成

下面我們就用代碼來實現(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)
        }
    }
}

梳理子任務

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í)行任務

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

如果進程不匹配直接跳過

繼續(xù)執(zhí)行下一個任務

private fun afterExecute(name: String, children: Set<TaskInfo>) {
    val allowTasks = synchronized(completedTasks) {
        completedTasks.add(name)
        children.filter { completedTasks.containsAll(it.depends) }
    }
    if (ThreadUtils.isInMainThread()) {
        // 如果是主線程,先將異步任務放入隊列晕粪,再執(zhí)行同步任務
        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) }
        }
    }
}

如果子任務的依賴任務都已經(jīng)執(zhí)行完畢挤悉,就可以執(zhí)行了

最后還需要提供一個啟動任務的接口,為了支持多進程巫湘,這里不能使用 ContentProvider装悲。

小結(jié)

通過層層拆解,將復雜的依賴梳理清楚剩膘,用通俗易懂的方法衅斩,實現(xiàn)任務調(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"

詳細使用請移步 GitHub

總結(jié)

本文以 StartUp 作為引子,闡述依賴任務啟動框架還需要具備哪些能力奈懒,通過 APT + 字節(jié)碼注入進行解耦奠涌,支持模塊化,通過一個簡單的模型來表述任務調(diào)度具體的實現(xiàn)方式磷杏。

希望本文能夠讓大家了解依賴任務啟動框架的核心思想溜畅,如果你有好的建議,歡迎評論极祸。

參考

Kotlin + Flow 實現(xiàn)的 Android 應用初始化任務啟動庫

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末慈格,一起剝皮案震驚了整個濱河市怠晴,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌浴捆,老刑警劉巖蒜田,帶你破解...
    沈念sama閱讀 216,496評論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異选泻,居然都是意外死亡冲粤,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,407評論 3 392
  • 文/潘曉璐 我一進店門页眯,熙熙樓的掌柜王于貴愁眉苦臉地迎上來梯捕,“玉大人,你說我怎么就攤上這事窝撵】耍” “怎么了?”我有些...
    開封第一講書人閱讀 162,632評論 0 353
  • 文/不壞的土叔 我叫張陵忿族,是天一觀的道長锣笨。 經(jīng)常有香客問我蝌矛,道長道批,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,180評論 1 292
  • 正文 為了忘掉前任入撒,我火速辦了婚禮隆豹,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘茅逮。我一直安慰自己璃赡,他們只是感情好,可當我...
    茶點故事閱讀 67,198評論 6 388
  • 文/花漫 我一把揭開白布献雅。 她就那樣靜靜地躺著碉考,像睡著了一般。 火紅的嫁衣襯著肌膚如雪挺身。 梳的紋絲不亂的頭發(fā)上侯谁,一...
    開封第一講書人閱讀 51,165評論 1 299
  • 那天,我揣著相機與錄音章钾,去河邊找鬼墙贱。 笑死,一個胖子當著我的面吹牛贱傀,可吹牛的內(nèi)容都是我干的惨撇。 我是一名探鬼主播,決...
    沈念sama閱讀 40,052評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼府寒,長吁一口氣:“原來是場噩夢啊……” “哼魁衙!你這毒婦竟也來了报腔?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 38,910評論 0 274
  • 序言:老撾萬榮一對情侶失蹤剖淀,失蹤者是張志新(化名)和其女友劉穎榄笙,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體祷蝌,經(jīng)...
    沈念sama閱讀 45,324評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡茅撞,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,542評論 2 332
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了巨朦。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片米丘。...
    茶點故事閱讀 39,711評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖糊啡,靈堂內(nèi)的尸體忽然破棺而出拄查,到底是詐尸還是另有隱情,我是刑警寧澤棚蓄,帶...
    沈念sama閱讀 35,424評論 5 343
  • 正文 年R本政府宣布堕扶,位于F島的核電站,受9級特大地震影響梭依,放射性物質(zhì)發(fā)生泄漏稍算。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,017評論 3 326
  • 文/蒙蒙 一役拴、第九天 我趴在偏房一處隱蔽的房頂上張望糊探。 院中可真熱鬧,春花似錦河闰、人聲如沸科平。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,668評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽瞪慧。三九已至,卻和暖如春部念,著一層夾襖步出監(jiān)牢的瞬間弃酌,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,823評論 1 269
  • 我被黑心中介騙來泰國打工印机, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留矢腻,地道東北人。 一個月前我還...
    沈念sama閱讀 47,722評論 2 368
  • 正文 我出身青樓射赛,卻偏偏與公主長得像多柑,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子楣责,可洞房花燭夜當晚...
    茶點故事閱讀 44,611評論 2 353

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