用模块化来管理你的Android项目

时间:2022-10-01 13:19:02

前言

在模块化和组件化横行的今天,module的数量越来越多,module数量增加的同时也给项目编译带来了极大的负担,相信大家都经历过一次冷编译耗时五六分钟,甚至七八分钟的时候,编译优化显然是势在必行,一种常见的思路是将module打包成aar本地引入,这样在编译速度上能有一个明显的提升,一些跨部门通用的module组件我们更是会发布到远程仓库来使用,而绝大多数情况下我们使用本地仓库就够了,虽然编译速度提升了,但发布配置和依赖切换依旧让人觉得麻烦,因此,一套简单高效的模块管理方案显得尤为重要。

用模块化来管理你的Android项目

module->aar

方案实践前,我们先来看一些aar常规发布的问题,有的人会说,既然绝大多数情况本地引入就够了,那为什么还需要发布到仓库呢,踩过坑的同学都知道直接引用本地aar,内部的第三方依赖关系是不能传递出来的,但很明显,我们在使用远程仓库的时候不会出现这个问题,这是因为我们从远程仓库拉取第三方库时拉取的不仅仅是aar文件,还有一个很重要的文件,pom文件。

pom文件

pom文件是什么?在官方的介绍里,pom文件就是一个maven项目的所有。简单一点说,pom是一个xml文件,定义了一系列的元素和依赖关系,这里我就不吹了,再吹也吹不过官方文档,大家有兴趣的可以去看看官方文档。

官方文档地址:

  • maven.apache.org/pom.html

继续回到我们的module来,将module发布到本地仓库就一定能生成完整的pom文件吗?当然不一定,如果你是从网上随便抄代码发布的话,你或许会发现根本无法生成pom文件,或者生成的pom文件不包含第三方依赖,从稳定性考虑,我们应当了解pom文件是如何生成的,在gradle源码里面,官方为我们提供了大量常用的插件,其中就包括了我们用来发布maven产物的插件maven-publish。

  1. applyplugin:'maven-publish'

maven-publish插件为我们提供了以maven产物形式发布到maven仓库的能力。当我们使用maven-publish发布maven物件到仓库时,maven-publish会自动为我们生成pom文件,maven-publish插件的实现类是 MavenPublishPlugin,让我们来跟下源码看下MavenPublishPlugin是如何生成pom文件的,我们尽量不去陷入到繁琐的源码探索里面。

MavenPublishPlugin

  1. @Override
  2. publicvoidapply(finalProjectproject){
  3. project.getPluginManager().apply(PublishingPlugin.class);
  4. ...
  5. project.getExtensions().configure(PublishingExtension.class,extension->{
  6. ...
  7. realizePublishingTasksLater(project,extension);
  8. });
  9. }

可以看到在加载maven-publish插件的同时立马加载了PublishingPlugin插件,这个插件是用来构建Publication的,我们暂时不需要管它,往下走,来到realizePublishingTasksLater(project, extension);

MavenPublishPlugin#realizePublishingTasksLater

  1. privatevoidrealizePublishingTasksLater(finalProjectproject,finalPublishingExtensionextension){
  2. finalNamedDomainObjectSetmavenPublications=extension.getPublications().withType(MavenPublicationInternal.class);
  3. ...
  4. mavenPublications.all(publication->{
  5. ...
  6. this.createGeneratePomTask(tasks,publication,buildDirectory,project);
  7. createLocalInstallTask(tasks,publishLocalLifecycleTask,publication);
  8. ...
  9. });
  10. }

为了不陷入到源码里面,尽量只展示相关的部分,这里做的事也很简单,从project的PublishingExtension里面取出所有类型为MavenPublicationInternal的Publication。

这话有点绕,理解为拿到当前project下所有的MavenPublication就可以了,MavenPublication是gradle用来表示Maven格式的发布件,拿到MavenPublication之后可以看到插件为每个MavenPublication都创建了构建Pom的任务,继续看createGeneratePomTask方法。

