fat-aar实践及原理分享

1字数 2354阅读 2242

项目背景

聚合收银台一直在滴滴内部使用,我们在编译的时候需要配置滴滴内部maven库,编译时必须连接公司内网;因雄安项目对外提供收银台SDK,但是外部编译时无法下载内部库,导致编译失败。于是思考把内部依赖的库全部下载到收银台本地项目中,一起打包提供给外部使用,经过查找,已有fat-aar这样一个开源解决方案,但因长时间没有维护,实践过程中存在一些问题

fat-aar项目地址:https://github.com/adwiv/android-fat-aar

方案概述

  1. aar包结构介绍

    aar是Android Library Project的二进制文件包,文件的扩展名是aar,其实文件本身就是一个简单的Zip文件,解压后有以下几种类型,相信Android开发同学都不会陌生

    • /AndroidManifest.xml(必须)
    • /classes.jar(必须)
    • /res/(必须)
    • /R.txt(必须)
    • /assets/(可选)
    • /libs/*.jar(可选)
    • /jni/<abi>/*.so(可选)
    • /proguard.txt(可选)
    • /lint.jar(可选)

    备注:R.txt文件是aapt --output -text -symbols输出,aapt相关细节这里不再叙述

  2. 方案思路:合并aar

image

如上图所示,我们把依赖的外部aar和内部module(可以看成aar)输出的N个aar文件进行合并,这样原来A模块的调用者接入方式保持不变,而且在依赖A时不必再重新下载A内部依赖的其他aar,可以提供给外部项目使用而避免访问滴滴maven库的场景

参考上面aar包结构形式,fat-aar合并主要过程为:

  • 合并Manifest
  • 合并jar
  • 合并res资源
  • 合并R文件(最关键的一步)
  • 合并assets
  • 合并libs
  • 合并jni
  • 合并Proguard

fat-aar接入

fat-aar接入非常简单,可直接参考 fat-aar

Step 1

下载fat-aar文件到本地项目module下,然后在build.gradle中依赖fat-aar.gradle

apply from: 'fat-aar.gradle'

或者直接远程依赖

apply from: 'https://raw.githubusercontent.com/adwiv/android-fat-aar/master/fat-aar.gradle'
Step 2

将需要下载的内部aar或本地lib的compile替换成embedded,embedded是fat-arr内部定义的一个属性

dependencies {
  compile fileTree(dir: 'libs', include: ['*.jar'])
  
  embedded project(':librarytwo')
  embedded project(':libraryone')
  embedded 'com.example.internal:lib-three:1.2.3'
  
  compile 'com.example:some-other-lib:1.0.3'
  compile 'com.android.support:appcompat-v7:22.2.0'
}
Step 3

将embedded依赖的project在对外发布aar时从pop.xml文件中去掉,避免外部依赖时再次下载,参考fat-aar下面的 publish.gradle,当然也可以自己实现

fat-aar工作原理

  1. fat-aar工作原理

    fat-aar主要思路就是合并aar,根据aar的文件结构,可划分为多个子任务

    首先,根据定义的embedded属性找出需要合并的aar,并将aar解压到相应目录下(注意gradle tools版本影响,建议设置为2.2.3)

    def dependencies = new ArrayList(configurations.embedded.resolvedConfiguration.firstLevelModuleDependencies)
    dependencies.reverseEach {
    
        def aarPath;
        if (gradleApiVersion >= 2.3f)
            aarPath = "${root_dir}/${it.moduleName}/build/intermediates/bundles/default"
        else
            aarPath = "${exploded_aar_dir}/${it.moduleGroup}/${it.moduleName}/${it.moduleVersion}"
        it.moduleArtifacts.each {
            artifact ->
    
                println "ARTIFACT 3 : "
                println artifact
                if (artifact.type == 'aar') {
                    if (!embeddedAarFiles.contains(artifact)) {
                        embeddedAarFiles.add(artifact)
                    }
                    if (!embeddedAarDirs.contains(aarPath)) {
                        if( artifact.file.isFile() ){
                            println artifact.file
                            println aarPath
    
                            copy {
                                from zipTree( artifact.file )
                                into aarPath
                            }
                        }
                        embeddedAarDirs.add(aarPath)
                    }
                } else if (artifact.type == 'jar') {
                    def artifactPath = artifact.file
                    if (!embeddedJars.contains(artifactPath))
                        embeddedJars.add(artifactPath)
                } else {
                    throw new Exception("Unhandled Artifact of type ${artifact.type}")
                }
        }
    }
    

    如果存在embedded属性的依赖,则定义各个子task执行的顺序(注意gradle版本影响,建议gradle tools版本设置为2.2.3)

    if (dependencies.size() > 0) {
        // Merge Assets
        generateReleaseAssets.dependsOn embedAssets
        embedAssets.dependsOn prepareReleaseDependencies
    
        // Embed Resources by overwriting the inputResourceSets
        packageReleaseResources.dependsOn embedLibraryResources
        embedLibraryResources.dependsOn prepareReleaseDependencies
    
        // Embed JNI Libraries
        bundleRelease.dependsOn embedJniLibs
    
        if (gradleApiVersion >= 2.3f) {
            embedJniLibs.dependsOn transformNativeLibsWithSyncJniLibsForRelease
            ext.bundle_release_dir = "$build_dir/intermediates/bundles/default"
        } else {
            embedJniLibs.dependsOn transformNative_libsWithSyncJniLibsForRelease
            ext.bundle_release_dir = "$build_dir/intermediates/bundles/release";
        }
    
        // Merge Embedded Manifests
        bundleRelease.dependsOn embedManifests
        embedManifests.dependsOn processReleaseManifest
    
        // Merge proguard files
        embedLibraryResources.dependsOn embedProguard
        embedProguard.dependsOn prepareReleaseDependencies
    
        // Generate R.java files
        compileReleaseJavaWithJavac.dependsOn generateRJava
        generateRJava.dependsOn processReleaseResources
    
        // Bundle the java classes
        bundleRelease.dependsOn embedJavaJars
        embedJavaJars.dependsOn compileReleaseJavaWithJavac
    
        // If proguard is enabled, run the tasks that bundleRelease should depend on before proguard
        if (tasks.findByPath('proguardRelease') != null) {
            proguardRelease.dependsOn embedJavaJars
        } else if (tasks.findByPath('transformClassesAndResourcesWithProguardForRelease') != null) {
            transformClassesAndResourcesWithProguardForRelease.dependsOn embedJavaJars
        }
    }
    
  2. fat-aar中定义的Task

    前面介绍了aar的结构以及fat-aar的工作原理,下面具体介绍几个Task

    • embedAssets

    合并Assets文件,其实就是简单的将embedded依赖的assets路径直接添加到当前project的assets目录下

    task embedAssets << {
        println "Running FAT-AAR Task :embedAssets"
        embeddedAarDirs.each { aarPath ->
        // Merge Assets
            android.sourceSets.main.assets.srcDirs += file("$aarPath/assets")
        }
    }
    
    • embedLibraryResources

    合并Res文件,通过getMergedInputResourceSets获取所有aar的res资源路径,然后添加到当前project的res资源路径

    task embedLibraryResources << {
        println "Running FAT-AAR Task :embedLibraryResources"
    
        def oldInputResourceSet = packageReleaseResources.inputResourceSets
        packageReleaseResources.conventionMapping.map("inputResourceSets") {
            getMergedInputResourceSets(oldInputResourceSet)
        }
    }
    
    • embedManifests

    合并Manifest,因代码片段过长,这里不粘贴代码了,主要思路就是通过XmlDocument操作Manifest节点将所有aar的Manifest文件合并

    • embedProguard

    合并Proguard,读取embedded依赖的aar中proguard混淆代码,直接追加在project的proguard后面

    task embedProguard << {
        println "Running FAT-AAR Task :embedProguard"
    
        def proguardRelease = file("$bundle_release_dir/proguard.txt")
        embeddedAarDirs.each { aarPath ->
            try {
                def proguardLibFile = file("$aarPath/proguard.txt")
                if (proguardLibFile.exists())
                    proguardRelease.append("\n" + proguardLibFile.text)
            } catch (Exception e) {
                e.printStackTrace();
                throw e;
            }
        }
    }
    
    • embedJniLibs

    合并jni中so文件,将embedded的aar中jni目录下所有文件拷贝到当前project的jni目录下

    task embedJniLibs << {
        println "Running FAT-AAR Task :embedJniLibs"
    
        embeddedAarDirs.each { aarPath ->
            println "======= Copying JNI from $aarPath/jni"
            // Copy JNI Folders
            copy {
                from fileTree(dir: "$aarPath/jni")
                into file("$bundle_release_dir/jni")
            }
        }
    }
    
    • generateRJava

    根据aar的R.txt文件生成相对应的R文件,首先通过Manifest文件获取相应的包名,然后通过遍历embeddedAarDirs查找每个aar中是否存在R.txt文件,根据R.txt生成相应的R文件,所有的id指向project的id

    task generateRJava << {
        println "Running FAT-AAR Task :generateRJava"
    
        // Now generate the R.java file for each embedded dependency
        def mainManifestFile = android.sourceSets.main.manifest.srcFile;
        def libPackageName = "";
    
        if(mainManifestFile.exists()) {
            libPackageName = new XmlParser().parse(mainManifestFile).@package
        }
    
        embeddedAarDirs.each { aarPath ->
    
            def manifestFile = file("$aarPath/AndroidManifest.xml");
            if(!manifestFile.exists()) {
                manifestFile = file("./src/main/AndroidManifest.xml");
            }
    
            if(manifestFile.exists()) {
                def aarManifest = new XmlParser().parse(manifestFile);
                def aarPackageName = aarManifest.@package
    
                String packagePath = aarPackageName.replace('.', '/')
    
                // Generate the R.java file and map to current project's R.java
                // This will recreate the class file
                def rTxt = file("$aarPath/R.txt")
                def rMap = new ConfigObject()
    
                if (rTxt.exists()) {
                    rTxt.eachLine {
                        line ->
                            //noinspection GroovyUnusedAssignment
                            def (type, subclass, name, value) = line.tokenize(' ')
                            rMap[subclass].putAt(name, type)
                    }
                }
    
                def sb = "package $aarPackageName;" << '\n' << '\n'
                sb << 'public final class R {' << '\n'
    
                rMap.each {
                    subclass, values ->
                        sb << "  public static final class $subclass {" << '\n'
                        values.each {
                            name, type ->
                                sb << "    public static $type $name = ${libPackageName}.R.${subclass}.${name};" << '\n'
                        }
                        sb << "    }" << '\n'
                }
    
                sb << '}' << '\n'
    
                mkdir("$generated_rsrc_dir/$packagePath")
                file("$generated_rsrc_dir/$packagePath/R.java").write(sb.toString())
    
                embeddedRClasses += "$packagePath/R.class"
                embeddedRClasses += "$packagePath/R\$*.class"
            }
        }
    }
    
    • collectRClass

    将generateRClass生成的R文件拷贝到'$build_dir/fat-aar/release/'目录下

    task collectRClass << {
        println "COLLECTRCLASS"
        delete base_r2x_dir
        mkdir base_r2x_dir
    
        copy {
            from classs_release_dir
            include embeddedRClasses
            into base_r2x_dir
        }
    }
    
    • embedJavaJars

    将'$build_dir/fat-aar/release/'路径中R文件打包进同一个jar包,放在'$bundle_release_dir/libs/'目录下,在collecRClass后执行

    task embedRClass(type: org.gradle.jvm.tasks.Jar, dependsOn: collectRClass) {
        println "EMBED R CLASS"
    
        destinationDir file("$bundle_release_dir/libs/")
        println destinationDir
        from base_r2x_dir
        println base_r2x_dir
    }
    

使用fat-aar遇到的一些问题

  1. generateRJava生成的R文件中id找不到

    修改generateRJava,在project生成R文件之后执行,可根据project的R文件来过滤aar中R.txt中的id(aar和project依赖的v7、v4版本不同),如果R.txt中的id在project的R.class文件中找不到,则过滤掉

    def rClassFile = file("$generated_rsrc_dir/com/didi/unified/pay/R.java")
    def rClassMap = new ConfigObject()
    
    def subClassName = null
    
    if (rClassFile.exists()) {
        rClassFile.eachLine {
            line ->
                line = line.trim()
                if(line.contains("public static final class ")) {
                    def subline = line.substring(("public static final class").length())
                    subClassName = subline.substring(0, subline.indexOf("{")).trim()
                } else if (line.contains("public static final int[] ")) {
                    def subline = line.substring(("public static final int[]").length())
                    def name = subline.substring(0, subline.indexOf("=")).trim()
                    rClassMap[subClassName].putAt(name, 1)
                } else if (line.contains("public static int ")) {
                    def subline = line.substring(("public static int").length())
                    def name = subline.substring(0, subline.indexOf("=")).trim()
                    rClassMap[subClassName].putAt(name, 1)
                }
        }
    }
    
    ...
    
    if (rTxt.exists()) {
        rTxt.eachLine {
        line ->
            //noinspection GroovyUnusedAssignment
             def (type, subclass, name, value) = line.tokenize(' ')
             if (rClassMap[subclass].containsKey(name)) {
                 rMap[subclass].putAt(name, type)
             }
        }
    }
    
  2. 自定义style,找不到相对应的id

    修改generateRJava,自定义Style在R.txt中存在形式为style,但是在class文件引用中为styleable,可以直接将style改为styleable

    if (rTxt.exists()) {
        rTxt.eachLine {
        line ->
            //noinspection GroovyUnusedAssignment
             def (type, subclass, name, value) = line.tokenize(' ')
             try {
                 if (subclass.equals("style")) {
                    subclass = "styleable"
                 }
                 if (rClassMap[subclass].containsKey(name)) {
                    rMap[subclass].putAt(name, type)
                 }
             } catch (Exception e) {
                 e.printStackTrace()
             }
       }
    }
    
  3. 发布aar打包时需要去掉pop.xml中embedded依赖的aar

fat-aar使用注意事项

  1. project目录下gradle tools版本配置为3.1.0时编译出错,建议使用2.2.3

  2. gradle目录下gradle-wrapper.properties中建议配置distributionUrl=https://services.gradle.org/distributions/gradle-3.3-all.zip 参考issue

  3. fat-aar合并aar时注意不要把一些公共库合并进去(比如v7、v4),如果模块中有重复的依赖,fat-aar会报错提示你某些类或资源文件冲突,解决方案有:

    • 打包aar时配置相关依赖transitive false
    compile ('com.example.internal:lib: x.x.x') {  
        // Notice the parentheses around project
        transitive false
    }
    
    • 外部项目中忽略掉远程的依赖
    configurations {
        all*.exclude group: 'com.example.internal', module: 'lib'
    }
    
  4. fat-aar最好只用来合并aar使用,embedded属性不等同于compile,开发和调试模块时最好使用compile,打包时使用embedded(建议开发和发布两个分支,需要打包时发布分支合并开发分支代码)

推荐阅读更多精彩内容