在模块化和组件化横行的今天,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的依赖关系即可,
发表评论 取消回复