安卓对外发布工程源码:怎么做到仅UI层公布 - Wesley's Blog

最近新客户要求将软件的代码公开给他们,允许他们自己修改 UI,进行定制。由于我们工程包含其他核心模块,不能全部开放,所以需要将非 UI 模块进行整合并混淆再发布。
由于工程具有良好的模块化,所以一开始想到的是参考安卓官方版fat-aar:使用Fused Library将多个Android库发布为一个库 - Wesley's Blog将非 UI 模块合并成一个 aar。
但有挑战:
- 依赖传递很难处理,就算处理了也不方便后续维护
- 存在部分内网 SDK 依赖
为了不改变 maven 的管理方式,决定使用本地 maven 的方式。
可以先用gradlew build --scan
生成一棵依赖树,网页版方便查看 app 有哪些依赖,还可以定位内网的依赖来自于哪个地址。
实施
在gradle.properties 定义本地 maven 路径
ini
LOCAL_MAVEN_URL=maven_repo
GROUP_ID=com.wesley.test
SDK_VERSION=1.0.0 #统一控制所有模块版本
settings.gradle引入本地依赖
bash
maven {
url = uri("file://${rootProject.projectDir}/${LOCAL_MAVEN_URL}")
}
各自模块接入maven-publish
scss
plugins {
alias(libs.plugins.android.library)
alias(libs.plugins.kotlin.android)
id 'maven-publish'
}
def ARTIFACT_ID='base-base'
android {
//不能发布源码给第三方
publishing {
singleVariant('release') // 不生成 sources JAR
}
...............
//如果有模块重名,比如 feature:base, base:base, 需要定义
compileOptions {
kotlinOptions.freeCompilerArgs += ['-module-name', "$GROUP_ID.$ARTIFACT_ID"]
}
}
publishing {
publications {
maven(MavenPublication) {
groupId GROUP_ID
artifactId ARTIFACT_ID
version SDK_VERSION
afterEvaluate {
from components.release
}
}
}
repositories {
maven {
url rootProject.file(LOCAL_MAVEN_URL).toURI()
}
}
}
如果模块存在多个 flavor:
scss
plugins {
alias(libs.plugins.android.library)
alias(libs.plugins.kotlin.android)
id 'maven-publish'
}
android {
publishing {
multipleVariants {
allVariants()
//withJavadocJar()
}
}
}
publishing {
publications {
maven(MavenPublication) {
groupId GROUP_ID
artifactId "feature-xxx"
version SDK_VERSION
afterEvaluate {
from components.default
}
}
}
repositories {
maven {
url rootProject.file(LOCAL_MAVEN_URL).toURI()
}
}
}
如果要在发布时控制某些代码不混淆或者依赖方式变更:
arduino
// 在根项目的 build.gradle 中
ext {
// 定义判断是否是发布任务的方法
isPublishTask = { ->
gradle.startParameter.taskNames.any {
it.startsWith("publish")
}
}
}
less
if (isPublishTask()) {
// 发布时将 aar 打包进 sdk
println("wesley publish")
//embed(project( ":feature:testAAR" ))
// id("com.kezong.fat-aar")
embed project(path: ":feature:testAAR", configuration: 'default')
} else {
implementation(project( ":feature:testAAR" ))
}
其他
对于不携带 aar 包的模块,比如某 B 模块implementation(project( ":base:base" )),如果:base:base 也配置了 publish 任务,依赖它的模块不需要修改成远程依赖方式。为什么呢?问了一下 AI:
核心原因:项目内依赖(Project Dependency) vs. Maven 依赖(Maven Dependency)的转换
在模块 A 的构建期间: (
implementation project(":base:base")
)
- 当 Gradle 构建你的
A
模块时,它看到了implementation project(":base:base")
。- 这里的
project(...)
告诉 Gradle:"这是一个项目内依赖 。你不需要去远程仓库下载任何东西,直接在本次构建中找到:base:base
模块,编译它,然后让A
模块使用它的代码即可。"- 在这个阶段,一切都在你的本地项目中进行,与 Maven 仓库和版本
1.1
是否已发布完全无关。在发布模块 A 时:(生成
pom.xml
)
- 当你运行
publish
任务时,maven-publish
插件的职责是为模块A
创建一个元数据文件,也就是pom.xml
。- 这个 POM 文件是给外部用户看的。外部用户(比如你的同事或者一个CI服务器)没有你项目的完整源码,他们需要通过 Maven 坐标(groupId:artifactId:version)来下载依赖。
- 因此,
maven-publish
插件必须将项目内依赖project(":base:base")
"翻译" 成一个外部用户可以理解的 Maven 依赖。"翻译"的过程是怎样的?
maven-publish
插件非常智能,它会执行以下操作:
它发现
A
依赖于project(":base:base")
。它会去检查
:base:base
模块的build.gradle.kts
文件。它在文件中找到了
publishing
配置块,并读取了里面定义的 Maven 坐标:
groupId: "com.wesley.test"
artifactId: "base-base"
version: "1.1"
然后,它将这些信息忠实地写入
A
模块的pom.xml
文件中,生成了你看到的<dependency>
块。一个形象的比喻: 这就像你在写一份菜谱(发布模块 A)。你在菜谱里写到需要"我祖母的秘制酱料"(项目内依赖
:base:base
)。为了让别人也能做出这道菜,你必须在菜谱的配料表上写清楚这种酱料在超市里的商品名和品牌(Maven坐标com.wesley.test:base-base:1.1
),而不是只写"我祖母的秘制酱料"。
maven-publish
就是那个负责把"我祖母的秘制酱料"翻译成商品名的角色。它并不关心此刻超市里到底有没有货(1.1
版本是否已发布),它只负责正确地翻译菜谱。
为什么它不关心
1.1
是否已发布?
publish
任务的职责是生成并上传当前模块的构件和元数据。它假设你(开发者)会确保所有声明的依赖项最终都是可用的。
- 后果的承担者是消费者: 如果你发布了
A
,但没有发布:base:base
的1.1
版本,那么问题不会在你发布时出现,而是在别人使用你的A
模块时 出现。当他们的项目尝试根据A
的pom.xml
去下载 com.wesley.test:base-base:1.1` 时,Maven 会因为找不到这个依赖而构建失败。- 责任在你: Gradle 将保证依赖关系链正确的责任交给了开发者。你需要在发布
A
之前,先将它的所有依赖(如:base:base
)发布到 Maven 仓库。
最后执行 publish任务,所有模块都发布到maven_repo 里面了,以后向客户更新这个目录就行。
而对于内网 SDK,可以在网页版依赖树找到他们,然后通过类似
wget -r -np -nH --cut-dirs=8 -R "index.html*" http://172.20.135.19:8080/nexus/content/repositories/releases/com/wesley/sdk/protocol/2.1.8/
的命令下载,把文件丢进maven_repo(注意目录结构),这样依赖也可以正确传递了。(如果包含源码,可以考虑删除)
注意
删掉工程签名文件。
如果有些模块不能公开,混淆后不能发布源码。
不能携带 .git 目录。
注意处理 UI 模块依赖其他模块的方法或者类,不能混淆。
注意处理一些配置信息或者关键代码,可以考虑转移到 native 层。