作者:王晨彥 https://juejin.cn/post/7042201901399539748
前言我們在開發應用的時候,一般都會引入 SDK,而大部分 SDK 都要求我們在 Application 中初始化,當我們引入的 SDK 越來越多,就會出現 Application 越來越長,如果 SDK 的初始化任務相互依賴,還要處理很多條件判斷,這時,如果再來個異步初始化,相信大家都會崩潰。
有人可能會說,我都在主線程按順序初始化不就行了,當然行,只要老闆不來找你麻煩
「小王啊,咱們的 APP 啟動時間怎麼這麼久?」
開個玩笑,可見,一個優秀的啟動框架對於 APP 啟動性能而言,是多麼的重要!
為什麼不用 Google 的 StartUp?說到啟動框架,就不得不提 StartUp,畢竟是 Google 官方出品,現有的啟動框架,或多或少都有參考 StartUp,這裡不再詳細介紹。StartUp 提供了簡便的依賴任務初始化功能,但是對於一個複雜項目來說,StartUp 有以下不足
如果要做到完全解耦,我們可以使用 APT 收集任務
首先定義註解,即任務的一些屬性
@Target(AnnotationTarget.CLASS)@Retention(AnnotationRetention.RUNTIME)annotationclassInitTask(/***任務名稱,需唯一*/valname:String,/***是否在後台線程執行*/valbackground:Boolean=false,/***優先級,越小優先級越高*/valpriority:Int=PRIORITY_NORM,/***任務執行進程,支持主進程、非主進程、所有進程、:xxx、特定進程名*/valprocess:Array<String>=[PROCESS_ALL],/***依賴的任務*/valdepends:Array<String>=[])任務的屬性定義好,還需要一個執行任務的接口
interfaceIInitTask{funexecute(application:Application)}任務需要收集的信息已經定義好了,那麼看一下一個真正的任務長什麼樣
@InitTask(name="main",process=[InitTask.PROCESS_MAIN],depends=["lib"])classMainTask:IInitTask{overridefunexecute(application:Application){SystemClock.sleep(1000)Log.e("WCY","main1execute")}}還是比較簡潔清晰的
接下來需要通過 Annotation Processor 收集任務,然後通過 kotlin poet 寫入文件
classTaskProcessor:AbstractProcessor(){overridefunprocess(annotations:MutableSet<outTypeElement>?,roundEnv:RoundEnvironment):Boolean{valtaskElements=roundEnv.getElementsAnnotatedWith(InitTask::class.java)valtaskType=elementUtil.getTypeElement("me.wcy.init.api.IInitTask")/***Paramtype:MutableList<TaskInfo>**There'snosuchtypeasMutableListatruntimesothelibraryonlyseestheruntimetype.*IfyouneedMutableListthenyou'llneedtouseaClassNametocreateit.*[https://github.com/square/kotlinpoet/issues/482]*/valinputMapTypeName=ClassName("kotlin.collections","MutableList").parameterizedBy(TaskInfo::class.asTypeName())/***Paramname:taskList:MutableList<TaskInfo>*/valgroupParamSpec=ParameterSpec.builder(ProcessorUtils.PARAM_NAME,inputMapTypeName).build()/***Method:overridefunregister(taskList:MutableList<TaskInfo>)*/valloadTaskMethodBuilder=FunSpec.builder(ProcessorUtils.METHOD_NAME).addModifiers(KModifier.OVERRIDE).addParameter(groupParamSpec)for(elementintaskElements){valtypeMirror=element.asType()valtask=element.getAnnotation(InitTask::class.java)if(typeUtil.isSubtype(typeMirror,taskType.asType())){valtaskCn=(elementasTypeElement).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)}}/***Writetofile*/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)returntrue}}看一下生成的文件長什麼樣
publicclassTaskRegister$sample:ModuleTaskRegister{publicoverridefunregister(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 可以生成代碼,但是無法修改字節碼,也就是說我們在運行時想到拿到注入的任務,還需要將收集的任務注入到源碼中。
這裡可以藉助 AutoRegister 幫我們完成注入。注入前的代碼如下:
internalclassFinalTaskRegister{valtaskList:MutableList<TaskInfo>=mutableListOf()init{init()}privatefuninit(){}funregister(register:ModuleTaskRegister){register.register(taskList)}}將收集到的任務注入到 init 方法中,注入後的字節碼
/*compiledfrom:FinalTaskRegister.kt*/publicfinalclassFinalTaskRegister{privatefinalList<TaskInfo>taskList=newArrayList();publicFinalTaskRegister(){init();}publicfinalList<TaskInfo>getTaskList(){returnthis.taskList;}privatefinalvoidinit(){register(newTaskRegister$sample_lib());register(newTaskRegister$sample());}publicfinalvoidregister(ModuleTaskRegisterregister){Intrinsics.checkNotNullParameter(register,"register");register.register(this.taskList);}}我們通過 APT 生成的類已經成功的注入到代碼中。
至此,我們已經完成了任務的收集,通過 APT 和字節碼修改是常見的類收集方案,相比反射,字節碼修改沒有任何性能的損失。
後來發現 Google 已經推出了新的註解處理框架 ksp,處理速度更快,於是果斷嘗試了一把,所以有兩種註解處理可以選擇,GitHub 上有詳細介紹:https://github.com/wangchenyan/init
任務調度任務調度是啟動框架的核心,大家可能聽到過
處理依賴任務首先要構建一個「有向無環圖」
什麼是有向無環圖,看下維基百科的介紹
在圖論中,如果一個有向圖從任意頂點出發無法經過若干條邊回到該點,則這個圖是一個有向無環圖(DAG, Directed Acyclic Graph)。
聽起來好像很簡單,那麼具體怎麼實現呢,今天我們拋開高級概念不談,用代碼帶大家實現任務的調度。
首先,需要把任務分為兩類,有依賴的任務和無依賴的任務。
有依賴的首先檢查是否有環,如果有循環依賴,直接 throw,這個可以套用公式 —— 如何判斷鍊表是否有環
如果沒有循環依賴,則收集每個任務的被依賴任務,我們稱之為子任務,用於當前任務執行完成後,繼續執行子任務。
無依賴的最簡單,直接按照優先級執行即可。
不知道大家是否有疑問:有依賴的任務什麼時候啟動?
有依賴的任務,依賴鏈的葉子端點一定是一個無依賴的任務,因此無依賴的任務執行完成後,就可以開始執行有依賴的任務。
下面用一個小例子來介紹
樹形結構


下面我們就用代碼來實現
使用遞歸檢查循環依賴
privatefuncheckCircularDependency(chain:List<String>,depends:Set<String>,taskMap:Map<String,TaskInfo>){depends.forEach{depend->check(chain.contains(depend).not()){"Foundcirculardependencychain:$chain->$depend"}taskMap[depend]?.let{task->checkCircularDependency(chain+depend,task.depends,taskMap)}}}梳理子任務
task.depends.forEach{valdepend=taskMap[it]checkNotNull(depend){"Cannotfindtask[$it]whichdependbytask[${task.name}]"}depend.children.add(task)}執行任務
privatefunexecute(task:TaskInfo){if(isMatchProgress(task)){valcost=measureTimeMillis{kotlin.runCatching{(task.taskasIInitTask).execute(app)}.onFailure{Log.e(TAG,"executingtask[${task.name}]error",it)}}Log.d(TAG,"Executetask[${task.name}]completeinprocess[$processName]"+"thread[${Thread.currentThread().name}],cost:${cost}ms")}else{Log.w(TAG,"Skiptask[${task.name}]causetheprocess[$processName]notmatch")}afterExecute(task.name,task.children)}如果進程不匹配直接跳過
繼續執行下一個任務
privatefunafterExecute(name:String,children:Set<TaskInfo>){valallowTasks=synchronized(completedTasks){completedTasks.add(name)children.filter{completedTasks.containsAll(it.depends)}}if(ThreadUtils.isInMainThread()){//如果是主線程,先將異步任務放入隊列,再執行同步任務allowTasks.filter{it.background}.forEach{launch(Dispatchers.Default){execute(it)}}allowTasks.filter{it.background.not()}.forEach{execute(it)}}else{allowTasks.forEach{valdispatcher=if(it.background)Dispatchers.DefaultelseDispatchers.Mainlaunch(dispatcher){execute(it)}}}}如果子任務的依賴任務都已經執行完畢,就可以執行了
最後還需要提供一個啟動任務的接口,為了支持多進程,這裡不能使用 ContentProvider。
通過層層拆解,將複雜的依賴梳理清楚,用通俗易懂的方法,實現任務調度。
總結本文以 StartUp 作為引子,闡述依賴任務啟動框架還需要具備哪些能力,通過 APT + 字節碼注入進行解耦,支持模塊化,通過一個簡單的模型來表述任務調度具體的實現方式。希望本文能夠讓大家了解依賴任務啟動框架的核心思想,如果你有好的建議,歡迎評論。
https://github.com/wangchenyan/init
-- END --
推薦閱讀
還在用 ContentProvider 初始化?App Startup 了解一下
巧用 ContentProvider 實現「無侵」初始化
再見 KAPT!使用 KSP 為 Kotlin 編譯提速