MavenPublishPlugin#createGeneratePomTask

  1. privatevoidcreateGeneratePomTask(TaskContainertasks,finalMavenPublicationInternalpublication,finalDirectoryPropertybuildDir,finalProjectproject){
  2. finalStringpublicationName=publication.getName();
  3. StringdescriptorTaskName="generatePomFileFor"+capitalize(publicationName)+"Publication";
  4. TaskProvidergeneratorTask=tasks.register(descriptorTaskName,GenerateMavenPom.class,generatePomTask->{
  5. ...
  6. generatePomTask.setPom(publication.getPom());
  7. if(generatePomTask.getDestination()==null){
  8. generatePomTask.setDestination(buildDir.file("publications/"+publication.getName()+"/pom-default.xml"));
  9. }
  10. ...
  11. publication.setPomGenerator(generatorTask);
  12. }

这里创建了生成pom文件的task并设置了pom文件的默认存放路径,我们重点关注生成pom文件的task,该task的实现类是GenerateMavenPom,看下它的执行方法。

GenerateMavenPom#doGenerate

  1. @TaskAction
  2. publicvoiddoGenerate(){
  3. MavenPomInternalpomInternal=(MavenPomInternal)getPom();
  4. MavenPomFileGeneratorpomGenerator=newMavenPomFileGenerator(
  5. ...
  6. );
  7. pomGenerator.configureFrom(pomInternal);
  8. for(MavenDependencymavenDependency:pomInternal.getApiDependencyManagement()){
  9. pomGenerator.addApiDependencyManagement(mavenDependency);
  10. }
  11. for(MavenDependencymavenDependency:pomInternal.getRuntimeDependencyManagement()){
  12. pomGenerator.addRuntimeDependencyManagement(mavenDependency);
  13. }
  14. ...
  15. pomGenerator.withXml(pomInternal.getXmlAction());
  16.  
  17. pomGenerator.writeTo(getDestination());
  18. }

@TaskAction注解是标识task被执行时调用的方法,这个方法内容很直白,通过MavenPomFileGenerator从Pom接口读取数据然后生成pom的xml文件。分析到这里,pom文件怎么生成的就不需要往下看了,我们需要关注的是Pom数据从哪里来,还记得我们上面分析的createGeneratePomTask方法吗,大家回头看一看,我就不回头了。

  1. generatePomTask.setPom(publication.getPom());

可以看到,Pom数据是从publication拿的,也就是我们上面说的MavenPublication,MavenPublication的默认实现类是DefaultMavenPublication,我们再看看DefaultMavenPublication的Pom数据是哪里来的。

DefaultMavenPublication

  1. pom=instantiator.newInstance(DefaultMavenPom.class,this,instantiator,objectFactory);

在GenerateMavenPom Task里面拿到的Pom数据其实就是DefaultMavenPom,而DefaultMavenPom的入参是DefaultMavenPublication本身,这里使用了代理模式,外部只需要从MavenPomInternal接口(上面的getPom())获取数据即可,而真正的数据来源则是DefaultMavenPublication本身,我们随便找一个数据获取流程跟踪一下。

  1. //获取所有的api依赖关系(这个api不是我们常用的api依赖,而是包括多个)
  2. DefaultMavenPom#getApiDependencies->
  3.  
  4. //实际获取数据是DefaultMavenPublication类
  5. DefaultMavenPublication#getApiDependencies
  6. @Override
  7. publicSetgetApiDependencies(){
  8. populateFromComponent();
  9. returnapiDependencies;
  10. }

populateFromComponent方法的逻辑就是解析数据,这个方法有点长我就不贴代码了,大家有兴趣的可以自己去看,大致逻辑就是从DefaultMavenPublication.component属性中解析出各种依赖关系以及其他的一些信息,那component是哪儿来的呢?

我们很快能查到是通过DefaultMavenPublication.from方法传入的,经常写插件的同学一定不会陌生,因为我们在构建插件Publication的时候经常会配置这样一段代码。

  1. *publishing{
  2. *publications{
  3. *maven(MavenPublication){
  4. *fromcomponents.java
  5. *}
  6. *}
  7. *}

这里的components.java就是数据来源了,到此pom文件生成和数据来源的分析就基本结束了。

选择合适的component

上面我们分析了pom文件生成的数据来源,但遗憾的是gradle官方目前只提供三种类型的component,分别是components.java、components.web、components.javaPlatform,对应的插件分别是javaPlugin、WarPlugin、JavaPlatFormPlugin。

显然这些都不是我们想要的,难道我们自己再写一个插件来提供android的component吗?Google表示这种小事交给我来就行了,Android Gradle 插件在3.6.0 及更高版本以上开始支持maven-publish插件,根据你依赖插件的类型来生成对应的components,来看下插件类型和components的对应关系。

用模块化来管理你的Android项目

对应关系相当清晰了,当你使用module插件的时候会为你自动构建components.variant和aar,当你使用app插件的时候会为你生成apk文件及components.variant_apk,根据这些信息我们很容易就能写出一个标准的module

Publication配置脚本

  1. publishing{
  2. publications{
  3. libraryA(MavenPublication){
  4. fromcomponents.release
  5. groupId='com.xxx'
  6. artifactId='xxx'
  7. version='1.1.1'
  8. }
  9. }
  10. }

配置完publication再依赖maven-publish插件我们就可以通过publishToMavenLocal愉快的发布aar到本地仓库了,但是一两个module还好,module数量一旦多起来,难道我要一个个去配置吗?这也太难为老夫了吧~

构建蓝图

一个个去配置是不可能的,这辈子都不可能,我们希望能够通过一种极其简洁明了的方式来配置所有的module,并且代码不侵入到module的build script里面去(先来做个梦,画出我们想要的蓝图),比如像下面这样:

  1. moduleSettings{
  2. libraryA(
  3. groupId:'com.default',
  4. artifactId:'libraryA',
  5. version:'1.3',
  6. )
  7. libraryB(
  8. groupId:'com.default',
  9. artifactId:'libraryB',
  10. version:'1.2',
  11. )
  12. ...
  13. }

libraryA、libraryB是module的名称,groupId、artifactId、version不用说了,maven发布三剑客,除了这些必要的参数外,其他的我们统统不想管,我们想只在工程目录下配置这个脚本就能完成所有module的发布配置。

上帝:“嗯,问题不大”

我:“那配置完之后我不可能一个个module去执行任务发布吧,这也太累了,能不能一键发布所有module啊”

上帝:“good idea~”

我:“那。。。发布完之后我怎么依赖aar呢?这么多module我每次切换aar和project依赖那得多累啊,能不能完成自动切换,不侵入到module的build script呢”

上帝:“That's a great idea~”

我:“哈哈,那还不错,满足的从睡梦中笑醒,揭开被子才发现上帝竟是我自己。”

完成蓝图

梦是做完了,但实现还是得努把力,下面我们就来圆梦。

module发布件统一配置

毫无疑问,实现这个功能需要通过插件来处理,先来看看一个module配置publication的必要步骤,说是必要步骤,其实所有步骤也就两步。

  • 依赖maven-publish
  • 配置publication

话不多说,先来定义一个插件。

  1. publicclassModuleManagePluginimplementsPlugin{
  2.  
  3. @Override
  4. publicvoidapply(Projectproject){
  5. for(ProjectsubProject:subProjects){
  6. project.afterEvaluate(p->{
  7. if(p.getPluginManager().hasPlugin("com.android.library")){
  8. p.getPluginManager().apply("maven-publish");
  9. }
  10. });
  11. }
  12. }

在项目工程下build.gradle apply该插件我们就能获取到所有settings脚本里面配置的module project,接着为每一个project都增加对maven-publish插件的引用,第一步就完成了。

再来看第二步,前面我们给出了module publication配置标准模板,除了maven三件套需要用户自己配置外(groupId、artifactId、version),其他的我们都可以通过插件来完成配置,我们可以定义一个root project的extension(ModuleConfig.class)来接收三件套的信息,然后在插件里面完成自动配置,还是和上面的方式一样,extension以subProject名字来命名。

  1. for(ProjectsubProject:subProjects){
  2. subProject.getExtensions().create(subProject.getName(),ModuleConfig.class);
  3. });

然后在每个subProject里面配置publication,代码也很简单。

  1. ModuleConfigmodulePublish=project.getRootProject().getExtensions().getByName(project.getName());
  2.  
  3. PublishingExtensionpublishingExt=project.getExtensions().getByType(PublishingExtension.class);
  4.  
  5. PublicationContainerpublications=publishingExt.getPublications();
  6. if(publications.findByName(PUBLISH_NAME)!=null){
  7. return;
  8. }
  9. publications.create(PUBLISH_NAME,MavenPublication.class,publication->{
  10. SoftwareComponentrelease=project.getComponents().findByName(DEFAULT_COMPONENT);
  11. if(release==null){
  12. System.out.println("can'tfinddefaultcomponent");
  13. return;
  14. }
  15. publication.from(release);
  16. publication.setGroupId(modulePublish.getGroupId());
  17. publication.setArtifactId(modulePublish.getArtifactId());
  18. publication.setVersion(modulePublish.getVersion());
  19. });

做完这些我们基本上就完成了module发布的统一配置,看起来没啥问题,但是有一个体验很不好的地方,那就是extension的命名取的是subProject的名称。

如果settings文件project的module配置被注释掉了,此时将无法获取到正确的subProject名称,配置脚本自然也就会报错,我总不可能再把配置文件对应的module配置也注释掉吧,本来是为了减轻工作量,这下反而又增加了,那有没有办法根据settings配置的module动态激活配置而不需要更改配置脚本呢?

MethodMissing机制

要实现根据settings配置的module动态激活配置,extension肯定是行不通了,因为extension需要提前创建,在groovy语言有一个很好玩的特性,那就是methodMissing,methodMissing允许你调用一个未定义过的方法并通过MethodMixIn接口转发,利用这一特性,我们完全不需要事先创建extension,我们只需要将配置文件转换成实体,然后再根据settings脚本配置的module来决定是否激活module配置,直接上代码。

  1. publicclassDynamicPublishMethodsimplementsMethodAccess{
  2. privateMapmoduleConfigHashMap;,moduleconfig>
  3.  
  4.  
  5. publicDynamicPublishMethods(MapmoduleConfigHashMap){,moduleconfig>
  6. this.moduleConfigHashMap=moduleConfigHashMap;
  7. }
  8.  
  9. @Override
  10. publicbooleanhasMethod(Stringname,Object...arguments){
  11. returntrue;
  12. }
  13.  
  14. @Override
  15. publicDynamicInvokeResulttryInvokeMethod(Stringname,Object...arguments){
  16. for(Objectobject:arguments){
  17. if(objectinstanceofMap){
  18. @SuppressWarnings("unchecked")
  19. Mapmap=(Map)object;,object>,object>
  20. ModuleConfigmoduleConfig=newModuleConfig();
  21. attemptPackageModuleConfig(moduleConfig,name,map);
  22. }
  23. }
  24. returnDynamicInvokeResult.found(moduleConfigHashMap);
  25. }
  26.  
  27. privatevoidattemptPackageModuleConfig(@NotNullModuleConfigmoduleConfig,
  28. @NotNullStringname,@NotNullMapparamsMap){,object>
  29. if(paramsMap.isEmpty()){
  30. return;
  31. }
  32. try{
  33. ClassmoduleConfigClass=moduleConfig.getClass();
  34. Set>entries=paramsMap.entrySet();
  35. for(Map.Entryentry:entries){,object>
  36. Fieldfield=moduleConfigClass.getField(entry.getKey());
  37. field.setAccessible(true);
  38. field.set(moduleConfig,entry.getValue());
  39. field.setAccessible(false);
  40. }
  41. moduleConfigHashMap.put(name,moduleConfig);
  42. }catch(Exceptione){
  43. e.printStackTrace();
  44. System.out.println(e.getMessage());
  45. }
  46. }
  47. }

在上面的代码里面我们定义一个MethodAccess的类来接收未定义方法的跳转,获取所有配置信息然后通过反射将这些信息转换成ModuleConfig实体,然后再在subProject里面完成配置。

  1. for(ProjectsubProject:subProjects){
  2. ...
  3. ModuleConfigmoduleConfig=moduleSettings.getModuleConfigHashMap().get(roject.getName())
  4. });
  5. ...

取到配置实体之后的步骤就和上面定义extension配置publication一样了,这里就不贴代码了,到这里我们就完成了module发布件统一配置。

module依赖方式自动切换

在我们项目中module大都是以这种方式来引用。

  1. implementationproject(':path')

如果想要改成aar引用, 不可避免的会改动build script,我们希望能在插件内部解决这件事,第一反应当然是手动删除替换依赖规则,但遗憾的是,直接对依赖进行删除的话则会直接抛出异常,官方不允许我们对已经添加的依赖直接做删除操作。

  1. @Override
  2. publicbooleanremove(Objecto){
  3. thrownewUnsupportedOperationException();
  4. }

当然这样做本身其实是有风险的,容易出现其他不可预期的问题,庆幸的是,善解人意的gradle为我们提供了官方解决方案,那就是ResolutionStrategy,通过配置ResolutionStrategy,我们可以实现根据不同的策略在执行阶段来调整依赖。我们在之前配置maven三件套的实体(ModuleConfig)里再定义一个字段 useByAar,通过这个字段来控制是否切换成aar依赖,完整的配置如下所示:

  1. moduleSettings{
  2. libraryA(
  3. useByAar:true,
  4. groupId:'com.default',
  5. artifactId:'libraryA',
  6. version:'1.3',
  7. )
  8. libraryB(
  9. useByAar:true,
  10. groupId:'com.default',
  11. artifactId:'libraryB',
  12. version:'1.2',
  13. )
  14. ...
  15.  
  16. }

然后在插件内部读取该字段来实现依赖的替换。

  1. privatevoidconfigResolutionStrategy(Projectproject,ModuleSettingsmoduleSettings){
  2. System.out.println("configResolutionStrategy");
  3. Mapresolutions=getResolutions(project,moduleSettings);,string>
  4. if(resolutions.isEmpty()){
  5. return;
  6. }
  7. project.getConfigurations().all(configuration->{
  8. Set>entries=resolutions.entrySet();
  9. for(Map.Entryentry:entries){,string>
  10. configuration.resolutionStrategy(
  11. resolutionStrategy->resolutionStrategy.dependencySubstitution(
  12. dependencySubstitutions->{
  13. DependencySubstitutions.Substitutionsubstitute=
  14. dependencySubstitutions.substitute(
  15. dependencySubstitutions.project(entry.getKey()));
  16. substitute.with(
  17. dependencySubstitutions.module(entry.getValue()));
  18. }));
  19. }
  20. });
  21. }

到这里我们已经实现了module aar和project依赖的动态切换,只需要在module配置文件里将对应module的useByAar设置为true,项目中所有以project方式引用该module的依赖全部会自动切换成aar依赖引用,而不需要改动引用方build script的任何代码。

一键发布所有module至本地仓库

这个就很简单了,我们只需要定义一个oneKeyPulish的task,然后重新定义该task和当前所有已配置module的publishToMavenLocal的依赖关系即可,需要注意的是要提前依赖assembleRelease来构建components,由于篇幅原因这里就不展开说了,大家有兴趣的可以直接看下方链接源码,嗯、、、、烂尾王。

总结

通过一番探索,我们解决了module管理中几个比较核心的痛点。

  • 自动构建发布脚本(除了用户必填的maven三件套)
  • 动态切换依赖
  • 一键发布所有module

以下是插件完整代码,可以说基本实现了一个非常实用的module管理插件,并在该基础插件上扩展了一些功能,但需求是因人而异的,如果大家有其他想法和需求,也可以提issue给我。