深入理解Android熱修復(fù)技術(shù)原理之資源熱修復(fù)技術(shù)
一、普遍的實(shí)現(xiàn)方式
目前市面上的很多資源熱修復(fù)方案基本上都是參考了 Instant Run的實(shí)現(xiàn)。
簡要說來,Instant Run中的資源熱修復(fù)分為兩步:
1.構(gòu)造一個(gè)新的 AssetManager,并通過反射調(diào)用 addAssetPath,把這個(gè)完 整的新資源包加入到AssetManager中。這樣就得到了一個(gè)含有所有新資源的 AssetManager。
2.找到所有之前引用到原有 AssetManager的地方,通過反射,把引用處替換 為 AssetManager。
一個(gè) Android 進(jìn)程只包含一個(gè) ResTable, ResTable 的成員變量 mPackageGroups 就是所有解析過的資源包的集合。任何一個(gè)資源包中都含有 resources.arsc,它記錄了所有資源的id分配情況以及資源中的所有字符串。這些信息是以二進(jìn)制方式存儲(chǔ)的。底層的AssetManager做的事就是解析這個(gè)文件,然后把相關(guān)信息存儲(chǔ)到 mPackageGroups 里面。
二、資源文件的格式
整個(gè) resources.arse 文件,實(shí)際上是由一個(gè)個(gè) ResChunk (以下簡稱 chunk) 拼接起來的。從文件頭開始,每個(gè) chunk 的頭部都是一個(gè) ResChunk_header結(jié)構(gòu),它指示了這個(gè)chunk的大小和數(shù)據(jù)類型。
通過ResChunk_header中的type成員,可以知道這個(gè)chunk是什么類型, 從而就可以知道應(yīng)該如何解析這個(gè)chunko
解析完一個(gè) chunk 后,從這個(gè) chunk + size的位置開始,就可以得到下一個(gè) chunk 起始位置,這樣就可以依次讀取完整個(gè)文件的數(shù)據(jù)內(nèi)容。
一般來說,一個(gè) resources.arsc 里面包含若干個(gè)package,不過默認(rèn)情況下, 由打包工具aapt 打出來的包只有一個(gè) package。這個(gè) package里包含了 app中的 所有資源信息。
資源信息主要是指每個(gè)資源的名稱以及它對應(yīng)的編號(hào)。我們知道,Android中的每個(gè)資源,都有它唯一的編號(hào)。編號(hào)是一個(gè) 32 位數(shù)字,用十六進(jìn)制來表示就是0xPPTTEEEE。PP 為 package id, TT 為 type id, EEEE 為 entry id。
它們代表什么?在 resources.arse 里是以怎樣的方式記錄的呢?
- 對于 package id,每個(gè) package 對應(yīng)的是類型為 RES_TABLE_PACKAG E_ TYPE 的 ResTable_package 結(jié)構(gòu)體,ResTable_package 結(jié)構(gòu)體的 id 成員變量就表示它的 package id。
- 對于 type id,每個(gè)type對應(yīng)的是類型為 RES_TABLE_TYPE_SPEC_ TYPE 的 ResTable_typeSpec 結(jié)構(gòu)體。它的id成員變量就是type id。但是,該type id 具體對應(yīng)什么類型,是需要到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。所以,每個(gè) type id對應(yīng)了 Type String Pool里的字符順序 所指定的類型。
- 對于 entry id,每個(gè) entry表示一個(gè)資源項(xiàng),資源項(xiàng)是按照排列的先后順序 自動(dòng)被標(biāo)機(jī)編號(hào)的。也就是說,一個(gè)type里按位置出現(xiàn)的第一個(gè)資源項(xiàng),其 entry id 為0x0000,第二個(gè)為 0x0001,以此類推。因此我們是無法直接指定entry id的,只能夠根據(jù)排布順序決定。資源項(xiàng)之間是緊密排布的,沒有空隙,但是可以指定資源項(xiàng)為ResTable_type::NO_ENTRY來填入一個(gè)空資源。
舉個(gè)例子,我們隨便找個(gè)帶資源的 apk,用 aapt解析一下,看到其中的一行是:
$ aapt d resources app-debug.apk
......
spec resource 0x7f040019 com.taobao.patch.demo:layout/activity_main: flags=0x00000000
......
這就表示,activity_main.xml 這個(gè)資源的編號(hào)是 0x7f040019。它的 package id 是 0x7f,資源類型的id為0x04, Type String Pool里的第四個(gè)字符串正是 layout 類型,而 0x04 類型的第 0x0019 個(gè)資源項(xiàng)就是 activity_main 這個(gè)資源。
三、運(yùn)行時(shí)資源的解析
默認(rèn)由 Android SDK 編出來的 apk,是由 aapt 具進(jìn)行打包的,其資源包的 package id 就是 0x7f。
系統(tǒng)的資源包,也就是 framework-res.jar, package id 為 0x01。
在走到 app的第一行代碼之前,系統(tǒng)就已經(jīng)幫我們構(gòu)造好一個(gè)已經(jīng)添加了安裝包資源的 AssetManager 了。
因此,這個(gè) AssetManager里就已經(jīng)包含了系統(tǒng)資源包以及 app的安裝包,就是 package id 為 0x01 的 framework-res.jar 中的資源和 package id 為 0x7f 的 app 安裝包資源。
如果此時(shí)直接在原有 AssetManager 上繼續(xù) addAssetPath的完整補(bǔ)丁包的 話,由于補(bǔ)丁包里面的package id 也是 0x7f,就會(huì)使得同一個(gè) package id的包被 加載兩次。這會(huì)有怎樣的問題呢?
在 Android L 之后,這是沒問題的,他會(huì)默默地把后來的包添加到之前的包的同—個(gè) PackageGroup 下面。
而在解析的時(shí)候,會(huì)與之前的包比較同一個(gè) type id所對應(yīng)的類型,如果該類型 下的資源項(xiàng)數(shù)目和之前添加過的不一致,會(huì)打出一條warning log,但是仍舊加入到該類型的TypeList 中。
在獲取某個(gè) Type的資源時(shí),會(huì)從前往后遍歷,也就是說先得到原有安裝包里 的資源,除非后面的資源的config比前面的更詳細(xì)才會(huì)發(fā)生覆蓋。而對于同一個(gè) config 而言,補(bǔ)丁中的資源就永遠(yuǎn)無法生效了。所以在 Android L以上的版本,在原有AssetManager 上加入補(bǔ)丁包,是沒有任何作用的,補(bǔ)丁中的資源無法生效。
而在 Android 4.4 及以下版本,addAssetPath只是把補(bǔ)丁包的路徑添加到 了 mAssetPath中,而真正解析的資源包的邏輯是在app第一次執(zhí)行 AssetManager::getResTable 的時(shí)候。
而在執(zhí)行到加載補(bǔ)丁代碼的時(shí)候,getResTable已經(jīng)執(zhí)行過了無數(shù)次了。這是因?yàn)榫退阄覀冎皼]做過任何資源相關(guān)操作,Android framework里的代碼也會(huì)多 次調(diào)用到那里。所以,以后即使是addAssetPath,也只是添加到了 mAssetPath, 并不會(huì)發(fā)生解析。所以補(bǔ)丁包里面的資源是完全不生效的!
所以,像 Instant Run 這種方案,一定需要一個(gè)全新的 AssetManager時(shí),然后再加入完整的新資源包,替換掉原有的AssetManager。
四、另辟蹊徑的資源修復(fù)方案
而一個(gè)好的資源熱修復(fù)方案是怎樣的呢?
首先,補(bǔ)丁包要足夠小,像直接下發(fā)完整的補(bǔ)丁包肯定是不行的,很占用空間。
而像有些方案,是先進(jìn)行 bsdiff,對資源包做差量,然后下發(fā)差量包,在運(yùn)行時(shí) 合成完整包再加載。這樣確實(shí)減小了包的體積,但是卻在運(yùn)行時(shí)多了合成的操作,耗費(fèi)了運(yùn)行時(shí)間和內(nèi)存。合成后的包也是完整的包,仍舊會(huì)占用磁盤空間。
而如果不采用類似 Instant Run 的方案,市面上許多實(shí)現(xiàn),是自己修改aapt, 在打包時(shí)將補(bǔ)丁包資源進(jìn)行重新編號(hào)。這樣就會(huì)涉及到修改 Android SDK工具包, 即不利于集成也無法很好地對將來的aapt 版本進(jìn)行升級(jí)。
針對以上幾個(gè)問題,一個(gè)好的資源熱修復(fù)方案,既要保證補(bǔ)丁包足夠小,不在 運(yùn)行時(shí)占用很多資源,又要不侵入打包流程。我們提出了一個(gè)目前市面上未曾實(shí)現(xiàn) 的方案。
簡單來說,我們構(gòu)造了一個(gè) package id 為 0x66的資源包,這個(gè)包里只包含改變了的資源項(xiàng),然后直接在原有AssetManager 中 addAssetPath 這個(gè)包。然后就可以了。真的這么簡單?
沒錯(cuò)!由于補(bǔ)丁包的 package id 為 0x66,不與目前已經(jīng)加載的 0x7f沖突,因 此直接加入到已有的AssetManager中就可以直接使用了。補(bǔ)丁包里面的資源,只包含原有包里面沒有而新的包里面有的新增資源,以及原有內(nèi)容發(fā)生了改變的資源。
而資源的改變包含增加、減少' 修改這三種情況,我們分別是如何處理的呢?
- 對于新增資源,直接加入補(bǔ)丁包,然后新代碼里直接引用就可以了,沒什么好說的。
- 對于減少資源,我們只要不使用它就行了,因此不用考慮這種情況,它也不影響補(bǔ)丁包。
- 對于修改資源,比如替換了一張圖片之類的情況。我們把它視為新增資源, 在打入補(bǔ)丁的時(shí)候,代碼在引用處也會(huì)做相應(yīng)修改,也就是直接把原來使用舊資源 id 的地方變?yōu)樾?id。
用一張圖來說明補(bǔ)丁包的情況,是這樣的:
圖中綠線表示新增資源。紅線表示內(nèi)容發(fā)生修改的資源。黑線表示內(nèi)容沒有變 化,但是id 發(fā)生改變的資源。x 表示刪除了的資源。
4.1、新增的資源及其導(dǎo)致 id 偏移
可以看到,新的資源包與舊資源包相比,新增了 holo_grey 和 dropdn_item2 資源,新增的資源被加入到 patch中。并分配了 0x66 開頭的資源 id。
而新增的兩個(gè)資源導(dǎo)致了在它們所屬的 type 中跟在它們之后的資源 id發(fā)生了 位移。比如 holojight, id 由 0x7f020002 變?yōu)?0x7f020003,而 abc_dialog 由 0x7f030004 變?yōu)?0x7f030003。新資源插入的位置是隨機(jī)的,這與每次 aapt打包 時(shí)解析xml 的順序有關(guān)。發(fā)生位移的資源不會(huì)加入 patch,但是在 patch的代碼中會(huì)調(diào)整id 的引用處。
比如說在代碼里,我們是這么寫的
imageView.setImageResource(R.drawable.holo_light);
這個(gè) R.drawable.holojight 是一個(gè)int 值,它的值是 aapt指定的,對于開發(fā)者 透明,即使點(diǎn)進(jìn)去,也會(huì)直接跳到對應(yīng)res/drawable/holo_light.jpg,無法查看。不過可以用反編譯工具,看到它的真實(shí)值是0x7f020002。所以這行代碼其實(shí)等價(jià)于:
imageView.setImageResource(0x7f020002);
而當(dāng)打出了一個(gè)新包后,對開發(fā)者而言,holojight的圖片內(nèi)容沒變,代碼引用處也沒變。但是新包里面,同樣是這句話,由于新資源的插入導(dǎo)致的id改變,對于 R.drawable.holojight 的引用已經(jīng)變成了:
imageView.setImageResource(0x7f020003);
但實(shí)際上這種情況并不屬于資源改變,更不屬于代碼的改變,所以我們在對比新舊代碼之前,會(huì)把新包里面的這行代碼修正回原來的id。
imageView.setImageResource(0x7f020002);
然后再進(jìn)行后續(xù)代碼的對比。這樣后續(xù)代碼對比時(shí)就不會(huì)檢測到發(fā)生了改變。
4.2、內(nèi)容發(fā)生改變的資源
而對于內(nèi)容發(fā)生改變的資源(類型為 layout 的 activity_main,這可能是我們修 改了 activity_main.xml 的文件內(nèi)容。還有類型為 string 的 no,可能是我們修改了這個(gè)字符串的值),它們都會(huì)被加入到 patch 中,并重新編號(hào)為新 id。而相應(yīng)的代碼,也會(huì)發(fā)生改變,比如,
setContentView(R.layout.activity_main);
實(shí)際上也就是
setContentView(0x7f030000);
在生成對比新舊代碼之前,我們會(huì)把新包里面的這行代碼變?yōu)?/p>
setContentView(0x6 6020000);
這樣,在進(jìn)行代碼對比時(shí),會(huì)使得這行代碼所在函數(shù)被檢測到發(fā)生了改變。于是相應(yīng)的代碼修復(fù)會(huì)在運(yùn)行時(shí)發(fā)生,這樣就引用到了正確的新內(nèi)容資源。
4.3、刪除了的資源
對于刪除的資源,不會(huì)影響補(bǔ)丁包。
這很好理解,既然資源被刪除了,就說明新的代碼中也不會(huì)用到它,那資源放在那里沒人用,就相當(dāng)于不存在了。
4.4、對于type的影響
可以看到,由于 type0x01 的所有資源項(xiàng)都沒有變化,所以整個(gè) type0x01資源都沒有加入到patch 中。這也使得后面的 type 的 id 都往前移了一位。因此 Type String Pool 中的字符串也要進(jìn)行修正,這樣才能使得 0x01 的 type 指向 drawable, 而不是原來的 attr。
所以我們可以看到,所謂簡單,指的是運(yùn)行時(shí)應(yīng)用patch變的簡單了。
而真正復(fù)雜的地方在于構(gòu)造 patch 。我們需要把新舊兩個(gè)資源包解開,分別解析 其中的resources.arsc 文件,對比新舊的不同,并將它們重新打成帶有新 package id 的新資源包。這里補(bǔ)丁包指定的 package id 只要不是 0x7f 和 0x01就行,可以是 任意0x7f 以下的數(shù)字,我們默認(rèn)把它指定為 0x66。
構(gòu)造這樣的補(bǔ)丁資源包,需要對整個(gè)resources.arsc的結(jié)構(gòu)十分了解,要對二 進(jìn)制形式的一個(gè)一個(gè)chunk進(jìn)行解析分類,然后再把補(bǔ)丁信息一個(gè)一個(gè)重新組裝成 二進(jìn)制的chunk。這里面很多工作與 aapt做的類似,實(shí)際上開發(fā)打包工具的時(shí)候也是參考了很多aapt和系統(tǒng)加載資源的代碼。
五、更優(yōu)雅地替換AssetManager
對于 Android L 以后的版本,直接在原有 AssetManager 上應(yīng)用 patch就行 了。并且由于用的是原來的AssetManager,所以原先大量的反射修改替換操作就 完全不需要了,大大提高了加載補(bǔ)丁的效率。
但之前提到過,在 Android KK 和以下版本,addAssetPath是不會(huì)加載資源 的,必須重新構(gòu)造一個(gè)新的AssetManager 并加入 patch,再換掉原來的。那么我們不就又要和Instant Run —樣,做一大堆兼容版本和反射替換的工作了嗎?
對于這種情況,我們也找到了更優(yōu)雅的方式,不需要再如此地大費(fèi)周章。
明顯,這個(gè)是用來銷毀 AssetManager并釋放資源的函數(shù),我們來看看它具體做了什么吧。
可以看到,首先,它析構(gòu)了 native 層的 AssetManager,然后把 java層的 AssetManager 對 native 層的 AssetManager 的引用設(shè)為空。
native 層的 AssetManager 析構(gòu)函數(shù)會(huì)析構(gòu)它的所有成員,這樣就會(huì)釋放之前加載了的資源。
而現(xiàn)在,java 層的 AssetManager 已經(jīng)成為了空殼。我們就可以調(diào)用它的 init 方法,對它重新進(jìn)行初始化了!
這同樣是個(gè)native方法,
這樣,在執(zhí)行 init 的時(shí)候,會(huì)在 native層創(chuàng)建一個(gè)沒有添加過資源,并且 mResources 沒有初始化的的 AssetManager。然后我們再對它進(jìn)行 addAssetPath,之后由于 mResource 沒有初始化過,就可以正常走到解析 mResources的邏輯,加載所有此時(shí)add進(jìn)去的資源了 !
由于我們是直接對原有的 AssetManager進(jìn)行析構(gòu)和重構(gòu),所有原先對 AssetManager 對象的引用是沒有發(fā)生改變的,這樣,就不需要像 Instant Run那樣進(jìn)行繁瑣的修改了。
順帶一提,類似 Instant Run 的完整替換資源的方案,在替換 AssetManager這一步,也可以采用我們這種方式進(jìn)行替換,省時(shí)省力又省心。
六、本章小結(jié)
總結(jié)一下,相比于目前市面上的資源修復(fù)方式,我們提出的資源修復(fù)的優(yōu)勢在于:
- 不侵入打包,直接對比新舊資源即可產(chǎn)生補(bǔ)丁資源包。(對比修改 aapt方式的 實(shí)現(xiàn))
- 不必下發(fā)完整包,補(bǔ)丁包中只包含有變動(dòng)的資源。(對比 Instanat Run,Amigo 等方式的實(shí)現(xiàn))
- 不需要在運(yùn)行時(shí)合成完整包。不占用運(yùn)行時(shí)計(jì)算和內(nèi)存資源。(對比 Tinker的 實(shí)現(xiàn))
唯一有個(gè)需要注意的地方就是,因?yàn)閷π碌馁Y源的引用是在新代碼中,所有資源修復(fù)是需要代碼修復(fù)的支持的。也因此所有資源修復(fù)方案必然是附帶代碼修復(fù)的。而 之前提到過,本方案在進(jìn)行代碼修復(fù)前,會(huì)對資源引用處進(jìn)行修正。而修正就是需要 找到舊的資源id,換成新的id。查找舊 id 時(shí)是直接對 int值進(jìn)行替換,所以會(huì)找到 0x7f ?????? 這樣的需要替換 id。但是,如果有開發(fā)者使用到了 0x7f ??????這樣的數(shù)字,而它并非資源id,可是卻和需要替換的id數(shù)值相同,這就會(huì)導(dǎo)致這個(gè)數(shù)字 被錯(cuò)誤地替換。
但這種情況是極為罕見的,因?yàn)楹苌贂?huì)有人用到這樣特殊的數(shù)字,并且還需要碰巧這數(shù)字和資源id相等才行。即使出現(xiàn),開發(fā)者也可以用拼接的方式繞過這類數(shù)字的產(chǎn)生。所以基本可以不用擔(dān)心這種情況,只是需要注意它的存在。
以上就是深入理解Android熱修復(fù)技術(shù)原理之資源熱修復(fù)技術(shù)的詳細(xì)內(nèi)容,更多關(guān)于Android資源熱修復(fù)的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Android實(shí)現(xiàn)雅虎新聞?wù)虞d視差動(dòng)畫效果
這篇文章主要介紹了Android實(shí)現(xiàn)雅虎新聞?wù)虞d視差動(dòng)畫效果,通過實(shí)例代碼給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2021-08-08淺析Kotlin使用infix函數(shù)構(gòu)建可讀語法流程講解
這篇文章主要介紹了淺析Kotlin使用infix函數(shù)構(gòu)建可讀語法,我們在Kotlin中就多次使用A to B這樣的語法結(jié)構(gòu)構(gòu)建鍵值對,包括Kotlin自帶的mapOf()函數(shù),這種語法結(jié)構(gòu)的優(yōu)點(diǎn)是可讀性強(qiáng)2023-01-01Android獲取本地相冊圖片和拍照獲取圖片的實(shí)現(xiàn)方法
這篇文章主要為大家詳細(xì)介紹了Android獲取本地相冊圖片和拍照獲取圖片的實(shí)現(xiàn)方法,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-03-03Android的RV列表刷新詳解Payload與Diff方式異同
這篇文章主要為大家介紹了Android的RV列表刷新詳解Payload與Diff方式異同,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-10-10android上實(shí)現(xiàn)0.5px線條的原理分析
這篇文章主要介紹了android上實(shí)現(xiàn)0.5px線條的原理分析,本文通過實(shí)例代碼給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2021-01-01Android開發(fā)中解析xml文件XmlUtils工具類與用法示例
這篇文章主要介紹了Android開發(fā)中解析xml文件XmlUtils工具類與用法,結(jié)合實(shí)例形式分析了Android開發(fā)中解析xml文件工具類定義與相關(guān)使用技巧,需要的朋友可以參考下2018-01-01Python基礎(chǔ)教程學(xué)習(xí)筆記 第二章 列表和元組
這篇文章主要介紹了Python基礎(chǔ)教程學(xué)習(xí)筆記 第二章 列表和元組,需要的朋友可以參考下2015-03-03android實(shí)現(xiàn)通過NFC讀取卡號(hào)
這篇文章主要介紹了android實(shí)現(xiàn)通過NFC讀取卡號(hào),文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2021-09-09