fat-aar实践及原理分享

项目背景

聚合收银台一直在滴滴内部使用,我们在编译的时候需要配置滴滴内部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(建议开发和发布两个分支,需要打包时发布分支合并开发分支代码)

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,132评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,802评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,566评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,858评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,867评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,695评论 1 282
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,064评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,705评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,915评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,677评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,796评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,432评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,041评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,992评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,223评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,185评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,535评论 2 343

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,598评论 18 139
  • fat-aar.gradle是什么? 在做android应用程序开发时,我们一般都会构建多个模块,来达到解耦的目的...
    王岩_shang阅读 16,564评论 13 23
  • 刘志文老师来的日子,是我脚伤50多天第一次走出家门的日子。 刘老师从医院退休已有三、四年了,丰富的咨...
    丑苹果阅读 264评论 0 0
  • 2015年8月之前,我用的是iPhone4。 自从换了iPhone6,我发现它的相机简直比4代要好太多了,而且还自...
    339da1fbd744阅读 265评论 0 1
  • 开源大数据 1.HadoopHDFS、HadoopMapReduce,HBase、Hive 渐次诞生,早期Hado...
    kuntoria阅读 200评论 0 1