为Fat-AAR增加多productFlavors支持,并支持AGP7,实现合并AAR的Gradle插件
AndroidAarPacker
仓库地址MaYiFei1995/AndroidAarPacker
问题
之前项目一直在使用cpdroid/fat-aar合并多个本地的AAR
和JAR
包,也专门进行过AGP的升级,但仍然无法满足根据productFlavor
进行定向embedded
的需求。
临时的方案是通过在dependencies
中获取 taskName ,再通过关键词判断选择embedded
,如:
dependencies {
def taskName = getGradle().getStartParameter().getTaskRequests().toString().toLowerCase()
if (taskName.contains("flavorA")) {
embedded(name:'a-a-a', ext:'aar')
} else if taskName.contains("flavorB") {
embedded(name:'b-b-b', ext:'jar')
}
// common
embedded(name:'common-lib', ext:'aar')
}
但这样配置总归是不好看,加上某个需要合并的 AAR 合并后报错找不到资源,就决定按照源码做一次完整的适配升级
AGP 升级
AS-2022不知道哪个版本开始,默认的版本就是7.3.3。索性就按照这个版本进行适配。
配置适配
首先是settings.gradle
,因为插件需要指定gradle
版本,要在dependencyResolutionManagement
下增加repositoriesMode.set(RepositoriesMode.PREFER_PROJECT)
。
新版本需要使用maven-publish
插件进行发布,由于是本地插件,只需要配置发布MavenLocal
上。新版本的build-tools
插件由于dependencies关系由Compile Dependencies
改为Runtime Dependencies
(见mvnrepository/build-tools-7.2.2) ,所以需要在dependencies
单独配置用到的插件并指定版本:
dependencies{
implementation 'com.android.tools.build:gradle:7.2.2'
implementation 'com.android.tools:sdk-common:30.2.2'
implementation 'com.android.tools:common:30.2.2'
implementation 'com.android.tools.layoutlib:layoutlib-api:30.2.2'
implementation gradleApi()
implementation localGroovy()
}
详细配置见plugin/build.gradle
工程适配
在需要使用打包插件的工程中修改settings.gradle
resolutionStrategy {
eachPlugin {
if (requested.id.namespace == "com.mai.aarpacker") {
// 适配
useModule("com.mai.aarpacker:aar-packer:${requested.version}")
}
}
}
repositories {
// 工程发布到了本地 Maven
mavenLocal()
//...
}
在需要合并的 module 中引入插件
plugins {
id 'com.android.library'
id 'com.mai.aarpacker.aar-packer' version '1.0'
}
增加新功能
支持 productFlavors
直接使用flavornameEmbedded
会在 snyc 时报错找不到方法,需要先遍历 flavor,再添加到 configuration 中
void apply(Project project) {
init(project)
project.android.productFlavors.all { flavor ->
// flavornameEmbedded
def configurationName = "${flavor.name}${EMBEDDED_CONFIGURATION_NAME.capitalize()}"
project.configurations.maybeCreate(configurationName)
}
// embedded
project.configurations.maybeCreate("$EMBEDDED_CONFIGURATION_NAME")
//...
此时已经可以声明了,但添加的 lib 没有参与到合并中,需要修改project.dependencices
:
project.android.libraryVariants.all { libraryVariant ->
try {
project.dependencies.add("implementation", project.configurations."$flavorName${EMBEDDED_CONFIGURATION_NAME.capitalize()}")
} catch (Throwable ignore) {
// 当没有声明favornameEmbedded时,会抛出空指针错误,需要忽略
logV("Embedded not found...")
}
project.dependencies.add("implementation", project.configurations."$EMBEDDED_CONFIGURATION_NAME")
并区分关键字的解压这些 lib:
// 解压aar
Task decompressTask = project.task("decompress${flavorBuildType}Dependencies", group: "aar-packer").doLast {
decompressDependencies(project, flavorName)
}
// embedded
project.configurations."$EMBEDDED_CONFIGURATION_NAME".dependencies.each {
dependencyTask(it, decompressTask, flavorBuildType, buildType)
}
try {
// flavorEmbedded
project.configurations."$flavorName${EMBEDDED_CONFIGURATION_NAME.capitalize()}".dependencies.each {
dependencyTask(it, decompressTask, flavorBuildType, buildType)
}
} catch (Throwable ignore) {
// NPE
}
然后在解压时,判断和添加这些到artifactList
中:
private def decompressDependencies(Project project, String flavorName) {
def artifactList = new ArrayList<ResolvedArtifact>()
// embedded
Configuration defaultConfiguration = project.configurations."$EMBEDDED_CONFIGURATION_NAME"
artifactList.addAll(defaultConfiguration.resolvedConfiguration.resolvedArtifacts)
try {
// flavorNameEmbedded
Configuration flavorConfiguration = project.configurations."$flavorName${EMBEDDED_CONFIGURATION_NAME.capitalize()}"
artifactList.addAll(flavorConfiguration.resolvedConfiguration.resolvedArtifacts)
} catch (Throwable ignore) {
}
}
其他步骤和之前几乎相同,直接移植fat-aar
就好
修改生成新的R文件部分
到这里适配和升级的工作就算完成了,但开头提到的某个需要合并的 AAR 合并后报错找不到资源
问题还没有解决。分析错误信息可以发现,是 lib 里直接使用了R.style.Theme_AppCompat
,但插件在根据aar/res
生成新的R.jar
指向时,没有办法生成这部分内容,导致NoSuchFieldError
。
那就从生成R.class
的部分入手修改插件,遍历R.txt
找到res/values
中不存在的style
并构造Symbol
合并到SymbolTable
中。参照com.android.ide.common.symbols.ResourceDirectoryParser#parseResourceSourceSetDirectory
方法实现:
def table = ResourceDirectoryParser.parseResourceSourceSetDirectory(resDir, IdProvider.@Companion.sequential(), null, null, true)
if (aarLibDir.contains("PREFIX_NAME")) {
table = parseAarRFile(new File("$aarLibsDir/R.txt"), IdProvider.@Companion.sequential(), table)
}
//...
/**
* 解析aar的R.txt文件,创建Symbol,合并SymbolTable
* 部分aar代码硬编码了R.style.AppCompat等属性
* 直接打包会因为AppCompat不在res的table中,最终在调用时报错NoSuchField
* 如找不到com.aar.pkg.R$style.Theme_AppCompat
*/
static SymbolTable parseAarRFile(File rFile, IdProvider idProvider, SymbolTable table) {
if (rFile.isFile() && rFile.exists()) {
def builder = new SymbolTable.Builder()
rFile.readLines().each { line ->
// "int anim abc_fade_in 0x7f010001"
def res = line.substring(line.indexOf(" ") + 1)
// "anim abc_fade_in 0x7f010001"
res = res.substring(0, res.lastIndexOf(" "))
// "anim abc_fade_in"
def split = res.split(" ")
def resourceType = ResourceType.fromClassName(split[0])
def symbolName = split[1]
// STYLE_ONLY && 非本地存在资源
if (resourceType == ResourceType.STYLE && !symbolName.startsWith("aar_name_pattern")(symbolName)) {
try {
addIfNotExisting(builder, Symbol.createSymbol(resourceType, symbolName, idProvider, false, false))
} catch (Throwable tr) {
tr.printStackTrace()
throw new RuntimeException(tr)
}
}
}
// merge
return builder.build().merge(table)
} else {
throw new IllegalArgumentException("Illegal file $rFile")
}
}
styleable
和attr
的Symble
创建需要特殊处理,如需要处理请参照com.android.ide.common.symbols.ResourceValuesXmlParser#parseChild
方法
这下生成的新R.class
中就包含了对应的资源属性,但打包运行后仍然报错,还需要继续分析处理
合并R.txt
按照上面的方式生成新的R$style.class
后依然不能正常运行,分析错误发现问题,生成的 aar 文件的R.txt
中没有包含这部分的内容。比对发现 AGP4 时输出的R.txt
包含了忽略的dependencies
的R.txt
,但这里只能手动合并文件了。
找到生成R.txt
的任务generate${flavorBuildType}RFile
,添加doFirst
的task
,将解压后的R.txt
合并到intermediaters/local_only_symbol_list/${flavorBuildType.uncapitalize()}/R-def.txt
中,这样根据规则合并后的内容会被用于生成输出的R.txt
project.tasks."generate${flavorBuildType}RFile".doFirst {
embeddedAarDirs.each { aarLibsDir ->
if (aarLibsDir.contains("aar_name_pattern")) {
def rFile = new File("$intermediatesDir/local_only_symbol_list/${flavorBuildType.uncapitalize()}/R-def.txt")
def rText = Files.asCharSource(rFile, Charsets.UTF_8).read()
def remoteFile = new File("$aarLibsDir/R.txt")
// merge aar_name_pattern.aar/R.txt
def newRText = mergeAARV28StyleRFile(remoteFile, rText)
// delete last line with content "\n"
// otherwise generateRFile will throw a index exception when call readLines()
Files.asCharSink(rFile, Charsets.UTF_8).write(newRText.substring(0, newRText.length() - 1))
}
}
}
根据规则合并
/**
* 合并AAR中的R.txt与工程的R.txt
* 在generate${FlavorBuildType}RFile前执行,将AAR中不存在的style写入输出的R-def.txt中
* 后续会根据合并后的R-def.txt生成最终输出的AAR的R.txt
*/
static def mergeAARV28StyleRFile(File remoteFile, String rText) {
remoteFile.readLines().each { line ->
// STYLE_ONLY
// int style Base_TextAppearance_AppCompat_Tooltip 0x7f150028
if (line.contains(" style ") && !line.contains("aar_name_pattern")) {
// "int style Theme_AppCompat 0x7f04008f"
def res = line.substring(0, line.lastIndexOf(" ")).substring(line.indexOf(" ") + 1)
// "style Theme_AppCompat"
def resName = res.substring(res.indexOf(" ") + 1)
logLevel1("resName:[$resName]")
// 非已存在
if (!rText.contains(res)) {
rText += "$res\n"
logLevel2("add res: $res")
}
}
}
return rText
}
AndroidX 项目此时已经完成了,但 Support 的项目还需要处理兼容问题
处理 Support-Compat 兼容问题
经过上面的处理,AndroidX 项目应该已经可以正常集成使用了,但测试时发现了 Support-Compat 的兼容问题。
因为要合并的 lib 使用了 V28 版本,所以合并时自动创建了 V28 部分的属性。但当合并后的 AAR 在编译的环境不是 V28 且运行在 api28 及以上的设备上时就会出现运行时的错误,读不到 V28 的属性。
由于项目需要兼容到 V26,只能在生成R.class
和合并R.txt
时额外处理,抛弃掉 V28 独有的属性:
/**
* 兼容AppCompatV26
* 工程的support-compat为26时,多出的主题会影响找不到资源,导致在api28以上的设备出现NoSuchField错误
* 应用编译时会根据R.txt和AppCompat的版本判断是否存在
* 需要在合并时移除V28独有的属性
*/
private static String[] appCompatV28ThemeList = new String[]{
"Base_V28_Theme_AppCompat",
"Base_V28_Theme_AppCompat_Light",
"RtlOverlay_Widget_AppCompat_PopupMenuItem_Shortcut",
"RtlOverlay_Widget_AppCompat_PopupMenuItem_SubmenuArrow",
"RtlOverlay_Widget_AppCompat_PopupMenuItem_Title",
}
static SymbolTable parseAarRFile(File rFile, IdProvider idProvider, SymbolTable table) {
//...
def resourceType = ResourceType.fromClassName(split[0])
def symbolName = split[1]
// STYLE_ONLY && 非本地存在资源 && 非AppCompat28独有属性
- if (resourceType == ResourceType.STYLE && !symbolName.startsWith("aar_name_pattern")
+ if (resourceType == ResourceType.STYLE && !symbolName.startsWith("aar_name_pattern") && !appCompatV28ThemeList.contains(symbolName)) {
try {
addIfNotExisting(builder, Symbol.createSymbol(resourceType, symbolName, idProvider, false, false))
} cathc (Throwable tr) {
//...
}
static def mergeAARV28StyleRFile(File remoteFile, String rText) {
//...
def resName = res.substring(res.indexOf(" ") + 1)
logLevel1("resName:[$resName]")
+ // 非AppCompat28独有属性
+ if (!appCompatV28ThemeList.contains(resName)) {
// 非已存在
if (!rText.contains(res)) {
rText += "$res\n"
logLevel2("add res: $res")
}
}
return rText
}
测试结果
当前需要合并的 lib 全部合并完成,在 supoort-compat:26\28 和 androidx.appcompat 环境下全部正常加载和展示
PUBLISH TO MAVEN
publish
时,在常规配置maven-publish
插件后,需要参照文档disabling-gmm-publication关闭 Metadata 的生成,并移除本地的denpendencies
项,如:
fterEvaluate {
publishing {
publications {
maven(MavenPublication) {
pom.withXml {
Node pomNode = asNode()
// remove <dependencies> node
pomNode.remove(pomNode["dependencies"])
// or remove only local aar
pomNode.dependencies.dependency.each() { node ->
if(node.type.text() == 'aar') {
node.parent().remove(node)
}
}
}
//...
总结
- 作为 SDK 编码时,尽量避免硬编码使用不属于自己的资源文件,更多的使用
context.getResources.getXXX
方法获取资源和属性 - 遇到 Gradle 编译过程中的错误,需要分析错误信息,找到报错的详细堆栈。可能会出现执行的方法中出错导致 gradle 报错
coudle not found method match signature
,不容易定位到问题。需要根据情况适当增加try-catch
或日志进行分析 - 为什么 google 不直接提供合并功能
- 暂不支持多 flavorDimensions 配置,需要调整插件适配