您好,登錄后才能下訂單哦!
小編給大家分享一下Android熱修復技術原理之資源熱修復技術的示例分析,相信大部分人都還不怎么了解,因此分享這篇文章給大家參考一下,希望大家閱讀完這篇文章后大有收獲,下面讓我們一起去了解一下吧!
目前市面上的很多資源熱修復方案基本上都是參考了 Instant Run的實現(xiàn)。
簡要說來,Instant Run中的資源熱修復分為兩步:
1.構造一個新的 AssetManager,并通過反射調(diào)用 addAssetPath,把這個完 整的新資源包加入到AssetManager中。這樣就得到了一個含有所有新資源的 AssetManager。
2.找到所有之前引用到原有 AssetManager的地方,通過反射,把引用處替換 為 AssetManager。
一個 Android 進程只包含一個 ResTable, ResTable 的成員變量 mPackageGroups 就是所有解析過的資源包的集合。任何一個資源包中都含有 resources.arsc,它記錄了所有資源的id分配情況以及資源中的所有字符串。這些信息是以二進制方式存儲的。底層的AssetManager做的事就是解析這個文件,然后把相關信息存儲到 mPackageGroups 里面。
整個 resources.arse 文件,實際上是由一個個 ResChunk (以下簡稱 chunk) 拼接起來的。從文件頭開始,每個 chunk 的頭部都是一個 ResChunk_header結構,它指示了這個chunk的大小和數(shù)據(jù)類型。
通過ResChunk_header中的type成員,可以知道這個chunk是什么類型, 從而就可以知道應該如何解析這個chunko
解析完一個 chunk 后,從這個 chunk + size的位置開始,就可以得到下一個 chunk 起始位置,這樣就可以依次讀取完整個文件的數(shù)據(jù)內(nèi)容。
一般來說,一個 resources.arsc 里面包含若干個package,不過默認情況下, 由打包工具aapt 打出來的包只有一個 package。這個 package里包含了 app中的 所有資源信息。
資源信息主要是指每個資源的名稱以及它對應的編號。我們知道,Android中的每個資源,都有它唯一的編號。編號是一個 32 位數(shù)字,用十六進制來表示就是0xPPTTEEEE。PP 為 package id, TT 為 type id, EEEE 為 entry id。
它們代表什么?在 resources.arse 里是以怎樣的方式記錄的呢?
對于 package id,每個 package 對應的是類型為 RES_TABLE_PACKAG E_ TYPE 的 ResTable_package 結構體,ResTable_package 結構體的 id 成員變量就表示它的 package id。
對于 type id,每個type對應的是類型為 RES_TABLE_TYPE_SPEC_ TYPE 的 ResTable_typeSpec 結構體。它的id成員變量就是type id。但是,該type id 具體對應什么類型,是需要到package chunk 里的 Type String Pool 中去解析得到的。比如 Type String Pool 中依次有 attr、 drawablex mipmap、layout 字符串。就表示 attr 類型的 type id 為 1, drawable 類型的 type id 為 2, mipmap 類型的 type id 為 3, layout 類型的type id 為 4。所以,每個 type id對應了 Type String Pool里的字符順序 所指定的類型。
對于 entry id,每個 entry表示一個資源項,資源項是按照排列的先后順序 自動被標機編號的。也就是說,一個type里按位置出現(xiàn)的第一個資源項,其 entry id 為0x0000,第二個為 0x0001,以此類推。因此我們是無法直接指定entry id的,只能夠根據(jù)排布順序決定。資源項之間是緊密排布的,沒有空隙,但是可以指定資源項為ResTable_type::NO_ENTRY來填入一個空資源。
舉個例子,我們隨便找個帶資源的 apk,用 aapt解析一下,看到其中的一行是:
$ aapt d resources app-debug.apk
......
spec resource 0x7f040019 com.taobao.patch.demo:layout/activity_main: flags=0x00000000
......
這就表示,activity_main.xml 這個資源的編號是 0x7f040019。它的 package id 是 0x7f,資源類型的id為0x04, Type String Pool里的第四個字符串正是 layout 類型,而 0x04 類型的第 0x0019 個資源項就是 activity_main 這個資源。
默認由 Android SDK 編出來的 apk,是由 aapt 具進行打包的,其資源包的 package id 就是 0x7f。
系統(tǒng)的資源包,也就是 framework-res.jar, package id 為 0x01。
在走到 app的第一行代碼之前,系統(tǒng)就已經(jīng)幫我們構造好一個已經(jīng)添加了安裝包資源的 AssetManager 了。
因此,這個 AssetManager里就已經(jīng)包含了系統(tǒng)資源包以及 app的安裝包,就是 package id 為 0x01 的 framework-res.jar 中的資源和 package id 為 0x7f 的 app 安裝包資源。
如果此時直接在原有 AssetManager 上繼續(xù) addAssetPath的完整補丁包的 話,由于補丁包里面的package id 也是 0x7f,就會使得同一個 package id的包被 加載兩次。這會有怎樣的問題呢?
在 Android L 之后,這是沒問題的,他會默默地把后來的包添加到之前的包的同—個 PackageGroup 下面。
而在解析的時候,會與之前的包比較同一個 type id所對應的類型,如果該類型 下的資源項數(shù)目和之前添加過的不一致,會打出一條warning log,但是仍舊加入到該類型的TypeList 中。
在獲取某個 Type的資源時,會從前往后遍歷,也就是說先得到原有安裝包里 的資源,除非后面的資源的config比前面的更詳細才會發(fā)生覆蓋。而對于同一個 config 而言,補丁中的資源就永遠無法生效了。所以在 Android L以上的版本,在原有AssetManager 上加入補丁包,是沒有任何作用的,補丁中的資源無法生效。
而在 Android 4.4 及以下版本,addAssetPath只是把補丁包的路徑添加到 了 mAssetPath中,而真正解析的資源包的邏輯是在app第一次執(zhí)行 AssetManager::getResTable 的時候。
而在執(zhí)行到加載補丁代碼的時候,getResTable已經(jīng)執(zhí)行過了無數(shù)次了。這是因為就算我們之前沒做過任何資源相關操作,Android framework里的代碼也會多 次調(diào)用到那里。所以,以后即使是addAssetPath,也只是添加到了 mAssetPath, 并不會發(fā)生解析。所以補丁包里面的資源是完全不生效的!
所以,像 Instant Run 這種方案,一定需要一個全新的 AssetManager時,然后再加入完整的新資源包,替換掉原有的AssetManager。
而一個好的資源熱修復方案是怎樣的呢?
首先,補丁包要足夠小,像直接下發(fā)完整的補丁包肯定是不行的,很占用空間。
而像有些方案,是先進行 bsdiff,對資源包做差量,然后下發(fā)差量包,在運行時 合成完整包再加載。這樣確實減小了包的體積,但是卻在運行時多了合成的操作,耗費了運行時間和內(nèi)存。合成后的包也是完整的包,仍舊會占用磁盤空間。
而如果不采用類似 Instant Run 的方案,市面上許多實現(xiàn),是自己修改aapt, 在打包時將補丁包資源進行重新編號。這樣就會涉及到修改 Android SDK工具包, 即不利于集成也無法很好地對將來的aapt 版本進行升級。
針對以上幾個問題,一個好的資源熱修復方案,既要保證補丁包足夠小,不在 運行時占用很多資源,又要不侵入打包流程。我們提出了一個目前市面上未曾實現(xiàn) 的方案。
簡單來說,我們構造了一個 package id 為 0x66的資源包,這個包里只包含改變了的資源項,然后直接在原有AssetManager 中 addAssetPath 這個包。然后就可以了。真的這么簡單?
沒錯!由于補丁包的 package id 為 0x66,不與目前已經(jīng)加載的 0x7f沖突,因 此直接加入到已有的AssetManager中就可以直接使用了。補丁包里面的資源,只包含原有包里面沒有而新的包里面有的新增資源,以及原有內(nèi)容發(fā)生了改變的資源。
而資源的改變包含增加、減少' 修改這三種情況,我們分別是如何處理的呢?
對于新增資源,直接加入補丁包,然后新代碼里直接引用就可以了,沒什么好說的。
對于減少資源,我們只要不使用它就行了,因此不用考慮這種情況,它也不影響補丁包。
對于修改資源,比如替換了一張圖片之類的情況。我們把它視為新增資源, 在打入補丁的時候,代碼在引用處也會做相應修改,也就是直接把原來使用舊資源 id 的地方變?yōu)樾?id。
用一張圖來說明補丁包的情況,是這樣的:
圖中綠線表示新增資源。紅線表示內(nèi)容發(fā)生修改的資源。黑線表示內(nèi)容沒有變 化,但是id 發(fā)生改變的資源。x 表示刪除了的資源。
可以看到,新的資源包與舊資源包相比,新增了 holo_grey 和 dropdn_item2 資源,新增的資源被加入到 patch中。并分配了 0x66 開頭的資源 id。
而新增的兩個資源導致了在它們所屬的 type 中跟在它們之后的資源 id發(fā)生了 位移。比如 holojight, id 由 0x7f020002 變?yōu)?0x7f020003,而 abc_dialog 由 0x7f030004 變?yōu)?0x7f030003。新資源插入的位置是隨機的,這與每次 aapt打包 時解析xml 的順序有關。發(fā)生位移的資源不會加入 patch,但是在 patch的代碼中會調(diào)整id 的引用處。
比如說在代碼里,我們是這么寫的
imageView.setImageResource(R.drawable.holo_light);
這個 R.drawable.holojight 是一個int 值,它的值是 aapt指定的,對于開發(fā)者 透明,即使點進去,也會直接跳到對應res/drawable/holo_light.jpg,無法查看。不過可以用反編譯工具,看到它的真實值是0x7f020002。所以這行代碼其實等價于:
imageView.setImageResource(0x7f020002);
而當打出了一個新包后,對開發(fā)者而言,holojight的圖片內(nèi)容沒變,代碼引用處也沒變。但是新包里面,同樣是這句話,由于新資源的插入導致的id改變,對于 R.drawable.holojight 的引用已經(jīng)變成了:
imageView.setImageResource(0x7f020003);
但實際上這種情況并不屬于資源改變,更不屬于代碼的改變,所以我們在對比新舊代碼之前,會把新包里面的這行代碼修正回原來的id。
imageView.setImageResource(0x7f020002);
然后再進行后續(xù)代碼的對比。這樣后續(xù)代碼對比時就不會檢測到發(fā)生了改變。
而對于內(nèi)容發(fā)生改變的資源(類型為 layout 的 activity_main,這可能是我們修 改了 activity_main.xml 的文件內(nèi)容。還有類型為 string 的 no,可能是我們修改了這個字符串的值),它們都會被加入到 patch 中,并重新編號為新 id。而相應的代碼,也會發(fā)生改變,比如,
setContentView(R.layout.activity_main);
實際上也就是
setContentView(0x7f030000);
在生成對比新舊代碼之前,我們會把新包里面的這行代碼變?yōu)?/p>
setContentView(0x6 6020000);
這樣,在進行代碼對比時,會使得這行代碼所在函數(shù)被檢測到發(fā)生了改變。于是相應的代碼修復會在運行時發(fā)生,這樣就引用到了正確的新內(nèi)容資源。
對于刪除的資源,不會影響補丁包。
這很好理解,既然資源被刪除了,就說明新的代碼中也不會用到它,那資源放在那里沒人用,就相當于不存在了。
可以看到,由于 type0x01 的所有資源項都沒有變化,所以整個 type0x01資源都沒有加入到patch 中。這也使得后面的 type 的 id 都往前移了一位。因此 Type String Pool 中的字符串也要進行修正,這樣才能使得 0x01 的 type 指向 drawable, 而不是原來的 attr。
所以我們可以看到,所謂簡單,指的是運行時應用patch變的簡單了。
而真正復雜的地方在于構造 patch 。我們需要把新舊兩個資源包解開,分別解析 其中的resources.arsc 文件,對比新舊的不同,并將它們重新打成帶有新 package id 的新資源包。這里補丁包指定的 package id 只要不是 0x7f 和 0x01就行,可以是 任意0x7f 以下的數(shù)字,我們默認把它指定為 0x66。
構造這樣的補丁資源包,需要對整個resources.arsc的結構十分了解,要對二 進制形式的一個一個chunk進行解析分類,然后再把補丁信息一個一個重新組裝成 二進制的chunk。這里面很多工作與 aapt做的類似,實際上開發(fā)打包工具的時候也是參考了很多aapt和系統(tǒng)加載資源的代碼。
對于 Android L 以后的版本,直接在原有 AssetManager 上應用 patch就行 了。并且由于用的是原來的AssetManager,所以原先大量的反射修改替換操作就 完全不需要了,大大提高了加載補丁的效率。
但之前提到過,在 Android KK 和以下版本,addAssetPath是不會加載資源 的,必須重新構造一個新的AssetManager 并加入 patch,再換掉原來的。那么我們不就又要和Instant Run —樣,做一大堆兼容版本和反射替換的工作了嗎?
對于這種情況,我們也找到了更優(yōu)雅的方式,不需要再如此地大費周章。
明顯,這個是用來銷毀 AssetManager并釋放資源的函數(shù),我們來看看它具體做了什么吧。
可以看到,首先,它析構了 native 層的 AssetManager,然后把 java層的 AssetManager 對 native 層的 AssetManager 的引用設為空。
native 層的 AssetManager 析構函數(shù)會析構它的所有成員,這樣就會釋放之前加載了的資源。
而現(xiàn)在,java 層的 AssetManager 已經(jīng)成為了空殼。我們就可以調(diào)用它的 init 方法,對它重新進行初始化了!
這同樣是個native方法,
這樣,在執(zhí)行 init 的時候,會在 native層創(chuàng)建一個沒有添加過資源,并且 mResources 沒有初始化的的 AssetManager。然后我們再對它進行 addAssetPath,之后由于 mResource 沒有初始化過,就可以正常走到解析 mResources的邏輯,加載所有此時add進去的資源了 !
由于我們是直接對原有的 AssetManager進行析構和重構,所有原先對 AssetManager 對象的引用是沒有發(fā)生改變的,這樣,就不需要像 Instant Run那樣進行繁瑣的修改了。
順帶一提,類似 Instant Run 的完整替換資源的方案,在替換 AssetManager這一步,也可以采用我們這種方式進行替換,省時省力又省心。
總結一下,相比于目前市面上的資源修復方式,我們提出的資源修復的優(yōu)勢在于:
不侵入打包,直接對比新舊資源即可產(chǎn)生補丁資源包。(對比修改 aapt方式的 實現(xiàn))
不必下發(fā)完整包,補丁包中只包含有變動的資源。(對比 Instanat Run,Amigo 等方式的實現(xiàn))
不需要在運行時合成完整包。不占用運行時計算和內(nèi)存資源。(對比 Tinker的 實現(xiàn))
唯一有個需要注意的地方就是,因為對新的資源的引用是在新代碼中,所有資源修復是需要代碼修復的支持的。也因此所有資源修復方案必然是附帶代碼修復的。而 之前提到過,本方案在進行代碼修復前,會對資源引用處進行修正。而修正就是需要 找到舊的資源id,換成新的id。查找舊 id 時是直接對 int值進行替換,所以會找到 0x7f ?????? 這樣的需要替換 id。但是,如果有開發(fā)者使用到了 0x7f ??????這樣的數(shù)字,而它并非資源id,可是卻和需要替換的id數(shù)值相同,這就會導致這個數(shù)字 被錯誤地替換。
但這種情況是極為罕見的,因為很少會有人用到這樣特殊的數(shù)字,并且還需要碰巧這數(shù)字和資源id相等才行。即使出現(xiàn),開發(fā)者也可以用拼接的方式繞過這類數(shù)字的產(chǎn)生。所以基本可以不用擔心這種情況,只是需要注意它的存在。
以上是“Android熱修復技術原理之資源熱修復技術的示例分析”這篇文章的所有內(nèi)容,感謝各位的閱讀!相信大家都有了一定的了解,希望分享的內(nèi)容對大家有所幫助,如果還想學習更多知識,歡迎關注億速云行業(yè)資訊頻道!
免責聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權請聯(lián)系站長郵箱:is@yisu.com進行舉報,并提供相關證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權內(nèi)容。