android aar保存图片文件异常_我去!合并AAR时踩坑了!
點擊上方“劉望舒”,馬上關注,早上8:42推送
真愛,請置頂或星標
作者: leeon7
https://www.jianshu.com/p/8f7e32015836
背景
在輸出Android模塊時,有時會因為個別原因(比如來自業務的不可抗力),要求將模塊打包成一個文件提供給接入方。這就意味著在輸出模塊由多個子模塊組成的情況下,我們需要把多個AAR(或JAR)合并成一個大AAR輸出,這個合并過程涉及到了很多有用的知識和難點,這篇文章就詳細解析下其中的內容。
首先來直觀認識下AAR
AAR文件是一種Android歸檔包(類比Jar:Java Archive),這種歸檔包是由Gradle構建庫的Android Library插件產出的。它是一個壓縮包,里面的內容可以總結為5個目錄和5個文件:
賣個關子,在上面10個內容中,其中有一個是已經合并后的結果,它默認已經包含了所有子模塊的內容,你能猜出來是那個么?
圖里文字已經基本解釋清楚了各個內容,不再贅述,現在我們根據現有的了解設想下合并AAR的大致思路:AAR里無非是幾個目錄和文件,所以最終AAR的5個目錄下,必然包含了所有子模塊的對應內容,而5個文件也肯定是各個子文件合并的結果。那么如何實現包含和合并呢?
第一反應是寫個腳本對AAR們做解壓、整合,再壓縮的思路,但稍微推演下就知道不現實(理論上也不可行,AAR和普通壓縮文件還是有區別的),再思考下,既然直接拿產物做合并不可行,能不能在構建主模塊AAR時,就順便將子模塊內容納入進來呢?
這無疑是個優雅的思路,理論上是否可行呢?答案是可以的,別忘了AAR是Android Gradle Plugin構建產出的,而Gradle強大的拓展支持剛好能實現我們的需求。所以合并AAR的方法,其實就是修改Gradle構建流程,在默認構建的基礎上,插入我們自己的操作,最終產出一個包含子模塊內容的大AAR。
在入題之前,有必要先理解下Gradle是如何支持構建拓展的,下圖是Gradle官網截圖:
Gradle中定義了Project和Task兩個概念,同時也將構建流程面向對象化(即構建是針對Project執行的一系列Task)。對Gradle的描述關鍵在于基于依賴編程,具體解釋就是可以用Gradle來定義Task和Task間的依賴關系,最終得出一個Task的有向無環圖來描述構建。
這意味著,我們需要將合并的操作封裝為Task,并在合適的生命周期內,將自定義Task插入到任務圖的依賴關系中去,進而得出一個新的Task圖,最終構建出我們要的產物。
我們在主工程下執行./gradlew assembleRelease,看一下默認構建中都執行了哪些Task:
可以看到依次執行了收集依賴,合并資源、Javac編譯、打包文件等Task。同時,通過觀察/build/intermediates目錄可以發現,不同任務會產出各自的構建中間結果,最終的AAR就是由這些中間結果打包而來的。所以理論上,我們需要在主模塊執行打包Task前將子模塊內容混入到中間結果中,而由于構建Task間存在依賴關系,混入操作需要分階段、找時機執行。
合并前需要先確定到底需要合并哪些子模塊。我們通過定義一個dependency configuration: emeded來標記需要被合并的模塊,然后在gradle構建的afterEvaluate階段收集被emeded依賴的模塊信息:
afterEvaluate?{????def?dependencies?=?new?ArrayList(configurations.embedded.resolvedConfiguration.firstLevelModuleDependencies)
????dependencies.reverseEach?{
????????...
????????it.moduleArtifacts.each?{
????????????artifact?->
????????????????if?(artifact.type?==?'aar')?{
????????????????????if?(!embeddedAarFiles.contains(artifact))?{
????????????????????????//要合并的AAR文件
????????????????????????embeddedAarFiles.add(artifact)
????????????????????}
????????????????????if?(!embeddedAarDirs.contains(modulePath))?{
????????????????????????...
????????????????????????//每個AAR的解壓目錄
????????????????????????embeddedAarDirs.add(modulePath)
????????????????????}
????????????????}?else?if?(artifact.type?==?'jar')?{
????????????????????...
????????????????????//要合并的JAR文件
????????????????????embeddedJars.add(artifactPath)
????????????????}?
????????????????...
????????}
????}
}
可以看出,我們收集了三個集合:
要合并的AAR文件
每個AAR的解壓目錄
要合并的JAR文件
AAR和JAR分開收集是因為合并這兩種文件的操作不同,JAR只需納入將其Class文件,而AAR需要合并更多內容。
下面針對AAR中的5和目錄和5個文件,逐個介紹合并Task,從最簡單的開始:
/assets目錄
合并assets內容的Task很簡單,只需將子模塊解壓后的assets目錄添加到主工程的assets.srcDirs中即可
task?embedAssets?<????embeddedAarDirs.each?{?aarPath?->????????android.sourceSets.main.assets.srcDirs?+=?file("$aarPath/assets")
????}
}
/res目錄
通過conventionMapping,修改構建流程task packageRecourses的輸入集合,將收集到的子模塊/res目錄追加到Task輸入參數中
task?embedLibraryResources?<????def?oldInputResourceSet?=?task_packageResources.inputResourceSets????task_packageResources.conventionMapping.map("inputResourceSets")?{
????????getMergedInputResourceSets(oldInputResourceSet)
????}
}
private?List?getMergedInputResourceSets(List?inputResourceSet)?{
????...
????List?newInputResourceSet?=?new?ArrayList(inputResourceSet)
????embeddedAarDirs.each?{?aarPath?->
????????...
????????rs.addSource(file("$aarPath/res"))
????????newInputResourceSet?+=?rs
????}
????return?newInputResourceSet
}
/jni目錄
遍歷子模塊解壓目錄,將其中的so文件copy到主工程build目錄下
task?embedJniLibs?<????embeddedAarDirs.each?{?aarPath?->????????copy?{
????????????from?fileTree(dir:?"$aarPath/jni")
????????????into?file("$bundle_release_dir/jni")
????????}
????}
}
proguard.txt
注意:這個文件與AAR構建自身時的混淆操作無關,只作用于接入到宿主后的混淆操作
task?embedProguard?<????//bundle_release_dir?=?"build/intermediates/bundles/release"????def?proguardFile?=?file("$bundle_release_dir/proguard.txt")
????//遍歷aar解壓目錄,將子模塊的proguard.txt文件內容追加到主工程build目錄內的proguard.txt中
????embeddedAarDirs.each?{?aarPath?->
?????????...
?????????def?proguardLibFile?=?file("$aarPath/proguard.txt")
?????????if?(proguardLibFile.exists())
????????????//調用file.append()方法可以在txt文件里追加內容
????????????proguardFile.append("\n"?+?proguardLibFile.text)
?????????...
????}
}
AndroidManifest.xml
合并AndroidManifest使用官方庫manifest-merger即可,這里遇到了第一個難點:想要把子模塊AndroidManifest合并進來必須使用MergeType為APPLICATION的ManifestMerger對象,但在這種MergeType下會替換定義在AndroidManifest里的PlaceHolder,比如${applicationId}。
然而矛盾的是,此時的構建還沒有接入到宿主App,也就拿不到最終的ApplicationId,怎么辦呢?通過閱讀源碼發現,官方仁慈的給我們開放了一個功能位,通過在初始化ManifestMerger對象時傳入一個NO_PLACEHOLDER_REPLACEMENT功能位,即可禁止在APPLICATION模式下替換PlaceHolder。
buildscript?{????repositories?{
????????jcenter()
????}
????dependencies?{
????????classpath?'com.android.tools.build:manifest-merger:25.3.2'
????}
}
...
task?embedManifests?<??...
??embeddedAarDirs.each?{?aarPath?->
??????File?dependencyManifest?=?file("$aarPath/AndroidManifest.xml")
??????if?(!libraryManifests.contains(aarPath)?&&?dependencyManifest.exists())?{
??????????//先收集需要合并的子模塊AndroidManifest
??????????libraryManifests.add(dependencyManifest)
??????}
??}
??...
??Invoker?manifestMergerInvoker?=?ManifestMerger2.newMerger(origManifest,?mLogger,?MergeType.APPLICATION)
????????//通過Invoker.Feature.NO_PLACEHOLDER_REPLACEMENT這個Flag禁止執行PlaceHolder替換
????????.withFeatures(Invoker.Feature.NO_PLACEHOLDER_REPLACEMENT)
??manifestMergerInvoker.addLibraryManifests(libraryManifests.toArray(new?File[libraryManifests.size()]))
??manifestMergerInvoker.setMergeReportFile(reportFile);
??//執行合并
??MergingReport?mergingReport?=?manifestMergerInvoker.merge();
??...
}
classes.jar
這似乎是最重要的文件,但是合并方法卻不難:對于AAR類型的子模塊,我們需要提取兩樣東西:一是解壓子模塊內部classes.jar得到的Class文件,二是看看子模塊內部有沒有自身攜帶的JAR文件。對于Class文件,將它們copy到主工程build/intermediates/classes/release目錄下,隨后的構建任務會從這個目錄打包出主模塊的classes.jar;對于子模塊內部的JAR文件,將它們和JAR類型的子模塊一起,放入主工程的build/intermediates/bundles/release/libs目錄下,作為依賴包攜帶即可
task?embedClassesAndJars(dependsOn:?embedRJar)?<{????embeddedAarDirs.each?{?aarPath?->
????????...
????????embeddedAarFiles.each?{
????????????????artifact?->
????????????????????...//找到每個aar里的clasess.jar
????????????????????def?aarFile?=?aarFileTree.files.find?{?it.name.contains("classes.jar")?}//?解壓clasess.jar,將classes放入build/intermediates/classes/release
????????????????????copy?{from?zipTree(aarFile)
????????????????????????into?classes_dir
????????????????????}
????????????}
????????...//找到每個aar里攜帶的額外jar包
????????FileTree?jars?=?fileTree(dir:?jar_dir,?include:?'*.jar',?exclude:?'classes.jar')
????????...//放入build/intermediates/bundles/release/libs
????????copy?{from?jars
????????????into?file("$bundle_release_dir/libs")
????????}
????}//主工程直接依賴的jar包,也放入build/intermediates/bundles/release/libs
????copy?{from?embeddedJars
????????into?file("$bundle_release_dir/libs")
????}
}
小節
至此,我們已經合并了三個文件和四個目錄:
剩下的內容里,/aidl目錄和annotation.zip不常用到,我們暫不理會,乍一看好像已經合并完成了,只剩下個看似無害的R.txt,它是干嘛用的?我們當真完成了么?
答案是沒有,假如按照目前的改造構建出一個AAR并接入到App中運行,結果是App會在每一處使用到AAR子模塊資源文件的時候崩潰,堆棧日志很簡單:NoClassDefFoundError?- 沒有子模塊包名下的R文件。啊~忘了R文件這個東西了!可R文件不是構建時自動生成的么?沒錯,是自動生成的,但由于我們合并了模塊,子模塊將丟失它們的R文件,仔細梳理下App構建流程就會發現原因:
如圖上部所示,正常情況下,不管是主模塊還是子模塊,都是作為一個個獨立的dependence引入到宿主工程的,App構建時會給每一個dependence生成它們包名下的R文件;而當我們自主的將一群AAR合并為一個AAR后(圖下半部),對于宿主來說最后只接入了一個dependence(主模塊),子模塊本該對應的dependences不存在了,所以App構建時只給主模塊生成了R文件!知道原因了,怎么解決呢?
我們知道,構建主模塊時,會在/build/generated/source/r目錄下生成所有包名的R文件,當然也包括子模塊R文件,那么我們把這個目錄下的子模塊R文件打成Jar包,放入主工程build/intermediates/bundles/release/libs目錄下,這樣R.jar會作為依賴庫被AAR攜帶,不就可以了嗎?我們興沖沖地改了腳本,再次構建運行,結果還是崩潰!看下堆棧信息,提示資源找不到:Resources$NotFoundException,這又是為什么?R文件可是自動生成的呀?為什么生成的文件內容(也就是資源ID)找不到對應資源?
在解釋原因之前,先拋幾個問題:
AAR里默認有R文件么?
為什么Android Library工程生成的R.java里的域不是final修飾的?
生成R文件流程的輸出無疑是R.java,那么輸入是什么?
在回答這些問題之前,先復習下Java基礎知識,下面是一段簡單的Java代碼:
public?class?Test?{????private?static?final?int?SOME_ID?=?0x07111111;
????private?int?getID(){
????????return?SOME_ID;
????}
}
假如我們將SOME_ID的final修飾符去掉,那么編譯后的字節碼跟不去掉final相比,會有什么不同呢?我們反編譯看下結果:
//帶final修飾符public?class?Test?{
????private?static?final?int?SOME_ID?=?118558993;
????public?Test()?{
????}
????private?int?getID()?{
????????return?118558993;
????}
}
//不帶final修飾符
public?class?Test?{
????private?static?int?SOME_ID?=?118558993;
????public?Test()?{
????}
????private?int?getID()?{
????????return?SOME_ID;
????}
}
對比發現,帶final時,getID()方法直接返回了數值;而不帶final時,getID()方法返回的是SOME_ID,即依然保留著對變量的符號引用。這個看似不起眼的差別卻暗藏玄機,看下圖:
如圖,每個AAR被接入后,都會跟隨App工程再經歷一次構建,即宿主工程的構建。而我們知道,R文件是跟隨每次構建重新生成的(不管是AAR的構建還是App的構建),而R.java中每個域的值是由當前工程的資源集合做排列得出的,這意味著,假如工程的資源集合發生了變化,那么R.java中域的值都可能發生變化。
對于AAR文件來說,自身工程的資源集合必然和宿主工程的資源集合不一樣,或者可以這樣理解,R.java的值是一次性的,它只保證在當前構建結果下有效,這次生成的R文件,不保證在下次構建后可用,這也是每次構建都重新生成R文件的原因。這樣我們就找到了上面的崩潰原因和拋出的前兩個問題的答案:AAR接入到App后跟隨App又經歷了一次構建,資源發生了重排列,所以手動打到AAR中的R文件ID值全部失效,無法再索引到資源,所以運行時崩潰。
AAR中默認沒有R文件,因為帶上也完全不能用。同樣因為重排列,AAR無法預知自身資源接入到App后的ID值,所以Library工程生成的R.java中的域都不能用final修飾。
*:這里的邏輯很刁鉆,但卻很重要,是理解后續操作的前提,務必要反復品味...
那要怎么辦呢?其實關鍵在主模塊身上:合并子模塊后,主模塊成了所有子模塊的代言人,App只需接入主模塊就等于接入了所有模塊。能力越大責任越大,我們賦予了主模塊如此特殊的地位,那么它也應該起到足夠特殊的作用,比如橋接子模塊到宿主的資源索引:
//com.sub.librarypublic?final?class?R?{
??public?static?final?class?string?{
????public?static?int?some_string?=?com.main.library.R.string.some_string;
????}
}
如代碼所示,我們在把子模塊R文件放入classes.jar之前,手動將其中的域指向主模塊R文件相同的域(正是因為這里沒有final修飾符,才能保留對主模塊域的符號引用,編譯后才不會變成數值而無法被更改),因為主模塊R文件會跟隨每一次App構建而重新生成,所以它的ID值總是新鮮可靠的,這樣子模塊就可以通過這個橋接拿到正確的資源索引了。這時你可能會拍案而起,不對啊,主模塊沒有子模塊里的資源文件,為什么R文件中會有跟子模塊相同的域呢?!
這其實就是上面提到的第三個問題:生成R文件流程的輸出無疑是R.java,那么輸入是什么?這個問題也牽扯到本文剛開始賣的關子:那個默認包含子模塊內容的文件是誰?沒錯,就是R.txt(這個看似老實的東西其實壞的很 XD)。
我們知道,APK或AAR構建時會用Aapt編譯資源文件,這個R.txt正是Aapt的產物,通過在aapt package命令后面跟上--output-text-symbols參數得到。R.txt中會記錄下-S參數傳入的資源目錄下所有的資源文件信息,一個資源占一行,格式為int type name id,比如:?int string some_string 0x7f050000(和R文件格式一致,所以猜到了吧,R.txt就是R.java的種子)。
在AAR的構建中,R.txt自然記錄了Library庫內的資源文件信息,不過一個很不自然的事情是,構建時aapt package命令的-S參數傳入的不是本工程/src/res目錄,而是build/intermediates/res/merged/release目錄,也就是合并子模塊資源后的/res目錄!所以,默認生成的R.txt中就已經包含了所有子模塊的資源文件,而這個R.txt恰恰就是構建生成R.java的種子文件,腳本會根據R.txt中每一行的內容生成R.java中對應的域。這也就解釋了為什么主模塊的R.java會包含所有子模塊的資源索引。終于,我們知道了如何處理R文件:
task?redirectRJava?<??...??embeddedAarDirs.each?{?aarPath?->
??????...
??????def?sb?=?"package?$subPackageName;"?<'\n'?<'\n'
???????//將ID賦值為主模塊包名下對應的值
??????sb?<"????public?static?$type?$name?=?${mainPackageName}.R.${subclass}.${name};"?<'\n'
??????...
??????//generated_rsrc_dir?=?"build/generated/source/r/release"
??????mkdir("$generated_rsrc_dir/$packagePath")
??????file("$generated_rsrc_dir/$packagePath/R.java").write(sb.toString())
??}
}
//將R文件打成Jar包放入libs目錄
task?embedRClass(type:?org.gradle.jvm.tasks.Jar,?dependsOn:?collectRClass)?{
????...
????baseName?"EmbedR"
????destinationDir?file("$bundle_release_dir/libs")
????from?base_r2x_dir
}
解決完R文件的難題后,我們就基本完成了AAR的合并工作,但是在實際業務中,會遇到另一個棘手的問題:Support庫兼容問題。上面已經分析過,一個庫的R文件中會包含它依賴的所有子模塊的資源文件(還記得原因么),假如我們的模塊依賴了Support庫(實際業務中很常見,比如用到了v4的Fragment或者RecyclerView),那么R文件或R.txt中就會包含Support庫內所有資源文件的信息,而當模塊被接入后,在App的構建流程中,會根據最終的Support庫版本(App依賴的Support庫版本不可知)對各個R.txt里的內容做過濾,只保留App工程中確實存在的資源文件,生成最終的R文件(源代碼見com.android.builder.symbols.RGeneration)。
舉個例子,假如模塊使用的A版本的Support庫中有資源文件res1,而接入方App使用了更高版本的Support庫中沒有res1這個資源,那么App構建生成的R文件中將沒有res1這個資源索引,可是我們打入AAR中的R文件還保留著對res1的符號引用,結果就是在運行時類初始化失敗(R.class的錯誤):
public?final?class?R?{??public?static?final?class?string?{
????//這里的com.main.library.R.string.res1被過濾掉不再存在,符號引用失效
????public?static?int?res1?=?com.main.library.R.string.res1;
????}
}
怎么辦呢?第一反應是保證support庫版本一致(必須精確到小版本號),即針對每個接入方構建出跟其Support庫一致的產物,這就帶來了很多隱藏成本(還需要手動做R.txt的過濾),更何況假如接入方某天更新了Support庫版本,直接就Runtime Crash了,這屬于對接事故,顯然不能接受。那么,應該怎么做呢?其實很簡單:禁止Support庫資源寫入R.txt即可(其實官方也明確提示過不要自己使用Support庫里的資源),還記得R.txt是根據什么生成的么?
沒錯,是build/intermediates/res/merged/release目錄下的資源合集,而這些文件又是根據遍歷依賴合并進來的,假如我們能在合并時過濾掉Support庫的依賴項,就在源頭上過濾掉了Support庫資源。所以我們只需要對合并Resources的Task稍做手腳:
task?filterMergeResource?<????def?oldInputResourceSet?=?task_mergeResources.inputResourceSets????List?newInputResourceSet?=?new?ArrayList(oldInputResourceSet)
????newInputResourceSet.removeAll?{//過濾com.android.support包名下的依賴,避免support庫資源打入R.txt
????????(null?!=?it.libraryName)?&&?(it.libraryName.contains("com.android.support"))
????}
????task_mergeResources.conventionMapping.map("inputResourceSets")?{
????????newInputResourceSet
????}
}
總結
最終我們插入了以上幾個自定義任務到不同的構建節點中,完成了合并AAR的構建改造,但仍然有拓展空間,比如支持buildType和productFlavor,有興趣的同學可以自己嘗試下(其實就是找到對應的build目錄)。
---------? END? ----------
Android領域數一數二的圈子,給自己投資365天的學習吧!
想了解詳情可以看這篇文章:
今天,我需要你的支持!
你好,我是劉望舒,十年經驗的資深架構師,著有兩本業界知名的技術暢銷書,多個知名技術大會的特邀演講嘉賓。
如果你喜歡我的文章,就給公眾號加個星標吧,方便閱讀。
??在看也是一種認可
總結
以上是生活随笔為你收集整理的android aar保存图片文件异常_我去!合并AAR时踩坑了!的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 老鼠走迷宫php算法,C语言经典算法 -
- 下一篇: 层次分析法AHP - 代码注释多 -