fat-aar實(shí)踐及原理分享

項(xiàng)目背景

聚合收銀臺(tái)一直在滴滴內(nèi)部使用挺庞,我們在編譯的時(shí)候需要配置滴滴內(nèi)部maven庫,編譯時(shí)必須連接公司內(nèi)網(wǎng)稼病;因雄安項(xiàng)目對(duì)外提供收銀臺(tái)SDK选侨,但是外部編譯時(shí)無法下載內(nèi)部庫,導(dǎo)致編譯失敗然走。于是思考把內(nèi)部依賴的庫全部下載到收銀臺(tái)本地項(xiàng)目中援制,一起打包提供給外部使用,經(jīng)過查找芍瑞,已有fat-aar這樣一個(gè)開源解決方案晨仑,但因長時(shí)間沒有維護(hù),實(shí)踐過程中存在一些問題

fat-aar項(xiàng)目地址:https://github.com/adwiv/android-fat-aar

方案概述

  1. aar包結(jié)構(gòu)介紹

    aar是Android Library Project的二進(jìn)制文件包拆檬,文件的擴(kuò)展名是aar洪己,其實(shí)文件本身就是一個(gè)簡單的Zip文件,解壓后有以下幾種類型秩仆,相信Android開發(fā)同學(xué)都不會(huì)陌生

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

    備注:R.txt文件是aapt --output -text -symbols輸出码泛,aapt相關(guān)細(xì)節(jié)這里不再敘述

  2. 方案思路:合并aar

image

如上圖所示,我們把依賴的外部aar和內(nèi)部module(可以看成aar)輸出的N個(gè)aar文件進(jìn)行合并澄耍,這樣原來A模塊的調(diào)用者接入方式保持不變噪珊,而且在依賴A時(shí)不必再重新下載A內(nèi)部依賴的其他aar,可以提供給外部項(xiàng)目使用而避免訪問滴滴maven庫的場景

參考上面aar包結(jié)構(gòu)形式齐莲,fat-aar合并主要過程為:

  • 合并Manifest
  • 合并jar
  • 合并res資源
  • 合并R文件(最關(guān)鍵的一步)
  • 合并assets
  • 合并libs
  • 合并jni
  • 合并Proguard

fat-aar接入

fat-aar接入非常簡單痢站,可直接參考 fat-aar

Step 1

下載fat-aar文件到本地項(xiàng)目module下,然后在build.gradle中依賴fat-aar.gradle

apply from: 'fat-aar.gradle'

或者直接遠(yuǎn)程依賴

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

將需要下載的內(nèi)部aar或本地lib的compile替換成embedded选酗,embedded是fat-arr內(nèi)部定義的一個(gè)屬性

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在對(duì)外發(fā)布aar時(shí)從pop.xml文件中去掉阵难,避免外部依賴時(shí)再次下載,參考fat-aar下面的 publish.gradle芒填,當(dāng)然也可以自己實(shí)現(xiàn)

