Android组件化Gradle插件Calces源码解析

2024-04-09 06:38

本文主要是介绍Android组件化Gradle插件Calces源码解析,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

随着很多公司的业务越来越多样化和复杂化,组件化开发也越来越流行,为我们调试代码和多人协助开发带来了巨大的好处,我们肯定会遇到下面几个痛点:

  • 组件是否单独运行
 if (isDebug) {apply plugin: 'com.android.application'} else {apply plugin: 'com.android.library'}

通过gradle.properties配置isDebug变量来处理组件作为app还是lib的存在。

  • 组件与组件之间的Manifest合并问题
sourceSets {main {if (rootProject.ext.isBuildApp) {manifest.srcFile 'src/main/debug/AndroidManifest.xml'} else {//移除debug资源manifest.srcFile 'src/main/release/AndroidManifest.xml'java {exclude 'debug/**'}}}}

通过config.gradle中的变量判断该moudle到底用哪个AndroidManifest.xml文件,这两个的区别在于进入的时候是否需要配置activity启动intent-filter。

其实上面的这些操作我们都可以通过Gradle插件来帮我们管理组件,已经有人帮我们实现了这个想法Gradle自动实现Android组件化模块构建,为了深入理解构建方式本文分析下该开源项目calces-gradle-plugin,阅读本文前可以先阅读关于Groovy的语法的文章Gradle自定义Plugin(上)。

我们先看下项目结构目录:
在这里插入图片描述
1处是定义的配置的moudle信息模型层,2是处理组件与组件之间的Manifest合并问题的,3是针对app壳工程编写的Plugin插件 ,4是针对非壳工程moudle编写的Plugin插件 AppConfigPlugin和ModulesConfigPlugin编写完后需要在下面的目录对应配置下。我们先看AppConfigExt类,这个一个gradle配置信息入口的类

class AppConfigExt {boolean isDebug = false //组件单独开启开关//容纳object的容器,它的特点是它的内部使用SortedSet实现的必须有一个public的构造函数,// 接受string作为一个参数,必须有一个叫做name 的propertyNamedDomainObjectContainer<AppExt> apps //宿主载体NamedDomainObjectContainer<LibraryExt> modules  //组件AppConfigExt(Project project){apps = project.container(AppExt) //创建object的容器对象modules = project.container(LibraryExt)}def isDebug(boolean isDebug){this.isDebug = isDebug}def apps(Closure closure){apps.configure(closure)}def modules(Closure closure){modules.configure(closure)}@OverrideString toString() {return "isDebug: $debugEnable\n" +"apps: ${apps.isEmpty()? "is empty" : "$apps"}"+"modules: ${modules.isEmpty()? "is empty" : "$modules"}"}
}

AppExt 这个类是宿主App

class AppExt extends ModulesExt{String dependMethod = "implementation" //默认依赖的方式List<String> modules = new ArrayList<>() //依赖组件的名称集合AppExt(String name) {super(name)}
......

各个组件的属性配置

class LibraryExt extends ModulesExt {boolean isRunAlone = false //是否独立运行String runAloneSuper //该moudle所依赖的子模块LibraryExt(String name) {super(name)}........

宿主App和各个组件的基类

class ModulesExt {String name  //组件名称String applicationId  //组件applicationIdString mainActivity   //该组件启动的activityModulesExt(String name){this.name = name //如果有父节点必须写构造方法}
}

先用AppConfigPlugin处理整个项目项目build.gradle的配置信息

public class AppConfigPlugin  implements Plugin<Project> {private static final String EXTENSION_NAME = "appConfig"@Overridevoid apply(Project project) {AppConfigExt appConfigExt = new AppConfigExt(project)// project.extensions,本扩展类其实就是只用于set、get的JavaBean类project.extensions.add(EXTENSION_NAME, appConfigExt)configApp(project)}void configApp(Project project) {List<String> moduleList = new ArrayList<>()NamedDomainObjectContainer<AppExt> appListAppConfigExt appConfigExt//当前project配置状态进行回调afterEvaluate(project开始配置前调用)//和beforeEvaluate,afterEvaluate(project配置完成后回调)project.afterEvaluate {//得到配置的信息beanappConfigExt = project.extensions.getByName(EXTENSION_NAME) as AppConfigExtappList = appConfigExt.appscheckRepeat(appConfigExt)checkModules(appConfigExt,moduleList)}initChildModules(moduleList, project)println("project child modules: $moduleList")}//运行后获取项目存在的所有组件void initChildModules(List<String> moduleList ,Project project){if (project.childProjects.isEmpty()){moduleList.add(project.toString().replace("project ","").replace('\'',''))return}//运行时遍历获取所有存在的组件(App壳工程和寄生组件)project.childProjects.entrySet().forEach{initChildModules(moduleList, it.value)}}//检查配置的模块是否重复static void checkRepeat(AppConfigExt appConfigExt){//取出App壳工程组件名称列表  以name字段为分组条件 //很重要的一点是NamedDomainObjectContainer容器节点的名称是唯一的,是按照节点的名称排序的,//如果有相同的节点名称 后面会覆盖前面的 (这个需要自行实验才能理解)//并且必须有一个public的构造函数,接受string作为一个参数,必须有一个叫做name 的property//这个property默认就是节点的名称 需要和项目路径对应,如果不填写默认为该配置的名字 //就是moudle节点的名称(如配置名为app的话,name则为:app)//倒入规则和setting.gradle中的include规则保持一致Map<String,List<AppExt>> appGroupMap =appConfigExt.apps.groupBy{ it.name.startsWith(':') ? it.name : new String(":" + it.name)}//k指的是name v指List<AppExt>   如果多个moudle节点的名称不同 但是里面的内容一样//当v.size() > 1的时候说明apps组件名称重复appGroupMap.forEach{k,v ->if (v.size() > 1){throw new IllegalArgumentException("app is repeat. app name: [$k]")}}//取出寄生组件名称列表Map<String,List<LibraryExt>> moduleGroupMap =appConfigExt.modules.groupBy{ it.name.startsWith(':') ? it.name : new String(":" + it.name)}//k指的是name v指List<LibraryExt>  如果多个moudle节点的名称不同 但是里面的内容一样//这个时候就表示v.size() > 1说明apps宿主名称名称重复moduleGroupMap.forEach{k,v ->if (v.size() > 1){throw new IllegalArgumentException("modules is repeat. modules name: [$k]")}}}//检查配置的模块名称是否合理正确static void checkModules(AppConfigExt appConfigExt,List<String> projectModules){Set<String> configSet = new HashSet<>() //配置的所有组件集合Set<String> modulesSet = new HashSet<>() //真实获取的组件集合if (projectModules != null){modulesSet.addAll(projectModules)}List<String> notFoundList = new ArrayList<>()List<String> appNameList = appConfigExt.apps.stream().map{it.name.startsWith(':') ? it.name : new String(":" + it.name)}.collect()List<String> moduleNameList =appConfigExt.modules.stream().map{String name = it.name.startsWith(':') ? it.name : new String(":" + it.name)//App壳工程的名字不能出现在其他组件当中if (appNameList.contains(name)){throw new IllegalArgumentException("$it.name already configured " +"as an application, please check appConfig")}name}.collect()println "moduleNameList = $moduleNameList"configSet.addAll(appNameList)configSet.addAll(moduleNameList)configSet.forEach{if(!modulesSet.contains(it)){notFoundList.add(it)}}//寄生组件配置的组件名称不存在if (notFoundList.size() > 0){throw  new IllegalArgumentException("not fount modules = " + notFoundList)}//App壳工程依赖的组件不存在appConfigExt.apps.stream().forEach{ app ->app.modules.stream().forEach{if (! configSet.contains(it)){throw  new IllegalArgumentException("appConfig error , can not find $app.name modules $it by project" )}}}println("modules: " + configSet)}}

然后处理每个moudle的build.gradle的配置信息的配置信息

public class ModulesConfigPlugin implements Plugin<Project> {private static final String PARENT_EXTENSION_NAME = "appConfig"@Overridevoid apply(Project project) {AppConfigExt appConfigExt = getAppConfigExtension(project)configModules(project, appConfigExt)}static void configModules(Project project, AppConfigExt appConfigExt){if (appConfigExt == null){throw new NullPointerException("can not find appConfig")}List<AppExt> filterList = appConfigExt.apps.stream().filter{ (it.name.startsWith(':') ? it.name : new String(":" + it.name)).endsWith(project.name) }.skip(0).collect()  //Java8 Stream 语法if (filterList != null && filterList.size() > 0){ //说明当前是已App壳工程编译AppExt appExt = filterList.get(0)AppPlugin appPlugin = project.plugins.apply(AppPlugin)//App壳工程的build.gradle文件设置ApplicationIdappPlugin.extension.defaultConfig.setApplicationId(appExt.applicationId)//检查配置app壳工程的Manifest文件信息new AppManifestStrategy(project).resetManifest(appExt)  //检查配置app壳工程的依赖问题dependModules(project, appExt, appConfigExt)}else {//检查配置子moudle独立运行modulesRunAlone(project,appConfigExt.modules, appConfigExt.isDebug)}}//app壳工程依赖static void dependModules(Project project, AppExt appExt, AppConfigExt appConfigExt){//遍历appConfigExt.modules数据和appExt.modules数据对比得到交集然后以map形式返回Map<String,LibraryExt> moduleExtMap = appConfigExt.modules.stream().filter{modules ->String modulesName = appExt.modules.stream().find{ it.contains(modules.name) }modulesName != null && !modulesName.isEmpty()}.collect(Collectors.toMap({ it.name},{ it -> it}))if (appExt.modules != null && appExt.modules.size() > 0){List<String> modulesList = appExt.modules.stream().filter{ //遍历数据并检查其中的元素时使用 类似if//是否开启debug模式 debug为true的时候 moudle的isRunAlone必须要是true才能被依赖appConfigExt.isDebug ? (moduleExtMap != null && !moduleExtMap[it].isRunAlone) : true }.map{ //map生成的是个一对一映射,for的作用//添加依赖 implementation XXXX模块project.dependencies.add(appExt.dependMethod, project.project(it))it}.collect()println("build app: [$appExt.name] , depend modules: $modulesList")}}//获取父节点的配置信息AppConfigExt getAppConfigExtension(Project project){try{//项目根节点下的配置信息return project.parent.extensions.getByName(PARENT_EXTENSION_NAME) as AppConfigExt }catch (UnknownDomainObjectException ignored){if (project.parent != null){getAppConfigExtension(project.parent)}else {throw new UnknownDomainObjectException(ignored as String)}}}//子moudle独立运行private static void modulesRunAlone(Project project, NamedDomainObjectContainer<LibraryExt> modules, boolean isDebug){List<LibraryExt> filterList = modules.stream().filter{ it.name.endsWith(project.name) }.skip(0).collect()if (filterList != null && filterList.size() > 0){//当前子moudle编译LibraryExt moduleExt = filterList.get(0)if (isDebug && moduleExt.isRunAlone){//开启debug模式并且isRunAlone为trueAppPlugin appPlugin = project.plugins.apply(AppPlugin)appPlugin.extension.defaultConfig.setApplicationId(moduleExt.applicationId) //设置App工程ApplicationIdif (moduleExt.runAloneSuper != null && !moduleExt.runAloneSuper.isEmpty()){ //project.dependencies.add("implementation", project.project(moduleExt.runAloneSuper))println("build run alone modules: [$moduleExt.name], runSuper = $moduleExt.runAloneSuper")}else{println("build run alone modules: [$moduleExt.name]")}if (moduleExt.mainActivity != null && !moduleExt.mainActivity.isEmpty()){//检查配置app壳工程的Manifest文件信息new AppManifestStrategy(project).resetManifest(moduleExt)}}else{project.plugins.apply(LibraryPlugin)//设置App依赖工程//检查配置依赖工程的Manifest文件信息new LibraryManifestStrategy(project).resetManifest(moduleExt)}}}}

处理每个moudle的时候需要处理每个moudle的AndroidManifest文件信息

abstract class ManifestStrategy {protected String pathprotected GPathResult manifestboolean edit = false //AndroidManifest配置文件是否修改过ManifestStrategy(Project project) {path = "${project.getBuildFile().getParent()}/src/main/AndroidManifest.xml"File manifestFile = new File(path) //找到moudle下的AndroidManifest.xml文件if (!manifestFile.getParentFile().exists() && !manifestFile.getParentFile().mkdirs()) {println "Unable to find AndroidManifest and create fail, please manually create"}manifest = new XmlSlurper().parse(manifestFile)//xml解析}//子类需要继承 设置启动Activity的IntentFilter属性abstract void setMainIntentFilter(def activity, boolean isFindMain)void resetManifest(ModulesExt moduleExt) {if (manifest.@package != moduleExt.applicationId && moduleExt.applicationId != null &&!moduleExt.applicationId.isEmpty()) {//重新设置manifest的包名(如果设置了 没用默认的applicationId)manifest.@package = moduleExt.applicationId edit = true}boolean isFindMain = false //是否找到了启动activityif (moduleExt.mainActivity != null && !moduleExt.mainActivity.isEmpty()) {manifest.application.activity.each { activity ->if (activity.@'android:name' == moduleExt.mainActivity) {def filter = activity.'intent-filter'.find {it.action.@'android:name' == "android.intent.action.MAIN"}isFindMain = true//如果没有设置IntentFilter条件 代码帮助其设置setMainIntentFilter(activity, filter != null && filter.size() > 0)}}}manifest.application.activity.each { activity ->def filter = activity.'intent-filter'.find {it.action.@'android:name' == "android.intent.action.MAIN"}if (filter != null&& moduleExt.mainActivity != null&& !moduleExt.mainActivity.isEmpty()&& activity.@'android:name' != moduleExt.mainActivity) {//如果设置了IntentFilter条件 但是启动activity和配置的不一样则清空IntentFilter条件filter.replaceNode {} edit = true}}//如果在AndroidManifest配置文件当中没有找到启动activity那么就手动设置if (!isFindMain) {addMainActivity(manifest.application, moduleExt)}if (edit) {buildModulesManifest(manifest)}}//代码添加启动activityvoid addMainActivity(def application, ModulesExt modulesExt) {if (modulesExt.mainActivity != null && !modulesExt.mainActivity.isEmpty()) {application.appendNode {activity('android:name': modulesExt.mainActivity) {'intent-filter' {action('android:name': "android.intent.action.MAIN")category('android:name': "android.intent.category.LAUNCHER")}}}edit = true}}//动态修改AndroidManifest节点下的配置问题void buildModulesManifest(def manifest) {def fileText = new File(path)StreamingMarkupBuilder outputBuilder = new StreamingMarkupBuilder() //创建XMLdef root = outputBuilder.bind {mkp.xmlDeclaration()mkp.declareNamespace('android': 'http://schemas.android.com/apk/res/android')mkp.yield manifest}String result = XmlUtil.serialize(root)fileText.text = result}
}//App壳工程的AndroidManifest配置文件  setMainIntentFilter中添加intent-filterclass AppManifestStrategy extends ManifestStrategy {AppManifestStrategy(Project project) {super(project)}@Overridevoid setMainIntentFilter(def activity, boolean isFindMain) {if (!isFindMain) {activity.appendNode {'intent-filter' {action('android:name': "android.intent.action.MAIN")category('android:name': "android.intent.category.LAUNCHER")}}edit = true}}}//依赖moudle工程的AndroidManifest配置文件  setMainIntentFilter中清除intent-filterclass LibraryManifestStrategy extends ManifestStrategy {LibraryManifestStrategy(Project project) {super(project)}@Overridevoid setMainIntentFilter(def activity, boolean isFindMain) {if (isFindMain) {println "build lib"activity.'intent-filter'.each {if (it.action.@'android:name' == "android.intent.action.MAIN") {it.replaceNode {}edit = true}}}}}

以上的代码注释信息便是这个项目的所有代码模块解析了,只有去阅读原作者相关文章和demo跑跑才能理解透彻。至于组件化全局的Application处理和组件通信这些方面的问题本文就不做深入探讨了。具体用法请查看相关说明calces-gradle-plugin。

这篇关于Android组件化Gradle插件Calces源码解析的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/887434

相关文章

深度解析Spring Security 中的 SecurityFilterChain核心功能

《深度解析SpringSecurity中的SecurityFilterChain核心功能》SecurityFilterChain通过组件化配置、类型安全路径匹配、多链协同三大特性,重构了Spri... 目录Spring Security 中的SecurityFilterChain深度解析一、Security

全面解析Golang 中的 Gorilla CORS 中间件正确用法

《全面解析Golang中的GorillaCORS中间件正确用法》Golang中使用gorilla/mux路由器配合rs/cors中间件库可以优雅地解决这个问题,然而,很多人刚开始使用时会遇到配... 目录如何让 golang 中的 Gorilla CORS 中间件正确工作一、基础依赖二、错误用法(很多人一开

Android Paging 分页加载库使用实践

《AndroidPaging分页加载库使用实践》AndroidPaging库是Jetpack组件的一部分,它提供了一套完整的解决方案来处理大型数据集的分页加载,本文将深入探讨Paging库... 目录前言一、Paging 库概述二、Paging 3 核心组件1. PagingSource2. Pager3.

Mysql中设计数据表的过程解析

《Mysql中设计数据表的过程解析》数据库约束通过NOTNULL、UNIQUE、DEFAULT、主键和外键等规则保障数据完整性,自动校验数据,减少人工错误,提升数据一致性和业务逻辑严谨性,本文介绍My... 目录1.引言2.NOT NULL——制定某列不可以存储NULL值2.UNIQUE——保证某一列的每一

深度解析Nginx日志分析与499状态码问题解决

《深度解析Nginx日志分析与499状态码问题解决》在Web服务器运维和性能优化过程中,Nginx日志是排查问题的重要依据,本文将围绕Nginx日志分析、499状态码的成因、排查方法及解决方案展开讨论... 目录前言1. Nginx日志基础1.1 Nginx日志存放位置1.2 Nginx日志格式2. 499

MySQL CTE (Common Table Expressions)示例全解析

《MySQLCTE(CommonTableExpressions)示例全解析》MySQL8.0引入CTE,支持递归查询,可创建临时命名结果集,提升复杂查询的可读性与维护性,适用于层次结构数据处... 目录基本语法CTE 主要特点非递归 CTE简单 CTE 示例多 CTE 示例递归 CTE基本递归 CTE 结

Spring Boot 3.x 中 WebClient 示例详解析

《SpringBoot3.x中WebClient示例详解析》SpringBoot3.x中WebClient是响应式HTTP客户端,替代RestTemplate,支持异步非阻塞请求,涵盖GET... 目录Spring Boot 3.x 中 WebClient 全面详解及示例1. WebClient 简介2.

在MySQL中实现冷热数据分离的方法及使用场景底层原理解析

《在MySQL中实现冷热数据分离的方法及使用场景底层原理解析》MySQL冷热数据分离通过分表/分区策略、数据归档和索引优化,将频繁访问的热数据与冷数据分开存储,提升查询效率并降低存储成本,适用于高并发... 目录实现冷热数据分离1. 分表策略2. 使用分区表3. 数据归档与迁移在mysql中实现冷热数据分

C#解析JSON数据全攻略指南

《C#解析JSON数据全攻略指南》这篇文章主要为大家详细介绍了使用C#解析JSON数据全攻略指南,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下... 目录一、为什么jsON是C#开发必修课?二、四步搞定网络JSON数据1. 获取数据 - HttpClient最佳实践2. 动态解析 - 快速

Spring Boot3.0新特性全面解析与应用实战

《SpringBoot3.0新特性全面解析与应用实战》SpringBoot3.0作为Spring生态系统的一个重要里程碑,带来了众多令人兴奋的新特性和改进,本文将深入解析SpringBoot3.0的... 目录核心变化概览Java版本要求提升迁移至Jakarta EE重要新特性详解1. Native Ima