好的朋友們,新的一週開始了,讓咱們繼續來學習插件化的知識吧。先回顧一下系列文章架構 java
根據個人行文思路,本篇文章講解資源和App打包的一些知識。算是插件化系列的第二篇基礎文章。閱讀完本文後,你應該會了解:android
資源這一部分將會先從你們的直觀印象切入,逐步的加大深度。而後我會結合前半部分資源的鋪墊講解App的打包流程。你們若是閱讀完之後發現,咦,這一點我還真不知道,那本文也算是有點意義了。由於本篇依然屬於插件化的基礎知識文章,因此仍是不會講到插件化,可是後面講到插件化的時候會引用到本篇文章的部分知識。從另外個角度來講,本篇文章也是一篇知識比較自成一體的文章。OK,那我們開始吧。git
先作一點準備工做,咱們建一個工程,這個工程下面有三個module,App和咱們自建的modulea,moduleb。這三個module的依賴關係是app->modulea->moduleb。而後咱們在每一個module裏面放一點資源,好比string之類的,這裏我在modulea中放了一個String叫testA, 在moduleb中放了一個String叫testB。而後咱們會發如今每一個module的build/generated/source/r(這個文件夾跟gradle版本有關係,3.5之後文件夾有變動)下面出現了R.java文件,這個就是android打包過程當中藉助於aapt工具生成的資源id目錄。github
而後咱們分別打開主模塊和modulea的R.java。下面是主模塊和普通模塊的R.java文件中的id示例。安全
// 主模塊app中的R.java
public static final int testA = 0x7f0b002a;
public static final int testB=0x7f0b002b;
// modulea中的R.java
public static int testA = 0x7f15002b;
public static int testB = 0x7f15002c;
複製代碼
你們能夠看到:微信
爲何會這樣呢?咱們將在後面講解這些內容並在最後給出結論架構
咱們先看看資源Id的組成。你們都知道,資源id是一個資源的惟一標識。那麼問題來了,這麼多的module,這麼多的資源種類,甚至還有Android自帶的資源,資源id爲何不會重複呢?祕訣就在資源id的組成上面。app
packageId: 前兩位是packageId,至關於一個命名空間,主要用來區分不一樣的包空間(不是不一樣的module)。目前來看,在編譯app的時候,至少會遇到兩個包空間:android系統資源包和我們本身的App資源包。你們能夠觀察R.java文件,能夠看到部分是以0x01開頭的,部分是以0x7f開頭的。以0x01開頭的就是系統已經內置的資源id,以0x7f開頭的是我們本身添加的app資源id。框架
typeId:typeId是指資源的類型id,咱們知道android資源有animator、anim、color、drawable、layout,string等等,typeId就是拿來區分不一樣的資源類型。ide
entryId:entryId是指每個資源在其所屬的資源類型中所出現的次序。注意,不一樣類型的資源的Entry ID有多是相同的,可是因爲它們的類型不一樣,咱們仍然能夠經過其資源ID來區別開來。
經過資源id的三個區塊的劃分,在編譯期間,同一個資源在普通的apk中只會屬於一個package,一個type,只擁有一個次序,因此一個資源的id是不會和別的資源重複的。固然這只是正常狀況下,要是咱們有部分資源沒有參與打包呢?好比說咱們要說的插件化,插件化是要下發一個插件,插件中固然也有資源,這部分資源是沒有通過統一的編譯的,那麼就可能存在和宿主(插件要下發到的App)資源衝突的狀況。好比你已經給梁山排好了108將,每一個人都有一個稱號,可是從山下又來了一個「及時雨」宋江,那豈不是同時存在兩個及時雨了,聽誰的呢?梁山就會大亂,app也是如此。
爲了不這種狀況,插件的資源id一般會採用0x02 - 0x7e之間的數值,避免和宿主資源衝突。至於怎麼作到的,等後面的文章再聊~
咱們一般會在編碼的時候使用相似於R.layout.xxxx一類的引用,這些引用就是R.java文件中的字段。而且咱們在主模塊和library模塊中使用這些id的時候,好像並無什麼區別,那麼這二者中的id真的是毫無區別嗎?
咱們先看看在主模塊中和庫模塊中分別去使用id的區別。咱們分別在app模塊和modulea模塊中分別建一個Activity。每一個Activity中有一段這樣的代碼,你們應該都比較熟悉
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
}
複製代碼
而後咱們點擊Android Studio的Tools->kotlin->show kotlin bytecode直接看這個類的字節碼。固然直接看字節碼仍是比較難,咱們再點面板上的decompile,把它解析成java代碼。而後咱們就會發現,有點細微的區別。
// 主模塊的代碼
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
this.setContentView(-1300009);
}
// 庫模塊的代碼
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
this.setContentView(layout.activity_module_a);
}
複製代碼
你們能夠看到,主模塊中的R.layout.xxx徹底是做爲常量,直接內聯進了代碼中。而庫模塊中的R.layout.xxx, 依然是做爲變量引用到了代碼中。這個規律在編譯期間也是存在的。這個規律和前面對R.java中的字段的處理是一致的,也便是說,
一般apk中的資源來源主要是3個,具體能夠參考官網:
一個資源一般會使用它的文件名做爲標識,也就是說,相同resource type(anim/drawable/string等)和相同resource qualifier(好比hdpi, value中的語言等)下相同文件名的資源,系統會認爲他是惟一的。那麼單一module下可能就會有相同的資源存在,好比有多個主資源集。那麼當出現這種衝突的狀況的時候,系統會怎麼處理呢?系統會進行合併,低優先級的資源會被覆蓋掉。
覆蓋的優先級以下: build variant > build type > product flavor > main source set > library dependences
舉個栗子,若是咱們主資源集下有兩個資源: res/layout/a.xml, res/layout/b.xml, build type文件夾下面有res/layout/a.xml。那麼最後打包生成的apk中的res/layout/a.xml來自於build type, res/layout/b.xml來自於main source set。
除了單一module不一樣文件夾下的資源覆蓋,不一樣module間也會有資源覆蓋。好比app模塊依賴了modulea,兩個module中都有一個資源文件res/layout/a.xml,那麼最後編譯的apk中的res/layout/a.xml必定是app模塊下定義的那個。
資源合併有什麼實際意義呢?我我的認爲經過資源合併能夠實現更高級別的自適應打包。好比說,咱們能夠爲不一樣的product flavor去設置不一樣的資源,好比頁面xml,這樣,只要改一下product flavor就能打出不同的包,實現更高級別的自適應。
上面講了資源id的一些機制,接下來咱們來探討一下R文件的生成機制。這裏的規律是基於gradle 3.1.2
。
首先咱們先看一下數量上的規律,仍是以咱們上面的例子爲例。三個module的依賴關係是app->modulea->moduleb。modulea中有個string叫testA,moduleb中有個string叫testB。最後咱們發現app模塊下面有三個R文件。
而且發現plugindemo(也便是App模塊)下面的R文件裏包含了咱們在modulea和moduleb中定義的string。
經過上面的例子能夠給出結論,用一個圖能夠說明。
1.數量的規律:一個module被編譯的時候,會生成當前module的R文件,而且該module依賴的module或者aar也會在當前module生成R文件。這種依賴關係不一樣於gradle裏面的implementation依賴傳遞,implementation是跨級不能傳遞,可是R文件的生成是跨級能夠傳遞的。因此, module的R文件數 = 依賴的module/aar數量 + 1(自身的R文件)
舉個例子,A模塊依賴了B模塊,同時也依賴了fresco,那麼他生成的R文件有幾個呢?答案是三個,B模塊,fresco,和自身的R文件。
2.生成順序的規律,三個模塊的依賴關係是app->modulea->moduleb。生成R文件的順序是從底層到上層,逐層生成。也就是說先生成moduleb的,再生成modulea的,再生成app模塊的。
3.資源的規律:上層模塊會把所依賴的模塊的R文件merge進去。好比app模塊並無testA和testB這兩個string,可是app的R文件卻包含了這兩個資源的id。這就是由於上層的模塊把下層模塊的資源給merge進去了。
講完了這些規律,咱們就能夠回答小節一開頭提出的三個問題了。
1.爲何資源id都以0x7f開頭?
由於這些資源都是應用包的資源,統一是0x7f開頭
2.爲何主模塊(application module)資源有final修飾,非主模塊(library module)都不是final的?
比較早的aapt的版本生成的非主模塊的資源id確實都是final修飾的,這樣會帶來一個問題,這些資源id所有內聯到代碼中,一旦新增或者刪除,修改了資源,資源id就會有變化,全部的代碼都須要從新編譯,形成嚴重的編譯耗時。後來改成主模塊final常量方式內聯,非主模塊引用方式,這樣等按照從下到上編譯到App模塊的時候,全部的資源id都已經肯定了,底層模塊的資源只須要經過引用就能拿到本身對應的id,而修改(新增,刪除,修改)了資源以後,也只須要從新生成R文件就行了。編譯耗時大大減小。
3.爲何同一個資源,不一樣模塊產生的R.java中的資源id值是不統一的?
由於資源id只是表示資源的次序,而不是別的跟資源自己綁定的屬性。當到了不一樣的模塊之後,參與編譯的資源變多了,那次序確定會改變。資源id也就改變了。而且子模塊的資源id只是引用形式存在於代碼中,id具體是什麼值並非很care。
不知道你們看完這些,有沒有什麼收穫呢?
不知道你們有沒有用過ButterKnife這個依賴注入框架,這個框架最核心的使用場景就是使用註解進行依賴注入。好比
@BindView(R.id.user) EditText username;
複製代碼
你們應該常見這種用法,那麼,這裏有沒有什麼玄機呢?咱們上面講到了,非主模塊中資源id是變量,沒有final修飾。可是註解你們都知道,傳入的參數必須是final常量。這樣的話豈不是相悖了嗎?
其實上面的兩個結論都沒有錯。Butterknife針對這種狀況作了一個騷處理。他直接copy了一份module中的R.java,搞了個R2.java,把R.java中全部的資源id所有改成final的,這樣就能在註解中使用了。等到真正使用的時候,再進行替換,使用真正的主模塊的生成的資源id。
具體能夠參考R.java、R2.java 是時候懂了
打包流程這一塊我會先講述基本流程,而後會補充一些關於打包流程的應用的擴展知識。
先來一張打包流程圖。
1.打包資源文件,生成R.java文件 這一過程主要是aapt對res和asset文件夾,AndroidManifest.xml,android庫(aar,jar)等的資源文件進行處理。先檢查AndroidManifest.xml的合法性,而後編譯res與asserts目錄下的資源並生成resource.arsc文件,再生成R文件。除了assets和res/raw資源被原封不動地打包進APK以外,其它的資源都會被編譯或者處理,大部分文本格式的XML資源文件會被編譯成二進制格式的XML資源文件。除了assets資源以外,其餘的資源都會在R文件中被賦予一個資源ID。也就是說,R文件中只會存在id,真正的資源存在於resource.arsc中,resource.arsc至關於一個資源索引表,資源id是key,value是資源路徑。咱們使用drawable-xdpi或者drawable-xxdpi這些不一樣分辨率的圖片的時候,就是依靠resource.arsc根據設備的分辨率選擇不一樣的圖片。
2.處理aidl文件,生成相應的.java文件
這一步就是咱們代碼中的aidl的文件被生成java文件。
3.編譯工程源碼,生成相應的class文件 R文件,aidl生成的java文件和咱們工程中的源代碼被javac工具編譯成了class文件。
4.轉換全部的class文件,生成classes.dex文件
Android系統的dalvik虛擬機的可執行文件爲dex格式,程序運行所需的classes.dex文件就是在這一步生成的,使用的工具爲dx,dx工具主要的工做是將java字節碼轉換爲dalvik字節碼、壓縮常量池、消除冗餘信息等。 這裏在生成dex的時候,就會遇到65536的問題。一個DEX文件中的method個數採用使用原生類型short來索引文件的方法,也就是4個字節共計最多表達65536個method。因此當method數過多的時候,就必須使用multidex。
5.打包生成apk
把全部的dex文件打包爲一個apk文件。
6.對apk文件進行簽名 apk須要簽名才能在手機上安裝。平時咱們測試主要是使用了一個debug.keystore對apk進行簽名。正式發佈時須要提供一個符合android開發文檔中要求的簽名文件。好比jarsigner和APK Signature Scheme v2。
7.對簽名後的apk進行對齊處理 一步須要使用的工具爲zipalign,它位於android-sdk/tools目錄,源碼位於android系統資源的build/tools/zipalign目錄,它的主要工做是將apk包進行對齊處理,使apk包中的全部資源文件舉例文件起始偏移爲4字節的整數倍,這樣經過內存映射訪問apk時的速度會更快。爲何快呢?若是每一個資源的開始位置上都是一個資源以後的4n字節,那麼訪問下一個資源就不用遍歷,直接跳到4字節以後便可。
8.混淆proguard:proguard主要的目的是混淆代碼,保護應用源代碼。次要的功能還有移除無用類等,優化字節碼,縮小包體積。
1.資源去重,極致縮包
前面咱們講到了proguard的功能是混淆代碼和縮減體積。可是proguard是不能處理資源文件的。爲了解決資源文件的混淆問題,微信推出了AndResGuard。使用AndResGuard能夠更加縮減包體積。
除了AndResGuard以外,咱們還會遇到資源被重複使用的問題,識別重複資源很簡單,只要計算一下md5就好了。而且咱們在resources.arsc中能夠拿到全部的資源,那麼咱們就能夠對resources.arsc中的全部資源進行處理,根據md5進行去重,把使用了相同資源的資源id都指向同一個資源,把多餘的資源刪除掉,再回寫入resources.arsc就行了。固然,這裏面仍是有挺多學問的。
2.Transform
Transform是Android gradle plugin提供給開發者的一套API,容許開發者在編譯以後,dex以前對class進行修改。開發者能夠經過AppExtension或者LibraryExtension進行註冊Transform。多個transform會造成一條鏈。上一個Transform的輸出是下一個Transform的輸入,所以,Transform的順序也很重要。
既然有了這個Transform,就意味着咱們有機會去操做java的字節碼。網上常見的處理字節碼的框架有AspectJ, Javasist, ASM。能夠利用這些工具進行字節碼插樁。這樣能夠把一些不能耦合在業務代碼中的代碼在字節碼階段給merge進去。
3.多渠道打包
Android和iOS不同的是市場和渠道衆多,爲了區分和統計不一樣的渠道包的效果,須要有一種方法來標記他們。你們可能會想到使用productFlavor,可是這樣的話要打多少包就須要build多少次,耗時很是長。
如今比較好的方案是在apk進行v2簽名的時候在簽名塊中寫入一些信息,這樣更快更安全。詳情能夠參考Android美團多渠道打包Walle集成
本小節講解了打包過程,和利用打包機制能夠作的一些技術點。打包過程若是能學的透徹的話,仍是能給android開發帶來不少的可能性。
參考文章:
羅昇陽 Android應用程序資源的編譯和打包過程分析
我是Android笨鳥之旅,一個陪着你慢慢變強的公衆號,歡迎關注我一塊兒學習,一塊兒進步哈~