fat-aar工作原理

  1. fat-aar工作原理

    fat-aar主要思路就是合并aar呜叫,根據(jù)aar的文件結(jié)構(gòu)空繁,可劃分為多個(gè)子任務(wù)

    首先,根據(jù)定義的embedded屬性找出需要合并的aar朱庆,并將aar解壓到相應(yīng)目錄下(注意gradle tools版本影響盛泡,建議設(shè)置為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屬性的依賴,則定義各個(gè)子task執(zhí)行的順序(注意gradle版本影響娱颊,建議gradle tools版本設(shè)置為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的結(jié)構(gòu)以及fat-aar的工作原理傲诵,下面具體介紹幾個(gè)Task

    • embedAssets

    合并Assets文件,其實(shí)就是簡單的將embedded依賴的assets路徑直接添加到當(dāng)前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資源路徑拴竹,然后添加到當(dāng)前project的res資源路徑

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

    合并Manifest,因代碼片段過長剧罩,這里不粘貼代碼了栓拜,主要思路就是通過XmlDocument操作Manifest節(jié)點(diǎn)將所有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目錄下所有文件拷貝到當(dāng)前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

    根據(jù)aar的R.txt文件生成相對(duì)應(yīng)的R文件,首先通過Manifest文件獲取相應(yīng)的包名舰罚,然后通過遍歷embeddedAarDirs查找每個(gè)aar中是否存在R.txt文件纽门,根據(jù)R.txt生成相應(yīng)的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文件打包進(jìn)同一個(gè)jar包营罢,放在'$bundle_release_dir/libs/'目錄下蝙搔,在collecRClass后執(zhí)行

    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文件之后執(zhí)行考传,可根據(jù)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,找不到相對(duì)應(yīng)的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. 發(fā)布aar打包時(shí)需要去掉pop.xml中embedded依賴的aar

fat-aar使用注意事項(xiàng)

  1. project目錄下gradle tools版本配置為3.1.0時(shí)編譯出錯(cuò)攘已,建議使用2.2.3

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

  3. fat-aar合并aar時(shí)注意不要把一些公共庫合并進(jìn)去(比如v7、v4)俊庇,如果模塊中有重復(fù)的依賴止后,fat-aar會(huì)報(bào)錯(cuò)提示你某些類或資源文件沖突歉糜,解決方案有:

    • 打包aar時(shí)配置相關(guān)依賴transitive false
    compile ('com.example.internal:lib: x.x.x') {  
        // Notice the parentheses around project
        transitive false
    }
    
    • 外部項(xiàng)目中忽略掉遠(yuǎn)程的依賴
    configurations {
        all*.exclude group: 'com.example.internal', module: 'lib'
    }
    
  4. fat-aar最好只用來合并aar使用,embedded屬性不等同于compile,開發(fā)和調(diào)試模塊時(shí)最好使用compile垫言,打包時(shí)使用embedded(建議開發(fā)和發(fā)布兩個(gè)分支笑陈,需要打包時(shí)發(fā)布分支合并開發(fā)分支代碼)

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末坡锡,一起剝皮案震驚了整個(gè)濱河市鹉勒,隨后出現(xiàn)的幾起案子皮官,更是在濱河造成了極大的恐慌,老刑警劉巖实辑,帶你破解...
    沈念sama閱讀 206,214評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件捺氢,死亡現(xiàn)場離奇詭異,居然都是意外死亡摄乒,警方通過查閱死者的電腦和手機(jī)馍佑,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,307評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來梨水,“玉大人挤茄,你說我怎么就攤上這事”荆” “怎么了?”我有些...
    開封第一講書人閱讀 152,543評(píng)論 0 341
  • 文/不壞的土叔 我叫張陵笼恰,是天一觀的道長踊沸。 經(jīng)常有香客問我,道長社证,這世上最難降的妖魔是什么逼龟? 我笑而不...
    開封第一講書人閱讀 55,221評(píng)論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮追葡,結(jié)果婚禮上腺律,老公的妹妹穿的比我還像新娘。我一直安慰自己宜肉,他們只是感情好匀钧,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,224評(píng)論 5 371
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著谬返,像睡著了一般之斯。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上遣铝,一...
    開封第一講書人閱讀 49,007評(píng)論 1 284
  • 那天佑刷,我揣著相機(jī)與錄音,去河邊找鬼酿炸。 笑死瘫絮,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的填硕。 我是一名探鬼主播麦萤,決...
    沈念sama閱讀 38,313評(píng)論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了频鉴?” 一聲冷哼從身側(cè)響起栓辜,我...
    開封第一講書人閱讀 36,956評(píng)論 0 259
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎垛孔,沒想到半個(gè)月后藕甩,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,441評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡周荐,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,925評(píng)論 2 323
  • 正文 我和宋清朗相戀三年狭莱,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片概作。...
    茶點(diǎn)故事閱讀 38,018評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡腋妙,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出讯榕,到底是詐尸還是另有隱情骤素,我是刑警寧澤,帶...
    沈念sama閱讀 33,685評(píng)論 4 322
  • 正文 年R本政府宣布愚屁,位于F島的核電站济竹,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏霎槐。R本人自食惡果不足惜送浊,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,234評(píng)論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望丘跌。 院中可真熱鬧袭景,春花似錦、人聲如沸闭树。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,240評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽蔼啦。三九已至榆纽,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間捏肢,已是汗流浹背奈籽。 一陣腳步聲響...
    開封第一講書人閱讀 31,464評(píng)論 1 261
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 45,467評(píng)論 2 352
  • 正文 我出身青樓皱炉,卻偏偏與公主長得像涝影,于是被迫代替她去往敵國和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子扭仁,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,762評(píng)論 2 345

推薦閱讀更多精彩內(nèi)容

  • Spring Cloud為開發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見模式的工具(例如配置管理致稀,服務(wù)發(fā)現(xiàn)宗苍,斷路器钻弄,智...
    卡卡羅2017閱讀 134,599評(píng)論 18 139
  • fat-aar.gradle是什么佃却? 在做android應(yīng)用程序開發(fā)時(shí),我們一般都會(huì)構(gòu)建多個(gè)模塊窘俺,來達(dá)到解耦的目的...
    王巖_shang閱讀 16,572評(píng)論 13 23
  • 劉志文老師來的日子饲帅,是我腳傷50多天第一次走出家門的日子。 劉老師從醫(yī)院退休已有三瘤泪、四年了灶泵,豐富的咨...
    丑蘋果閱讀 264評(píng)論 0 0
  • 2015年8月之前实檀,我用的是iPhone4惶洲。 自從換了iPhone6,我發(fā)現(xiàn)它的相機(jī)簡直比4代要好太多了膳犹,而且還自...
    339da1fbd744閱讀 265評(píng)論 0 1
  • 開源大數(shù)據(jù) 1.HadoopHDFS湃鹊、HadoopMapReduce,HBase、Hive 漸次誕生镣奋,早期Hado...
    kuntoria閱讀 200評(píng)論 0 1