作者:王晨彦 我们在开发应用的时候,一般都会引入SDK,而大部分SDK都要求我们在Application中初始化,当我们引入的SDK越来越多,就会出现Application越来越长,如果SDK的初始化任务相互依赖,还要处理很多条件判断,这时,如果再来个异步初始化,相信大家都会崩溃。 有人可能会说,我都在主线程按顺序初始化不就行了,当然行,只要老板不来找你麻烦。 小王啊,咱们的APP启动时间怎么这么久? 开个玩笑,可见,一个优秀的启动框架对于APP启动性能而言,是多么的重要!一、为什么不用Google的StartUp? 说到启动框架,就不得不提StartUp,毕竟是Google官方出品,现有的启动框架,或多或少都有参考StartUp,这里不再详细介绍,如果对StartUp还不了解,可以参考这篇文章Jetpack系列之AppStartup从入门到出家。 https:juejin。cnpost7023643365048582174 StartUp提供了简便的依赖任务初始化功能,但是对于一个复杂项目来说,StartUp有以下不足: 1。不支持异步任务 如果通过ContentProvider启动,所有任务都在主线程执行,如果通过接口启动,所有任务都在同一个线程执行。 2。不支持组件化 通过Class指定依赖任务,需要引用依赖的模块。 3。不支持多进程 无法单独配置任务需要执行的进程。 4。不支持启动优先级 虽然可以通过指定依赖来设置优先级,但是过于复杂。二、一个合格的启动框架是怎么样的? 1。支持异步任务 减少启动时间的有效手段。 2。支持组件化 其实就是解耦,一方面是解耦任务依赖,另一方面是解耦app和module的依赖。 3。支持任务依赖 可以简化我们的任务调度。 4。支持优先级 在没有依赖的情况下,允许任务优先执行。 5。支持多进程 只在需要的进程中执行初始化任务,可以减轻系统负载,侧面提升APP启动速度。三、收集任务 如果要做到完全解耦,我们可以使用APT收集任务。 首先定义注解,即任务的一些属性。Target(AnnotationTarget。CLASS)Retention(AnnotationRetention。RUNTIME)annotationclassInitTask(任务名称,需唯一valname:String,是否在后台线程执行valbackground:Booleanfalse,优先级,越小优先级越高valpriority:IntPRIORITYNORM,任务执行进程,支持主进程、非主进程、所有进程、:xxx、特定进程名valprocess:ArrayString〔PROCESSALL〕,依赖的任务valdepends:ArrayString〔〕) name作为任务唯一标识,类型为String主要是解耦任务依赖。 background即是否后台执行。 priority是在主线程、无依赖场景下的执行顺序。 process指定了任务执行的进程,支持主进程、非主进程、所有进程、:xxx、特定进程名。 depends指定依赖的任务。 任务的属性定义好,还需要一个执行任务的接口:interfaceIInitTask{funexecute(application:Application)} 任务需要收集的信息已经定义好了,那么看一下一个真正的任务长什么样。InitTask(namemain,process〔InitTask。PROCESSMAIN〕,depends〔lib〕)classMainTask:IInitTask{overridefunexecute(application:Application){SystemClock。sleep(1000)Log。e(WCY,main1execute)}} 还是比较简洁清晰的。 接下来需要通过AnnotationProcessor收集任务,然后通过kotlinpoet写入文件。classTaskProcessor:AbstractProcessor(){overridefunprocess(annotations:MutableSetoutTypeElement?,roundEnv:RoundEnvironment):Boolean{valtaskElementsroundEnv。getElementsAnnotatedWith(InitTask::class。java)valtaskTypeelementUtil。getTypeElement(me。wcy。init。api。IInitTask)Paramtype:MutableListTaskInfoTheresnosuchtypeasMutableListatruntimesothelibraryonlyseestheruntimetype。IfyouneedMutableListthenyoullneedtouseaClassNametocreateit。〔https:github。comsquarekotlinpoetissues482〕valinputMapTypeNameClassName(kotlin。collections,MutableList)。parameterizedBy(TaskInfo::class。asTypeName())Paramname:taskList:MutableListTaskInfovalgroupParamSpecParameterSpec。builder(ProcessorUtils。PARAMNAME,inputMapTypeName)。build()Method:overridefunregister(taskList:MutableListTaskInfo)valloadTaskMethodBuilderFunSpec。builder(ProcessorUtils。METHODNAME)。addModifiers(KModifier。OVERRIDE)。addParameter(groupParamSpec)for(elementintaskElements){valtypeMirrorelement。asType()valtaskelement。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。PARAMNAME,TaskInfo::class。java,task。name,task。background,task。priority,ProcessorUtils。formatArray(task。process),ProcessorUtils。formatArray(task。depends),taskCn)}}WritetofileFileSpec。builder(ProcessorUtils。PACKAGENAME,TaskRegistermoduleName)。addType(TypeSpec。classBuilder(TaskRegistermoduleName)。addKdoc(ProcessorUtils。JAVADOC)。addSuperinterface(ModuleTaskRegister::class。java)。addFunction(loadTaskMethodBuilder。build())。build())。build()。writeTo(filer)returntrue}} 看一下生成的文件长什么样。publicclassTaskRegistersample:ModuleTaskRegister{publicoverridefunregister(taskList:MutableListTaskInfo):Unit{taskList。add(TaskInfo(main2,true,0,arrayOf(PROCESSALL),arrayOf(main1,lib1),MainTask2()))taskList。add(TaskInfo(main3,false,1000,arrayOf(PROCESSALL),arrayOf(),MainTask3()))taskList。add(TaskInfo(main1,false,0,arrayOf(PROCESSMAIN),arrayOf(lib1),MainTask()))}} sample模块收集到了3个任务,TaskInfo对任务信息做了聚合。 我们知道APT可以生成代码,但是无法修改字节码,也就是说我们在运行时想到拿到注入的任务,还需要将收集的任务注入到源码中。 这里可以借助AutoRegister帮我们完成注入。 https:github。comluckybillyAutoRegister 注入前:internalclassFinalTaskRegister{valtaskList:MutableListTaskInfomutableListOf()init{init()}privatefuninit(){}funregister(register:ModuleTaskRegister){register。register(taskList)}} 将收集到的任务注入到init方法中,注入后的字节码:compiledfrom:FinalTaskRegister。ktpublicfinalclassFinalTaskRegister{privatefinalListTaskInfotaskListnewArrayList();publicFinalTaskRegister(){init();}publicfinalListTaskInfogetTaskList(){returnthis。taskList;}privatefinalvoidinit(){register(newTaskRegistersamplelib());register(newTaskRegistersample());}publicfinalvoidregister(ModuleTaskRegisterregister){Intrinsics。checkNotNullParameter(register,register);register。register(this。taskList);}} 我们通过APT生成的类已经成功的注入到代码中。 小结 至此,我们已经完成了任务的收集,通过APT和字节码修改是常见的类收集方案,相比反射,字节码修改没有任何性能的损失。 后来发现Google已经推出了新的注解处理框架ksp,处理速度更快,于是果断尝试了一把,所以有两种注解处理可以选择,GitHub上有详细介绍。四、任务调度 任务调度是启动框架的核心,大家可能听到过。 处理依赖任务首先要构建一个有向无环图。 什么是有向无环图,看下维基百科的介绍: 在图论中,如果一个有向图从任意顶点出发无法经过若干条边回到该点,则这个图是一个有向无环图(DAG,DirectedAcyclicGraph)。 听起来好像很简单,那么具体怎么实现呢,今天我们抛开高级概念不谈,用代码带大家实现任务的调度。 首先,需要把任务分为两类,有依赖的任务和无依赖的任务。 有依赖的首先检查是否有环,如果有循环依赖,直接throw,这个可以套用公式如何判断链表是否有环。 如果没有循环依赖,则收集每个任务的被依赖任务,我们称之为子任务,用于当前任务执行完成后,继续执行子任务。 无依赖的最简单,直接按照优先级执行即可。 不知道大家是否有疑问:有依赖的任务什么时候启动? 有依赖的任务,依赖链的叶子端点一定是一个无依赖的任务,因此无依赖的任务执行完成后,就可以开始执行有依赖的任务。 下面用一个小例子来介绍:A依赖B、CB依赖CC无依赖 树形结构: 1、分组并梳理子任务。有依赖: A:无子任务 B:子任务:〔A〕无依赖: C:子任务:〔A,B〕 2、执行无依赖的任务C。 3、更新已完成的任务:〔C〕。 4、检查C的子任务是否可以执行。 A:依赖〔B,C〕,已完成任务中不包含B,无法启动 B:依赖〔C〕,已完成任务中包含C,可以执行 5、执行任务B。 6、重复步骤3,直到所有任务执行完成。 下面我们就用代码来实现: 使用递归检查循环依赖:privatefuncheckCircularDependency(chain:ListString,depends:SetString,taskMap:MapString,TaskInfo){depends。forEach{dependcheck(chain。contains(depend)。not()){Foundcirculardependencychain:chaindepend}taskMap〔depend〕?。let{taskcheckCircularDependency(chaindepend,task。depends,taskMap)}}} 梳理子任务:task。depends。forEach{valdependtaskMap〔it〕checkNotNull(depend){Cannotfindtask〔it〕whichdependbytask〔{task。name}〕}depend。children。add(task)} 执行任务:privatefunexecute(task:TaskInfo){if(isMatchProgress(task)){valcostmeasureTimeMillis{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:SetTaskInfo){valallowTaskssynchronized(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{valdispatcherif(it。background)Dispatchers。DefaultelseDispatchers。Mainlaunch(dispatcher){execute(it)}}}} 如果子任务的依赖任务都已经执行完毕,就可以执行了。 最后还需要提供一个启动任务的接口,为了支持多进程,这里不能使用ContentProvider。 小结 通过层层拆解,将复杂的依赖梳理清楚,用通俗易懂的方法,实现任务调度。 源码 https:github。comwangchenyaninit 另外,我也在JitPack上发布了alpha版本,欢迎大家尝试:kaptcom。github。wangchenyan。init:initcompiler:1alpha。1implementationcom。github。wangchenyan。init:initapi:1alpha。1 详细使用请移步GitHub。 https:github。comwangchenyaninit最后 本文以StartUp作为引子,阐述依赖任务启动框架还需要具备哪些能力,通过APT字节码注入进行解耦,支持模块化,通过一个简单的模型来表述任务调度具体的实现方式。 希望本文能够让大家了解依赖任务启动框架的核心思想,如果你有好的建议,欢迎评论交流探讨。 在这里就还分享一份由大佬亲自收录整理的学习PDF架构视频面试文档源码笔记,高级架构技术进阶脑图、Android开发面试专题资料,高级进阶架构资料 这些都是我现在闲暇时还会反复翻阅的精品资料。里面对近几年的大厂面试高频知识点都有详细的讲解。相信可以有效地帮助大家掌握知识、理解原理,帮助大家在未来取得一份不错的答卷。 当然,你也可以拿去查漏补缺,提升自身的竞争力。 真心希望可以帮助到大家,Android路漫漫,共勉! 如果你有需要的话,只需私信我【进阶】即可获取