@TOC
前言
提示:这里需要提前对Android-模块化-基本知识了解
本文主要分享个人在项目中实现Android模块化中的gradle统一配置、nexus、maven-publish、动态依赖、模块通信等思路
一、gradle统一配置
1. 多模块项目的构建
settings.gradle
是根模块项目以及模块描述文件,include '模块路径(分隔符是冒号)'
或如下别名引入
include 'VScreen_App' //不建议有冒号
project(":VScreen_App").projectDir = file("VScreen") //指定真实模块路径
include 子模块技巧
,如下
def sub_father = ':' //子项目父工程名, 更为了能Find Usages
//基础组件库
sub_father = ':--base_modules'
include '',
"$sub_father:lib_arouter", //阿里路由
"$sub_father:lib_baseAndroid", //安卓基础api
"$sub_father:lib_comm_ui", // ui组件库
"$sub_father:lib_component", // 常用组件库
"$sub_father:lib_export_table_java", // export_table组件库
"$sub_father:lib_glide", // img_glide
"$sub_father:lib_okhttp", // net_okhttp
//"$sub_father:lib_zxing",
''
业务模块过多,include 业务模块技巧
,约定在指定目录如下
//业务模块
def business_modules_name = new ArrayList<String>()
def business_modules_symbol = new ArrayList<String>()
for (f in file("business_modules").listFiles()) {
if (f.isDirectory() && new File(f, "build.gradle").exists()) {
def name = ":business_modules:${f.name}"
business_modules_name.add("${name}")
business_modules_symbol.add("'${name}'")
}
}
//业务模块动态添加 (考虑的业务模块有很多)
def business_modules_dynamically_add = true
if (business_modules_dynamically_add) {
//动态添加目录底下所有
business_modules_name.forEach {
include(it)
}
} else {
//手动按需添加
def include_business_modules_str = "include '',\n"
business_modules_symbol.forEach {
include_business_modules_str += "$it,\n"
}
include_business_modules_str += "''"
println "输出include脚本, 按需开启\n" + include_business_modules_str + "\n输出include脚本, 按需开启"
//Gradle窗口: 输出include脚本, 按需开启
//include '',
// ':business_modules:lib_attendance',
// ':business_modules:lib_consume',
// ':business_modules:lib_family_phone',
// ''
}
println "> Configure 业务模块 : ${business_modules_symbol}"
老项目工程庞大臃肿,一时无法分离。
一般我们会把这个app工程转化为核心库(下沉给其它工程依赖使用),添加新的壳工程。我们能不能做到不需要空壳app ?答案是肯定的
build.gradle
描述子模块的项目的插件、属性、依赖等。可以在settings.gradle
中自定义脚本文件名
project(":VScreen").buildFileName = "lib_core.gradle" //改变脚本一个工程打两份工,实测ojbk
Gradle Event Log 提示重复工程,不友好,但是能节省了一个无意义的壳。
23:37 Duplicate content roots detected: Path [/Users/system/Work/projectcode/zippkgcode/vx-screen/VScreen] of module [vx-screen.VScreen] was removed from modules [vx-screen.VScreen_App]
gradle 命令时, 默认情况下总是会构建当前目录下的文件 build.gradle
可以添加-b 参数
或 -p 参数
gradle xxxTask -b lib_core.gradle
gradle xxxTask -p 所在目录
2. 根项目的构建配置
根项目下build.gradle 描述根模块的项目的插件、属性、依赖等。
大家最熟悉的buildscript
,里面也一般配置大家熟悉的repositories
dependencies
属性
buildscript {
ext.gradle_tools_version = '7.0.4' //可定义全局属性和函数
repositories {}
dependencies {}
}
另外allprojects
的下配置repositories
是不是也熟悉,这是配置此项目及其每个子项目属性。因此这里可以很灵活地配置项目所需属性。如统一编译配置、动态依赖
//配置此项目及其每个子项目。
allprojects { //此方法针对该项目及其子项目执行给定的闭包。目标Project作为闭包的委托传递给闭包。
//配置此项目的存储库
repositories {
google()
maven { url "https://jitpack.io" } //也可以使用nexus,下文会说到
}
configurations.all { //目前只发现这里处理依赖相关的配置 [官网文档说明](https://docs.gradle.org/current/userguide/resolution_rules.html)
//每隔24小时检查远程依赖是否存在更新
resolutionStrategy.cacheChangingModulesFor 24, 'hours'
//每隔10分钟..
//resolutionStrategy.cacheChangingModulesFor 10, 'minutes'
// 采用动态版本声明的依赖缓存10分钟
resolutionStrategy.cacheDynamicVersionsFor 10 * 60, 'seconds'
resolutionStrategy.dependencySubstitution {
//project&module依赖关系切换处理 方式1
substitute(module("cn.mashang.stub_modules:api_box:1.0.0")) using(project(":stub_modules:api_box"))
substitute(module("cn.mashang.stub_modules:constant_box:1.0.0")) using(project(":stub_modules:constant_box"))
}
//transitive = false //默认为true,一般不会这样设,还可以指定Force、exclude等配置
}
//verbose javac: 开启java 编译log
gradle.projectsEvaluated {
tasks.withType(JavaCompile) {
options.compilerArgs << "-Xlint" << "-verbose" << "-XprintRounds" << "-XprintProcessorInfo" << "-Xmaxerrs" << "2000"
}
}
//:app 添加在评估此项目后立即调用的闭包。项目作为参数传递给闭包。当属于该项目的构建文件已执行时,此类侦听器会收到通知。例如,父项目可以将这样的监听器添加到其子项目。这样的侦听器可以在它们的构建文件运行后根据子项目的状态进一步配置这些子项目。
project.afterEvaluate { Project p ->
if (p.plugins.hasPlugin('com.android.application') || p.plugins.hasPlugin('com.android.library')) {
android {
compileSdkVersion 32
defaultConfig {
minSdkVersion 21 (默认)
targetSdkVersion 32
//构建project版本信息,此处能读取配置后的版本信息
if (buildFeatures.buildConfig) {
buildConfigField(intType, "BUILD_CODE", "${versionCode}")
buildConfigField(str, "BUILD_VERSION_NAME", "\"${versionName}\"")
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}
}
}
}
}
3. 常用公用的构建配置
一般我们会定义一些config.gradle和config.properties
配置文件,引用这些文件达到复用公用的配置信息。一般引用方式代码如下(示例):
apply from: rootProject.file('./buildConfig/baseAndroid.gradle') //这里建议用rootProject.file,和'./' 避免无法定位文件路径
//加载properties配置文件
def dict = new Properties()
dict.load(new FileInputStream(rootProject.file("./buildConfig/base.properties")))
def str = dict['DefString']
base.properties
定义了常用的变量值,相对gradle
更方便索引和维护以及覆盖属性 代码如下(示例):
#要用gbk 编码
# author rentianlong
#2020年 8月 7日 星期五 12时03分25秒 CST
#java basic type
DefString=String
DefInt=int
DefBool=boolean
DefLong=long
trueStr=true
falseStr=false
#android buildVersion
compileSdkVersion=30
minSdkVersion=19
targetSdkVersion=22
## 项目模块配置
# 所有模块app/lib切换开关, 集成相应模块:默认true
libModulesIsLib=true
如果是新项目,推荐buildSrc配置信息
baseAndroid.gradle
定义通用的配置, 更方便索引和维护 代码如下(示例):
/**
* 作用描述:
* Base-Android build file where you can add configuration options common to all sub-projects/modules.
* Base - Android构建文件,您可以添加配置选项常见的所有子项目/模块。
*/
//打印日志
println rootProject.file('./buildConfig/baseAndroid.gradle').getAbsolutePath()
//当前模块信息
def projectDir = getProjectDir()
def projectDirPath = projectDir.absolutePath
println projectDirPath + "\\build.gradle"
def projectName = project.getName()
//Properties工具方法
static def getBool(Properties properties, String key) {
return Boolean.parseBoolean(properties[key])
}
//加载配置文件
def dict = new Properties()
dict.load(new FileInputStream(rootProject.file("./buildConfig/base.properties")))
def moduleConfig = new File(projectDir, 'debugConfig.properties')
if (moduleConfig.exists()) {
println 'load submodule_customization configs: ' + moduleConfig.getAbsolutePath()
dict.load(new FileInputStream(moduleConfig))
}
def BUILD_COMPUTER_TIME = "BUILD_COMPUTER_TIME"
def str = dict['DefString']
def intType = dict['DefInt']
def longType = dict['DefLong']
def trueStr = dict['trueStr']
def compileSdkVersionVar = dict['compileSdkVersion'] as int
def minSdkVersionVar = dict['minSdkVersion'] as int
def targetSdkVersionVar = dict['targetSdkVersion'] as int
//组件化application和library 动态切换
def hasAppPlugin = pluginManager.hasPlugin("com.android.application")
def libModulesIsLib = getBool(dict, 'libModulesIsLib')
//是否是正式包 (BuildTypes)
boolean isReleaseBuildType() {
for (String s : gradle.startParameter.taskNames) {
if (s.contains("Release") | s.contains("release")) {
return true
}
}
return false
}
def isRelease = isReleaseBuildType()
project.ext.isRelease = isRelease
//println(">>>>> isRelease:$isRelease") //打印日志
//获取构建时间
long getBuildTime() {
def calendar = Calendar.getInstance()
if (!isRelease) { //编译优化策略
calendar.set(Calendar.HOUR_OF_DAY, 0)
calendar.set(Calendar.MINUTE, 0)
calendar.set(Calendar.SECOND, 0)
calendar.set(Calendar.MILLISECOND, 0)
}
return calendar.getTimeInMillis()
}
def myBuildTime = "${getBuildTime()}"
if (!hasAppPlugin) { //如果是非app模块
if (libModulesIsLib) { //组件化切换调试常见方案
plugins.apply("com.android.library")
println 'apply lib'
} else {
hasAppPlugin = true
plugins.apply("com.android.application")
println 'apply application'
}
}
ext.set("hasAppPlugin", hasAppPlugin)
ext.set("libModulesIsLib", libModulesIsLib)
//阿里路由框架启用, 像UI类库不需要路由增加编译压力
def hasLibARouter = ext.find("lib_arouter") == true
if (hasLibARouter) {
apply plugin: 'com.alibaba.arouter' //arouter register plugin 实现自动注册
println 'apply arouter '
}
android {
compileSdk compileSdkVersionVar
//resourcePrefix "submodule_customization_todo" //子模块定制待办事项
defaultConfig {
multiDexEnabled true
minSdk minSdkVersionVar
targetSdk targetSdkVersionVar
//版本信息默认
versionCode 1
versionName "1.0.0"
//资源配置
resConfigs "en", "zh"
//ndk配置
ndk {
//设置支持的so库框架
abiFilters 'armeabi-v7a'
}
//阿里路由框架启用, 像UI类库不需要路由增加编译压力
if (hasLibARouter) {
// 阿里路由框架注解配置, 每个模块需要依赖
javaCompileOptions {
annotationProcessorOptions {
arguments = [AROUTER_MODULE_NAME: projectName]
}
}
}
if (buildFeatures.buildConfig) {
buildConfigField("boolean", "IS_APPLICATION", "${hasAppPlugin}")
//构建时间
buildConfigField(longType, BUILD_COMPUTER_TIME, "${myBuildTime}")
//构建project版本信息,此处只能读取到版本1, 需要放在主脚本android闭包里
//buildConfigField(intType, "BUILD_CODE", "${versionCode}")
//buildConfigField(str, "BUILD_VERSION_NAME", "\"${versionName}\"")
}
}
//apk签名配置
signingConfigs {
keystore {
keyAlias 'xxx'
keyPassword 'xxx'
storeFile rootProject.file('./Release/xxx.jks')
storePassword 'xxx'
enableV1Signing true
enableV2Signing true
//通过 APK v4 签名,您可以使用 Android 11 中的 ADB 增量 APK 安装快速部署大型 APK。此新标志负责部署过程中的 APK 签名步骤。
enableV3Signing true
enableV4Signing true
}
}
buildTypes {
debug {
zipAlignEnabled true
minifyEnabled false
signingConfig signingConfigs.keystore
//独立调试
if (!libModulesIsLib) {
applicationIdSuffix ".debug"
sourceSets {
main { //建立demo资源夹
manifest.srcFile 'src/demo/AndroidManifest.xml'
java.srcDirs = ['src/main/java', 'src/demo/java']
res.srcDirs = ['src/main/res', 'src/demo/res']
}
}
}
}
release {
}
}
//java编译配置
compileOptions {
// Flag to enable support for the new language APIs
coreLibraryDesugaringEnabled true
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
//lint配置
lintOptions {
//不检查release版本的构建
checkReleaseBuilds false
//停用 出现错误时停止编译
abortOnError false
}
lintOptions {
checkDependencies true
}
//打包配置
packagingOptions {
merge "/arouter/config.properties"
}
//dex配置
dexOptions {
javaMaxHeapSize "4g"
//是否支持大工程模式
jumboMode = true
//预编译
preDexLibraries = true
//线程数
threadCount = 8
maxProcessCount = 8 // this is the default value 4 //根据CPU核心设置
//设置是否启用dx增量模式 debug时,开启有加速效果
incremental true
//是将 dx 编译器作为单独的进程运行还是在 Gradle 守护进程 JVM 中运行
dexInProcess = true
}
//adb配置
adbOptions {
//timeOutInMs 5 * 1000 //超时
//installOptions '-r' //覆盖
//installOptions '-r -t' //覆盖测试 ()
//installOptions '-t' //测试 ()
//installOptions '-d' //降级
}
buildFeatures {
//feature enable state config
}
sourceSets {
main {
}
}
}
def autoDependencies = ext.find("auto_dependencies") == false //自动依懒关闭 (默认开启)
def autoBasicLibDependencies = ext.find("auto_basiclib_dependencies") == null //自动依懒基本库开启 (默认开启)
//公共依赖
dependencies {
//api fileTree(include: ['*.jar'], dir: 'libs') //确保libs 都是要加入才开启注释
if (autoDependencies) { //自动依懒关闭
println("baseAndroid.gradle:auto_dependencies:close " + projectName)
return null
}
rootProject.ext.dependencies.basicApi.each { implementation(it) }
println("baseAndroid.gradle:basicApi:auto: " + projectName + " <<== " + rootProject.ext.dependencies.basicApi)
if (autoBasicLibDependencies) {
//本地lib工程
rootProject.ext.dependencies.basicLibProject.each {
String itemName = it
if (!itemName.contains(projectName)) {
println("baseAndroid.gradle:basicLibProject:auto: " + projectName + " <<== " + itemName)
implementation project(itemName)
}
}
//本地lib Nexus
if ("lib_baseAndroid" != projectName) {
//lib_baseAndroid 模块
//implementation 'cn.mashang.base_modules:lib_baseAndroid:1.0.0'
}
}
//阿里路由框架启用, 像UI类库不需要路由增加编译压力
if (hasLibARouter) {
println("baseAndroid.gradle:lib_arouter:auto: ==> " + projectName)
implementation('com.alibaba:arouter-api:1.5.2') { // 阿里路由框架api
exclude group: 'com.android.support', module: 'support-v4'
}
annotationProcessor 'com.alibaba:arouter-compiler:1.5.2' // 阿里路由注解框架,每个模块需要依赖
}
//Java 8 及更高版本 API 脱糖支持
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.5'
}
以上代码(示例):
它方便了 lib模块切换为app、签名配置、公共依赖等
-
lib模块切换为app实现关键:要根据开关配置应用的application或library插件
关键代码如下:
if (!hasAppPlugin) { //如果是非app模块
if (libModulesIsLib) { //组件化切换调试常见方案
plugins.apply("com.android.library") //等同于apply plugin: 'com.android.library'
println 'apply lib'
} else {
hasAppPlugin = true
plugins.apply("com.android.application") //apply plugin: 'com.android.application'
println 'apply application'
}
}
base.properties 下libModulesIsLib 控制所有模块app/lib切换开关,false所有lib模块转换为app
# 所有模块app/lib切换开关, 集成相应模块:默认true
libModulesIsLib=true
工程app简单示例如下:
apply plugin: 'com.android.application'
apply from: rootProject.file('./buildConfig/baseAndroid.gradle')
模块lib简单示例如下:
//默认应用的是com.android.library
apply from: rootProject.file('./buildConfig/baseAndroid.gradle')
真实项目中,不需要所有模块都能单独调试和运行
所以利用了properties
覆盖属性,其实properties
是扩展Hashtable
的,不难想象load
其它配置后相当于map.put
def moduleConfig = new File(projectDir, 'debugConfig.properties')
if (moduleConfig.exists()) {
println 'load submodule_customization configs: ' + moduleConfig.getAbsolutePath()
dict.load(new FileInputStream(moduleConfig))
}
每个模块下创建debugConfig.properties
文件, 放置调试的配置信息
debugConfig.properties
,配置变动记得在View-Gradle 视图中reload gradle project
# 单模块app/lib切换开关, 集成相应模块: false或'',null为app, 默认true, 修改后需要 reload gradle project
#libModulesIsLib=false
.gitignore
小技巧,提交完debugConfig.properties
文件,使用它
/build
./debugConfig.properties
- 公共依赖要注意依赖的合理性和传递性
二、nexus与maven-publish
Nexus 最为大家熟知的功能就是 maven 的依赖包管理器。
架设 Nexus 私服有很多优点,其中之一就是:
- 方便上传团队内部的依赖,
统一管理,共享
aar
大家最为熟悉,也称为本地静态aar
依赖,对比远程仓库中的依赖包 implementation('com.squareup.retrofit2:retrofit:2.4.0')
,发现远程仓库中的依赖包的pom.xml
文件已经包括相应okhttp的依赖关系,这里不展开说明,所以远程依赖包的好处如下:
- 不用维护依赖传递
- 代码不需暴露
- 加快编译
1.安装nexus
运行命令
bin/nexus.exe /run
注册账号,nexus相关配置不一一说明
2.仓库
这里的仓库是指项目中依赖的第三方库,这个库所在的位置叫做仓库。 在Maven 中,任何一个依赖、插件或者项目构建的输出,都可以称之为构件。
跟项目下build.gradle
声明仓库地址
ext.maven_local_repo_url = "$projectDir/.repo" //本地仓库地址
ext.maven_nexus_snapshots_repo_url = 'http://xx.cpolar.cn/repository/yourProj-snapshots/'
ext.maven_nexus_releases_repo_url = 'http://xx.cpolar.cn/repository/yourProj-releases/'
repositories
配置
allprojects {
repositories {
maven {
url maven_local_repo_url
}
maven {
url maven_nexus_snapshots_repo_url
allowInsecureProtocol = true
credentials {
username = "guest"
password = "guest"
}
}
maven {
url maven_nexus_releases_repo_url
allowInsecureProtocol = true
credentials {
username = "guest" //nexus游客,只允许访问
password = "guest"
}
}
google()
jcenter()
}
3. maven-publish
Android Gradle 插件 3.6.0 及更高版本支持 Maven Publish Gradle 插件
使用 Maven Publish 插件
多数模块都需要发布,maven-publish.gradle 示例:
/**
* 作用描述: maven版本发布共享库管理,依赖maven可大大节省编译时间
* 创建人 rentl
* 创建日期 2022/1/30
* 修改日期 2022/1/30
*/
println 'Executing maven-publish...'
apply plugin: 'maven-publish'
def ENV = System.getenv()
task generateSourcesJar(type: Jar) {
from android.sourceSets.main.java.srcDirs
classifier 'sources'
}
def groupIdStr = ext.find("groupId")
def artifactIdStr = ext.find("artifactId")
if (groupIdStr == null) {
System.err.println('Executing maven-publish fail. groupId == null')
throw new IllegalArgumentException('Executing maven-publish fail. groupId == null')
}
if (artifactIdStr == null) {
System.err.println('Executing maven-publish fail. artifactId == null')
throw new IllegalArgumentException('Executing maven-publish fail. artifactId == null')
}
println "Executing maven-publish: groupId=$groupId, artifactId=$artifactId"
afterEvaluate {
publishing {
publications {
release(MavenPublication) {
from components.release
groupId = "$groupIdStr"
artifactId = "$artifactIdStr"
version = project.android.defaultConfig.versionName
artifact generateSourcesJar
}
}
repositories {
maven {
url = rootProject.ext.maven_local_repo_url
}
maven {
name = "nexus"
url = project.android.defaultConfig.versionName.endsWith('SNAPSHOT') ? rootProject.ext.maven_nexus_snapshots_repo_url : rootProject.ext.maven_nexus_releases_repo_url
allowInsecureProtocol = true
// 仓库用户名密码
credentials {
username = ENV['NEXUS_NAME']
password = ENV['NEXUS_PWD']
}
}
}
}
def publishTask = project.tasks.getByName('publishReleasePublicationToMavenRepository')
if (publishTask != null) {
publishTask.doLast {
println "maven-publish to .repo, Usage:\nimplementation '${groupIdStr}:${artifactIdStr}:${project.android.defaultConfig.versionName}'"
}
}
def publishTask2 = project.tasks.getByName('publishReleasePublicationToNexusRepository')
if (publishTask2 != null) {
publishTask2.doLast {
println "maven-publish to nexus, Usage:\nimplementation '${groupIdStr}:${artifactIdStr}:${project.android.defaultConfig.versionName}'"
}
}
}
模块的build.gradle 示例:
apply from: rootProject.file('./buildConfig/baseAndroid.gradle')
//maven
ext.groupId = "cn.mashang.base_modules"
ext.artifactId = project.getName()
android {
resourcePrefix "base_base_"
defaultConfig {
versionCode 1
versionName "1.0.0"
//versionName "1.0.1-SNAPSHOT"
consumerProguardFiles "consumer-rules.pro" //lib-proguard
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
}
if (!hasAppPlugin) { //这里需要放在这里底下,需要获取版本信息
apply from: rootProject.file('./buildConfig/maven-publish.gradle')
}
maven规约
这里还是要提及一下要遵maven规约
, 大家认同的详细规定参考下方:
1)GroupID格式:com.{公司/BU }.业务线.[子业务线],最多4级
正例:com.joymef.platform 或 com.joymef.social.blog
2)ArtifactID格式:产品线名-模块名。语义不重复不遗漏,先到仓库中心去查证一下
3)正例:user-service / user-client / blog-service ) Version
4)开发阶段版本号定义为SNAPSHOT,发布后版本改为RELEASE(强制)
上面是安卓模块publish,java的模块需要稍微调整
,maven-jar-publish.gradle示例如下:
/**
* 作用描述: maven jar版本发布共享库管理,依赖maven可大大节省编译时间
* 组件描述:
* 创建人 rentl
* 创建日期 2022/2/26
* 修改日期 2022/2/26
* 版权 mashang
*/
println 'Executing maven-java-publish...'
compileJava.options.encoding = 'UTF-8'
javadoc.options.encoding = 'UTF-8'
apply plugin: 'maven-publish'
java {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
//withJavadocJar()
withSourcesJar()
}
components.java.withVariantsFromConfiguration(configurations.sourcesElements) {
skip()
}
def ENV = System.getenv()
def groupIdStr = ext.find("groupId")
def artifactIdStr = ext.find("artifactId")
def versionStr = ext.find("version")
afterEvaluate {
publishing {
publications {
release(MavenPublication) {
from components.java
groupId = "$groupIdStr"
artifactId = "$artifactIdStr"
version = versionStr
}
}
repositories {
maven {
url = rootProject.ext.maven_local_repo_url
}
maven {
name = "nexus"
url = versionStr.endsWith('SNAPSHOT') ? rootProject.ext.maven_nexus_snapshots_repo_url : rootProject.ext.maven_nexus_releases_repo_url
allowInsecureProtocol = true
// 仓库用户名密码
credentials {
username = ENV['NEXUS_NAME']
password = ENV['NEXUS_PWD']
}
}
}
}
def publishTask = project.tasks.getByName('publishReleasePublicationToMavenRepository')
if (publishTask != null) {
publishTask.doLast {
println "maven-publish to .repo, Usage: implementation '${groupIdStr}:${artifactIdStr}:${versionStr}'"
}
}
def publishTask2 = project.tasks.getByName('publishReleasePublicationToNexusRepository')
if (publishTask2 != null) {
publishTask2.doLast {
println "maven-publish to nexus, Usage: implementation '${groupIdStr}:${artifactIdStr}:${versionStr}'"
}
}
}
另外对于gradle task不熟悉的同学可以打开Gradle 视图的 不启用Do not build task list..
注意发布依赖包时,注意模块的之间依赖关系,模块尽可能独立
三、动态依赖
- 模块下
build.gradle
在adnroid.dependencies{}
中配置当前项目的依赖信息,属于分离式配置的一种 - 在项目业务复杂的情况下,业务A、B模块依赖关系大体差不多,对于上面的静态依赖,则不灵活,难以复用,故想办法动态构建依赖树
1.依赖的传递性
这里要明白依赖的传递性,依赖关系树的概念,示例如下:
C模块依赖于==> B模块
B模块依赖于==> A模块
由于传递性:C模块同样依赖于==>A模块
2.project/module
依赖切换
基本实现示例如下:
if(op){
api project(':base_modules:annotation_lib') //
}else{
api('cn.mashang.base_modules:annotation_lib:1.0.0-SNAPSHOT') { changing = true }
}
3. 总结与实践
初步分析:
- 模块的依赖对应关系应该采用
map
数据结构构建关系,key
为project,value
为上面的project/module
依赖切换详情项 - 依赖的传递性可以采用
递归循环
- 压缩
project/module
依赖信息为map
,project
、moudule
、dep_option
字段必须
进步分析:
-
依赖项配置:
implementation
、api
、compileOnly
、runtimeOnly
、annotationProcessor
、debugImplementation
等,其中api
具有传递性 - 依赖信息为
map
,aar
、all_dep_option
、my_dep_option
、description
、version
、group
扩展以上字段 - 建立初步的
ext.modules_dependencies=[]
key项目PATH, value依赖项等描述依赖关系,示例如下:
//全局依赖设置,只有project依赖或module依赖方式
ext.all_dep_option = "project" //module/project
ext.all_dep_map = [
//依赖详情map
"lib_face_detect" : [ //project:项目PATH, module:maven, aar:aar文件, all_dep_option:全局参数, my_dep_option:单项参数
"project" : ":feature_face:lib_face_detect",
"module" : "cn.mashang.feature_face:lib_face_detect:1.0.3",
"aar" : "",
"all_dep_option": all_dep_option,
//"my_dep_option" : "module", //打开注释可以单独生效
"description" : "",
"version" : "",
"group" : "",
],
//添加更多
]
//添加依赖方式,默认`implementation`
ext.addDep = { String score = "implementation", String key ->
println "addDep: $key"
Map<String, String> map = new HashMap<>(all_dep_map[key])
map.put("score", score)
return map
}
//基础依赖
def app_core_map = [
//依赖详情map
addDep("lib_face_detect"),
addDep("api_face")
addDep("multidex"),
addDep("lib_comm_ui"),
addDep("lib_arouter"),
]
//项目中依赖关系,集中管理
ext.modules_dependencies = [
//key项目PATH, value依赖项
":app" : app_core_map, //复用!!!
":app1" : app_core_map,//复用!!!
":app2" : app_core_map,//复用有点吊!!!
":api_face" : [
//依赖详情map
addDep("commons-net"),
]
]
println "modules_dependencies: " + modules_dependencies
//添加应用依赖工具方法并返回执行结果
ext.utils = [
applyDependency: { Project p1, Map<String, String> map ->
def name = p1.name
def isApplication = p1.pluginManager.hasPlugin("com.android.application")
def projectInfo = map.getOrDefault("project", "")
def moduleInfo = map.getOrDefault("module", "")
def aarInfo = map.getOrDefault("aar", "")
def all_dep_option = map.getOrDefault("all_dep_option", "")
def my_dep_option = map.getOrDefault("my_dep_option", "")
def score = map.getOrDefault("score", "implementation")
//println "> applyDependency: ${name} ${isApplication ? "isApp" : ""}, map: " + map
if (projectInfo.isBlank() && moduleInfo.isBlank() && aarInfo.isBlank()) {
System.err.println("warning: projectInfo.isBlank() && moduleInfo.isBlank() && aarInfo.isBlank()")
return
}
boolean applyProject = ("project" == my_dep_option) && !projectInfo.isBlank()
boolean applyModule = ("module" == my_dep_option) && !moduleInfo.isBlank()
boolean applyAAR = ("aar" == my_dep_option) && !aarInfo.isBlank()
if (!(applyProject || applyModule || applyAAR)) {
applyProject = ("project" == all_dep_option) && !projectInfo.isBlank()
applyModule = ("module" == all_dep_option) && !moduleInfo.isBlank()
applyAAR = ("aar" == all_dep_option) && !aarInfo.isBlank()
}
if (applyProject) {
Project depProject = p1.findProject(projectInfo)
if (depProject == null) {
//按需处理,项目没有include时是否采用远程依赖
//if (!moduleInfo.isBlank()) {
// p1.dependencies.add(score, moduleInfo, { changing = moduleInfo.contains('cn.mashang') })
// //println "> applyDependency: ${name} ${isApplication ? "isApp" : ""}, score:$score, addModule: " + moduleInfo
// return "$score '$moduleInfo'"
//}
throw new Exception("请检查 ${projectInfo}")
}
p1.dependencies.add(score, depProject)
//println "> applyDependency: ${name} ${isApplication ? "isApp" : ""}, score:$score, addProject: " + projectInfo
return "$score project('$projectInfo')"
} else if (applyModule) {
p1.dependencies.add(score, moduleInfo, { changing = moduleInfo.contains('cn.mashang') })
//println "> applyDependency: ${name} ${isApplication ? "isApp" : ""}, score:$score, addModule: " + moduleInfo
return "$score '$moduleInfo'"
} else if (applyAAR) {
ConfigurableFileCollection depProject = p1.files(aarInfo)
if (depProject == null) {
throw new Exception("请检查 ${aarInfo}")
}
p1.dependencies.add(score, depProject)
//println "> applyDependency: ${name} ${isApplication ? "isApp" : ""}, score:$score, addAar: " + moduleInfo
return "$score files('$aarInfo')"
}
return ""
}
]
- 最后
应用动态依赖
, 配置项目阶段可以注入关系
项目build.gradle
project.afterEvaluate { Project p ->
//println "> afterEvaluate: " + p
def list = modules_dependencies.get(p.path)
if (list == null) {
return
}
def logStr = new StringBuilder(p.name)
logStr.append(",配置动态依赖阶段 (可参照输出日志,修改静态依赖)")
logStr.append("\n")
logStr.append("dependencies {")
logStr.append("\n")
//动态依赖
list.each { e ->
def ret = rootProject.ext.utils.applyDependenc
logStr.append(" ")
logStr.append(ret)
logStr.append("\n")
}
logStr.append("}\n")
println logStr
}
Build Project
查看gradle 依赖关系,欧凯
总结:通过上面分析和示例,大体实现了项目的动态依赖,集中管理,能一键切换所有本地模块和远程模块依赖方式(调整
all_dep_option
参数即可),也能单独切换某一项(调整my_dep_option
参数即可),对于一些特殊模块也可以声明aar
参数强制本地依赖包。更多实践取决分析项目需要
四、模块通信
模块化目的是为了降低低耦合,提高独立性。集成模块时,业务间需要进行相互通信(调用),有经验的同学会立马想起路由、事件、接口方式
1.通信方式
- 路由(是值得推荐的)
- 事件(Eventbus、广播不值得使用,难追溯)
- 接口(暴露接口,值得推荐)如微信 Android 模块化架构重构实践(上)暴露api
2.路由方式
对于新项目必须引用路由框架如ARouter、WMRouter、DRouter,必须用💜考虑一番,下面简单介绍一下
-
ARouter 稳定、易上手,但使用IProvider 接口的
Service
是单例,且生成代码增量编译以及多线程扫描方面一直没改进迭代,不支持SPI
是最大的鸡肋,作为一款UI路由倒十分合适 -
WMRouter 易调试,提供了
ServiceLoader
模块, 对AGP7.0
插件Transform不友好,并同ARouter
注解生成多数代码用的全是反射 - DRouter 功能强大,不仅支持ServiceLoader,还支持增量编译,多线程扫描,提升编译效率,较难掌握,类似多维过滤器、跨进程、共享内存方面不一定需要使用
3.接口方式
- 微信api化,需要自定义插件,在开始编译时,复制
.api
文件并重命名.java
-
api lib工程
放入基本数据结构和接口 (模块多时维护频繁变)
4.总结与实践
从上面分析知,模块化通信解决方案各有优劣,如路由框架的笨重难以维护,微信api化需要维护
gradle插件
,api文件容易被误修改等,所以要针对自身项目灵活运用,如下分析一个项目情况:
- 实践模块化项目比较老,已经采用了
ARouter
, 不可能再引用路由或者重写路由框架来解决SPI支持问题
- 实践模块化项目刚起步,逐步分离20个模块,模块之间通信较少
- 项目开启
proguard
,如果某些类方法没有KEEP
,会导致反射调用失败,另外项目注重效率,尽量避免反射 -
java spi
在Android
上需要读取整个apk,十分耗时,且安卓上是apk,dex,定义的Service
可能会发生冲突,难以定位
通过分析项目情况,需要大概的解决方案
如下:
- 输出一套简单实用的
android spi
机制.export_table
- 输出一套简单好维护的
api化
方案,api_box
总结
以上就是要讲的Android-模块化-项目实践和探索分享内容,本文仅仅简单分享了模块中的灵活运用一些方法,如有疑问,请和我联系,交流学习。