本网站(662p.com)打包出售,且带程序代码数据,662p.com域名,程序内核采用TP框架开发,需要联系扣扣:2360248666 /wx:lianweikj
精品域名一口价出售:1y1m.com(350元) ,6b7b.com(400元) , 5k5j.com(380元) , yayj.com(1800元), jiongzhun.com(1000元) , niuzen.com(2800元) , zennei.com(5000元)
需要联系扣扣:2360248666 /wx:lianweikj
用模块化来管理你的Android项目
chenguangming9 · 483浏览 · 发布于2021-09-18 +关注

在模块化和组件化横行的今天,module的数量越来越多,module数量增加的同时也给项目编译带来了极大的负担,相信大家都经历过一次冷编译耗时五六分钟,甚至七八分钟的时候,编译优化显然是势在必行。

前言

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

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。

apply plugin: 'maven-publish'

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

    MavenPublishPlugin

    @Override 
    public void apply(final Project project) { 
        project.getPluginManager().apply(PublishingPlugin.class); 
        ... 
        project.getExtensions().configure(PublishingExtension.class, extension -> { 
            ... 
            realizePublishingTasksLater(project, extension); 
        }); 
    }

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

      MavenPublishPlugin#realizePublishingTasksLater

      private void realizePublishingTasksLater(final Project project, final PublishingExtension extension) { 
          final NamedDomainObjectSet<MavenPublicationInternal> mavenPublications = extension.getPublications().withType(MavenPublicationInternal.class); 
          ... 
          mavenPublications.all(publication -> { 
              ... 
              this.createGeneratePomTask(tasks, publication, buildDirectory, project); 
              createLocalInstallTask(tasks, publishLocalLifecycleTask, publication); 
              ... 
          }); 
      }

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

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

        MavenPublishPlugin#createGeneratePomTask

        private void createGeneratePomTask(TaskContainer tasks, final MavenPublicationInternal publication, final DirectoryProperty buildDir, final Project project) { 
            final String publicationName = publication.getName(); 
            String descriptorTaskName = "generatePomFileFor" + capitalize(publicationName) + "Publication"; 
            TaskProvider<GenerateMavenPom> generatorTask = tasks.register(descriptorTaskName, GenerateMavenPom.class, generatePomTask -> { 
                ... 
                generatePomTask.setPom(publication.getPom()); 
                if (generatePomTask.getDestination() == null) { 
                    generatePomTask.setDestination(buildDir.file("publications/" + publication.getName() + "/pom-default.xml")); 
                } 
            ... 
            publication.setPomGenerator(generatorTask); 
        }

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

          GenerateMavenPom#doGenerate

          @TaskAction 
          public void doGenerate() { 
              MavenPomInternal pomInternal = (MavenPomInternal) getPom(); 
              MavenPomFileGenerator pomGenerator = new MavenPomFileGenerator( 
                     ... 
                     ); 
              pomGenerator.configureFrom(pomInternal); 
              for (MavenDependency mavenDependency : pomInternal.getApiDependencyManagement()) { 
                  pomGenerator.addApiDependencyManagement(mavenDependency); 
              } 
              for (MavenDependency mavenDependency : pomInternal.getRuntimeDependencyManagement()) { 
                  pomGenerator.addRuntimeDependencyManagement(mavenDependency); 
              } 
              ... 
              pomGenerator.withXml(pomInternal.getXmlAction()); 
          
              pomGenerator.writeTo(getDestination()); 
          }

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

            generatePomTask.setPom(publication.getPom());

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

              DefaultMavenPublication

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

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

                //获取所有的api依赖关系(这个api不是我们常用的api依赖,而是包括多个) 
                DefaultMavenPom#getApiDependencies -> 
                
                //实际获取数据是DefaultMavenPublication类 
                DefaultMavenPublication#getApiDependencies 
                @Override 
                public Set<MavenDependencyInternal> getApiDependencies() { 
                    populateFromComponent(); 
                    return apiDependencies; 
                }

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

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

                  * publishing { 
                  *   publications { 
                  *     maven(MavenPublication) { 
                  *       from components.java 
                  *     } 
                  *   } 
                  * }

                    这里的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的对应关系。

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

                    Publication配置脚本

                    publishing { 
                        publications { 
                            libraryA(MavenPublication) { 
                                from components.release 
                                groupId = 'com.xxx' 
                                artifactId = 'xxx' 
                                version = '1.1.1' 
                            } 
                        } 
                    }

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

                      构建蓝图

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

                      moduleSettings { 
                          libraryA( 
                                  groupId: 'com.default', 
                                  artifactId: 'libraryA', 
                                  version: '1.3', 
                          ) 
                          libraryB( 
                                  groupId: 'com.default', 
                                  artifactId: 'libraryB', 
                                  version: '1.2', 
                          ) 
                          ... 
                      }

                        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

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

                        public class ModuleManagePlugin implements Plugin<Project> { 
                        
                            @Override 
                            public void apply(Project project) { 
                                for (Project subProject : subProjects) { 
                                    project.afterEvaluate(p -> { 
                                        if (p.getPluginManager().hasPlugin("com.android.library")){ 
                                        p.getPluginManager().apply("maven-publish"); 
                                    } 
                                }); 
                            } 
                        }

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

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

                          for (Project subProject : subProjects) { 
                                     subProject.getExtensions().create(subProject.getName(), ModuleConfig.class); 
                                 });

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

                              ModuleConfig modulePublish = project.getRootProject().getExtensions().getByName(project.getName()); 
                            
                            PublishingExtension publishingExt = project.getExtensions().getByType(PublishingExtension.class); 
                            
                            PublicationContainer publications = publishingExt.getPublications(); 
                            if (publications.findByName(PUBLISH_NAME) != null) { 
                                return; 
                            } 
                            publications.create(PUBLISH_NAME, MavenPublication.class, publication -> { 
                                SoftwareComponent release = project.getComponents().findByName(DEFAULT_COMPONENT); 
                                if (release == null) { 
                                    System.out.println("can't find default component"); 
                                    return; 
                                } 
                                publication.from(release); 
                                publication.setGroupId(modulePublish.getGroupId()); 
                                publication.setArtifactId(modulePublish.getArtifactId()); 
                                publication.setVersion(modulePublish.getVersion()); 
                            });

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

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

                              MethodMissing机制

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

                              public class DynamicPublishMethods implements MethodAccess { 
                                  private Map<String, ModuleConfig> moduleConfigHashMap; 
                              
                              
                                  public DynamicPublishMethods(Map<String, ModuleConfig> moduleConfigHashMap) { 
                                      this.moduleConfigHashMap = moduleConfigHashMap; 
                                  } 
                              
                                  @Override 
                                  public boolean hasMethod(String name, Object... arguments) { 
                                      return true; 
                                  } 
                              
                                  @Override 
                                  public DynamicInvokeResult tryInvokeMethod(String name, Object... arguments) { 
                                      for (Object object : arguments) { 
                                          if (object instanceof Map) { 
                                              @SuppressWarnings("unchecked") 
                                              Map<String, Object> map = (Map<String, Object>) object; 
                                              ModuleConfig moduleConfig = new ModuleConfig(); 
                                              attemptPackageModuleConfig(moduleConfig, name, map); 
                                          } 
                                      } 
                                      return DynamicInvokeResult.found(moduleConfigHashMap); 
                                  } 
                              
                                  private void attemptPackageModuleConfig(@NotNull ModuleConfig moduleConfig, 
                                          @NotNull String name, @NotNull Map<String, Object> paramsMap) { 
                                      if (paramsMap.isEmpty()) { 
                                          return; 
                                      } 
                                      try { 
                                          Class<? extends ModuleConfig> moduleConfigClass = moduleConfig.getClass(); 
                                          Set<Map.Entry<String, Object>> entries = paramsMap.entrySet(); 
                                          for (Map.Entry<String, Object> entry : entries) { 
                                              Field field = moduleConfigClass.getField(entry.getKey()); 
                                              field.setAccessible(true); 
                                              field.set(moduleConfig, entry.getValue()); 
                                              field.setAccessible(false); 
                                          } 
                                          moduleConfigHashMap.put(name, moduleConfig); 
                                      } catch (Exception e) { 
                                          e.printStackTrace(); 
                                          System.out.println(e.getMessage()); 
                                      } 
                                  } 
                              }

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

                                for (Project subProject : subProjects) {  
                                    ... 
                                    ModuleConfig moduleConfig = moduleSettings.getModuleConfigHashMap().get(roject.getName()) 
                                    }); 
                                    ...

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

                                  module依赖方式自动切换

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

                                  implementation project(':path')

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

                                    @Override 
                                    public boolean remove(Object o) { 
                                        throw new UnsupportedOperationException(); 
                                    }

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

                                      moduleSettings { 
                                          libraryA( 
                                                  useByAar: true, 
                                                  groupId: 'com.default', 
                                                  artifactId: 'libraryA', 
                                                  version: '1.3', 
                                          ) 
                                          libraryB( 
                                                  useByAar: true, 
                                                  groupId: 'com.default', 
                                                  artifactId: 'libraryB', 
                                                  version: '1.2', 
                                          ) 
                                          ... 
                                      
                                      }

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

                                        private void configResolutionStrategy(Project project, ModuleSettings moduleSettings) { 
                                            System.out.println("configResolutionStrategy"); 
                                            Map<String, String> resolutions = getResolutions(project, moduleSettings); 
                                            if (resolutions.isEmpty()) { 
                                                return; 
                                            } 
                                            project.getConfigurations().all(configuration -> { 
                                                Set<Map.Entry<String, String>> entries = resolutions.entrySet(); 
                                                for (Map.Entry<String, String> entry : entries) { 
                                                    configuration.resolutionStrategy( 
                                                            resolutionStrategy -> resolutionStrategy.dependencySubstitution( 
                                                                    dependencySubstitutions -> { 
                                                                        DependencySubstitutions.Substitution substitute = 
                                                                                dependencySubstitutions.substitute( 
                                                                                        dependencySubstitutions.project(entry.getKey())); 
                                                                        substitute.with( 
                                                                                dependencySubstitutions.module(entry.getValue())); 
                                                                    })); 
                                                } 
                                            }); 
                                        }

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

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

                                          这个就很简单了,我们只需要定义一个oneKeyPulish的task,然后重新定义该task和当前所有已配置module的publishToMavenLocal的依赖关系即可,

                                          相关推荐

                                          android下vulkan与opengles纹理互通

                                          talkchan · 1174浏览 · 2020-11-23 10:37:39
                                          Android 使用RecyclerView实现轮播图

                                          奔跑的男人 · 2173浏览 · 2019-05-09 17:11:13
                                          微软发布新命令行工具 Windows Terminal

                                          吴振华 · 867浏览 · 2019-05-09 17:15:04
                                          Facebook 停止屏蔽部分区块链广告

                                          · 753浏览 · 2019-05-09 17:20:08
                                          加载中

                                          0评论

                                          评论
                                          分类专栏
                                          小鸟云服务器
                                          扫码进入手机网页