作者:leobertlan 如果没有记错,15年那会Android项目逐步转向使用Gradle构建,时至今日,组件化已经不再是一个新颖的话题。 虽然我将这篇文章放在了Gradle分类中,但是我们知道,使用gradle构建的后端项目,热点聚焦在:实现微服务化,项目是拆开的,决定了依赖库已经是静态jar包,和我们要讨论的场景是不一致的。所以我们还是在Android领域中讨论这个问题。 在各种方案的组件化实施中,一定会将部分功能模块拆分,进行library下沉。于是,就有了处理依赖的场景。相信大家思考过这样一个问题:如果下沉的library也提前编译好静态aar包,我们的项目编译时间会缩短。 毋庸置疑,这样做会直接从源头解决编译时间长的问题,就是减少编译内容。但是,项目合并在一起,难免就想在开发下层library时,直接用上层业务集成进行冒烟。ps:这个做法并不好,应当为library配置好冒烟测试环境,虽然会耗费掉一定的时间。 理想归理想,最终还是会败给现实,这个问题就变成了鱼和熊掌想要兼得的问题。 为了让阅读的目标更加明确,我们先思考一个问题: 这样一个项目依赖关系,如果做到改动B的内容,却不需要重新编译A,运行APP,验证B的修改我们下面会进行一定地展开,来体悟这个问题。为什么使用远程仓库中的依赖包比使用本地静态aar要方便 我们知道,对于一个module,我们对其进行编译生成静态aar包,只会处理它自身的内容。那么他的依赖是如何传递的? 通过pom文件 举个例子: 我们新建一个module,看一下依赖:dependencies{implementationorg。jetbrains。kotlin:kotlinstdlib:kotlinversionimplementationandroidx。core:corektx:1。3。2implementationandroidx。appcompat:appcompat:1。2。0implementationcom。google。android。material:material:1。2。1testImplementationjunit:junit:4。androidTestImplementationandroidx。test。ext:junit:1。1。2androidTestImplementationandroidx。test。espresso:espressocore:3。3。0} 利用mavenplugin进行发布,会有任务生成pom文件,如下:lt;?xmlversion1。0encodingUTF8?projectxsi:schemaLocationhttp:maven。apache。orgPOM4。0。0http:maven。apache。orgxsdmaven4。0。0。xsdxmlnshttp:maven。apache。orgPOM4。0。0xmlns:xsihttp:www。w3。org2001XMLSchemainstancemodelVersion4。0。0modelVersiongroupIdleobertgroupIdBartifactIdversion1。0。0versionpackagingaarpackagingdependenciesdependencygroupIdorg。jetbrains。kotlingroupIdkotlinstdlibartifactIdversion1。4。21versionscopecompilescopedependencydependencygroupIdandroidx。coregroupIdcorektxartifactIdversion1。3。2versionscopecompilescopedependencydependencygroupIdandroidx。appcompatgroupIdappcompatartifactIdversion1。2。0versionscopecompilescopedependencydependencygroupIdcom。google。android。materialgroupIdmaterialartifactIdversion1。2。1versionscopecompilescopedependencydependenciesproject 我们发现,关于测试相关的依赖并没有被收录到pom文件中。这很合理,测试代码是针对该module的,并不需要提供给使用方,其依赖自然也不需要传递。我们知道,AGP中现在有4种声明依赖的方式(除去testXXX这种变种)apiimplementationcompileOnlyruntimeOnly runtimeOnly对应以前的apk方式声明依赖,我们直接忽略掉,测试一下生成的pom文件。dependencies{apiorg。jetbrains。kotlin:kotlinstdlib:kotlinversionimplementationandroidx。core:corektx:1。3。2compileOnlyandroidx。appcompat:appcompat:1。2。0compileOnlycom。google。android。material:material:1。2。1testImplementationjunit:junit:4。androidTestImplementationandroidx。test。ext:junit:1。1。2androidTestImplementationandroidx。test。espresso:espressocore:3。3。0}lt;?xmlversion1。0encodingUTF8?projectxsi:schemaLocationhttp:maven。apache。orgPOM4。0。0http:maven。apache。orgxsdmaven4。0。0。xsdxmlnshttp:maven。apache。orgPOM4。0。0xmlns:xsihttp:www。w3。org2001XMLSchemainstancemodelVersion4。0。0modelVersiongroupIdleobertgroupIdBartifactIdversion1。0。0versionpackagingaarpackagingdependenciesdependencygroupIdorg。jetbrains。kotlingroupIdkotlinstdlibartifactIdversion1。4。21versionscopecompilescopedependencydependencygroupIdandroidx。coregroupIdcorektxartifactIdversion1。3。2versionscopecompilescopedependencydependenciesproject 使用compileOnly方式的并没有被收录到pom文件中,而api和implementation方式,在pom文件中,都表现为采用compile的方案应用依赖。 ps:api和implementation在编码期的不同,不是我们讨论的重点,略。 回到我们开始的问题,将library发布时,按照约定,会将library本身的依赖收录到pom文件中。相应的,使用方使用仓库中的依赖项时,gradle会拉取其对应的pom文件,并添加依赖。 所以,如果我们直接使用一个编译好的静态包,而丢弃了他对应的pom文件时,可能会丢失依赖,出现打包失败或者运行异常。这意味着我们需要人为维护依赖传递 我们记住这些内容,并先放到一边。下沉后,library会有多个层级 例如图中:APPAB,即APP依赖A,A依赖B,而A和B都是library 我们知道,对于B,并不会有什么说法,只会出现在A和APP 如果不使用静态包,那么A会声明:apiproject(:B)或者implementationproject(:B) 我们先看一下,这样生成的libraryA的pom文件lt;?xmlversion1。0encodingUTF8?projectxsi:schemaLocationhttp:maven。apache。orgPOM4。0。0http:maven。apache。orgxsdmaven4。0。0。xsdxmlnshttp:maven。apache。orgPOM4。0。0xmlns:xsihttp:www。w3。org2001XMLSchemainstancemodelVersion4。0。0modelVersiongroupIdleobertgroupIdAartifactIdversion1。0。0versionpackagingaarpackagingdependenciesdependencygroupIdDemogroupIdBartifactIdversionunspecifiedversionscopecompilescopedependencydependenciesproject 会得到groupID是项目名,artifactId是module名,version是未知的一个依赖项。假如我将A编译为静态包并发布到仓库,并运用了pom中的依赖描述,一定会得到无法找到:DemoBunspecified。pom的问题。 当然,这个问题可以通过在APP中重新声明B的依赖来解决。 这意味着,我们需要时刻保持警惕,维护各个module的依赖。否则,我们无法同时享受:静态包减少编译随心的修改局部并集成测试 这显然是一件不人道主义的事情。 反思一下,对于A而言,它需要B,但仅在两个时机需要:编译时受检,完成编译运行时 作为一个library,它本身并不对应运行时,所以,compileOnly是其声明对B的依赖的最佳方式。这意味着,最终对应运行时的内容,即APP,需要在编译时加入对B的依赖。在原先A使用Api方式声明对B的依赖时,是通过gradle分析pom文件实现的依赖加入。而现在,需要人为维护,只需要实现人道主义,就可以鱼和熊掌兼得。反思依赖传递的本质 一般我们会像下面的演示代码一样声明依赖:APP:implementationproject(A)implementationproject(Foo)A:implementationproject(B)implementationproject(Bar) 因为依赖传递性,APP其实依赖了A,Foo,B,Bar。其实就是一颗树中,除去根节点的节点集合。而对于一个非根节点,它被依赖的形式只有两种:静态包,不需要重新编译,节约编译时间module,需要再次编译,可以运用最新改动 我们可以定义这样一个键值对信息:project。ext。depRules〔B:p,A:a〕 p代表使用project,a代表使用静态包。 并将这颗树的内容表达出来:我们先忽略掉Foo和Barproject。ext。deps〔A:〔B:〔p:project(:B),a:leobert:B:1。0。0〕〕,APP:〔A:〔p:project(:A),a:leobert:A:1。0。0〕〕〕。with(true){A。each{eAPP。put(e。key,e。value)}} 以A为例,我们可以通过代码实现动态添加依赖:project。afterEvaluate{pprintln(handledepsfor:p)deps。A。each{edefruledepRules。get(e。key)println(finddepsofA:ruleisrule,depis:e。value。get(rule)。toString())project。dependencies。add(compileOnly,e。value。get(rule))}} 同理,对于APP:project。afterEvaluate{pprintln(handledepsfor:p)deps。APP。each{edefruledepRules。get(e。key)println(finddepsofApp:ruleisrule,depis:e。value。get(rule)。toString())project。dependencies。add(implementation,e。value。get(rule))}} 查看输出:Configureproject:Ahandledepsfor:project:AfinddepsofA:ruleisp,depis:project:BConfigureproject:apphandledepsfor:project:appfinddepsofApp:ruleisa,depis:leobert:A:1。0。0finddepsofApp:ruleisp,depis:project:B 这样,我们就可以通过修改对应节点的依赖方式配置而实现鱼和熊掌兼得。不再受pom文件的约束。当时,我们回到上面说的不人道主义之处,我们通过了with函数,将A自身的依赖信息,注入到APP中。 但是当树的规模变大时,人为维护就很累了。这是必须要解决的,当然,这很容易解决。我们直接使用递归处理即可 贴近人的直观感受才优雅,逐步实现人道主义我们添加一个全局闭包:ext。utils〔applyDependency:{project,edefruledepRules。get(e。key)println(finddepsofApp:ruleisrule,depis:e。value。get(rule)。toString())project。dependencies。add(implementation,e。value。get(rule))try{println(trytoaddsubdepsof:e。key)defsubdeps。get(e。key)if(sub!nullsub。get(isEnd)!true){sub。each{seext。utils。applyDependency(project,se)}}}catch(Exceptionignore){}}〕 注意,因为我们定义的依赖信息是:moduleName(moduleName(scopeNamedepInfo))的方式。 这导致我们判断末端节点有一定的困难,即递归的尾部判断存在困难,我们需要人为标记一下末端节点这时,我们只需描述一下树即可:同样忽略Foo,Barproject。ext。deps〔A:〔B:〔isEnd:true,p:project(:B),a:leobert:B:1。0。0〕〕,APP:〔A:〔p:project(:A),a:leobert:A:1。0。0〕〕〕 问题基本得到解决了,但是并不优雅。优雅,优雅,优雅 我们不妨再修改一下对依赖树的描述方式,将节点信息和树结构分开,重新改进: 更人道主义的依赖描述project。ext。deps〔A:〔B〕,app:〔A〕〕project。ext。modules〔A:〔p:project(:A),a:leobert:A:1。0。0〕,B:〔p:project(:B),a:leobert:B:1。0。0〕〕project。ext。depRules〔B:p,A:a〕 抽象添加依赖的过程,递归处理每一个节点的依赖收集,并向宿主module添加,当某个节点在ext。deps中没有任何依赖时,归:ext。utils〔applyDependency:{project,scope,edefruledepRules。get(e)defeInfoext。modules。get(e)println(finddepsofproject:ruleisrule,depis:eInfo。get(rule)。toString())project。dependencies。add(scope,eInfo。get(rule))defsubdeps。get(e)listdepsofeprintln(trytoaddsubdepsof:esub)if(sub!null!sub。isEmpty()){sub。each{dOfEext。utils。applyDependency(project,scope,dOfE)}}}〕 每个module只需要指定自己的scope::appproject。afterEvaluate{pprintln(handledepsfor:p)deps。get(p。name)。each{erootProject。ext。utils。applyDependency(p,implementation,e)}}:Aproject。afterEvaluate{pprintln(handledepsfor:p。name)deps。get(p。name)。each{erootProject。ext。utils。applyDependency(p,compileOnly,e)}} 只要不是独立运行的module,就是compileOnly,否则就是implementation。输出也容易拍错:Configureproject:Ahandledepsfor:Afinddepsofproject:A:ruleisp,depis:project:Btrytoaddsubdepsof:BnullConfigureproject:apphandledepsfor:project:appfinddepsofproject:app:ruleisa,depis:leobert:A:1。0。0trytoaddsubdepsof:A〔B〕finddepsofproject:app:ruleisp,depis:project:Btrytoaddsubdepsof:Bnull 测试一个复杂场景我们再上图的基础上,让B和Foo依赖Baseproject。ext。deps〔app:〔A,Foo〕,A:〔B,Bar〕,Foo:〔Base〕,B:〔Base〕,〕project。ext。modules〔A:〔p:project(:A),a:leobert:A:1。0。0〕,B:〔p:project(:B),a:leobert:B:1。0。0〕,Foo:〔p:project(:Foo),〕,Bar:〔p:project(:Bar),〕,Base:〔p:project(:Base),〕〕project。ext。depRules〔B:p,A:a,Foo:p,Bar:p,Base:p〕Configureproject:Ahandledepsfor:Afinddepsofproject:A:ruleisp,depis:project:Btrytoaddsubdepsof:B〔Base〕finddepsofproject:A:ruleisp,depis:project:Basetrytoaddsubdepsof:Basenullfinddepsofproject:A:ruleisp,depis:project:Bartrytoaddsubdepsof:BarnullConfigureproject:apphandledepsfor:project:appfinddepsofproject:app:ruleisa,depis:leobert:A:1。0。0trytoaddsubdepsof:A〔B,Bar〕finddepsofproject:app:ruleisp,depis:project:Btrytoaddsubdepsof:B〔Base〕finddepsofproject:app:ruleisp,depis:project:Basetrytoaddsubdepsof:Basenullfinddepsofproject:app:ruleisp,depis:project:Bartrytoaddsubdepsof:Barnullfinddepsofproject:app:ruleisp,depis:project:Footrytoaddsubdepsof:Foo〔Base〕finddepsofproject:app:ruleisp,depis:project:Basetrytoaddsubdepsof:BasenullConfigureproject:Barhandledepsfor:BarConfigureproject:Basehandledepsfor:BaseConfigureproject:Foohandledepsfor:Foofinddepsofproject:Foo:ruleisp,depis:project:Basetrytoaddsubdepsof:Basenull 随着,树规模的增大,阅读依赖关系还算明显,但是阅读日志,又不太优雅了。总结和展望 我们通过探寻,发现了一种可以鱼和熊掌兼得地依赖处理方式,让我们在Android领域组件化场景下(单项目,多module),能够灵活地切换:静态包依赖,缩短编译时间项目依赖,快速部署变更进行集成测试 对了,上面我们没有重点提到如何切换,其实非常地简单: 只需要修改project。ext。depRules中对应的配置项即可。 如果后面还有闲情逸致的话,可以再写一个studio的插件,获取dependency。gradle的信息,输出可视化的依赖树;rule配置,直接做成多个开关,优雅,永不过时。最后 在这里就再分享一份由大佬亲自收录整理的Android学习PDF架构视频面试文档源码笔记,高级架构技术进阶脑图、Android开发面试专题资料,高级进阶架构资料 这些都是我现在闲暇时还会反复翻阅的精品资料。里面对近几年的大厂面试高频知识点都有详细的讲解。相信可以有效地帮助大家掌握知识、理解原理,帮助大家在未来取得一份不错的答卷。 当然,你也可以拿去查漏补缺,提升自身的竞争力。 真心希望可以帮助到大家,Android路漫漫,共勉! 如果你有需要的话,只需私信我【进阶】即可获取