为Fat-AAR增加多productFlavors支持,并支持AGP7,实现合并AAR的Gradle插件

AndroidAarPacker

仓库地址MaYiFei1995/AndroidAarPacker

问题

之前项目一直在使用cpdroid/fat-aar合并多个本地的AARJAR包,也专门进行过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")
        }
    }

styleableattrSymble创建需要特殊处理,如需要处理请参照com.android.ide.common.symbols.ResourceValuesXmlParser#parseChild方法

这下生成的新R.class中就包含了对应的资源属性,但打包运行后仍然报错,还需要继续分析处理

合并R.txt

按照上面的方式生成新的R$style.class后依然不能正常运行,分析错误发现问题,生成的 aar 文件的R.txt中没有包含这部分的内容。比对发现 AGP4 时输出的R.txt包含了忽略的dependenciesR.txt,但这里只能手动合并文件了。

找到生成R.txt的任务generate${flavorBuildType}RFile,添加doFirsttask,将解压后的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 配置,需要调整插件适配

posted on 2022-10-24 18:05  maiiiiii  阅读(301)  评论(0编辑  收藏  举报

导航