自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

Android 插件化中資源錯(cuò)亂的解決方案

精選
移動(dòng)開發(fā) Android
本文介紹了 Android 插件化框架中,插件使用宿主資源時(shí)資源錯(cuò)亂的問題,以及錯(cuò)亂的原因、業(yè)界通用解決方案、我們提出的優(yōu)化方案。

摘要

本文介紹了 Android 插件化框架中,插件使用宿主資源時(shí)資源錯(cuò)亂的問題,以及錯(cuò)亂的原因、業(yè)界通用解決方案、我們提出的優(yōu)化方案。

本文將按照如下順序,循序漸進(jìn)地進(jìn)行講解:

  • 簡單介紹 Android 插件化中資源部分的動(dòng)態(tài)化。
  • 簡單介紹 Android 中的資源的一些基礎(chǔ)知識(shí)、使用方式及其編譯原理。
  • 介紹插件化場景下出現(xiàn)的資源錯(cuò)亂問題及業(yè)界通用的解決方案。
  • 介紹一種新的方案——免資源固定方案,用于解決資源錯(cuò)亂問題。
  • 單獨(dú)介紹一下免資源固定方案中的一個(gè)技術(shù)點(diǎn):修改 apk 中的資源文件。

1. Android 插件化中資源的動(dòng)態(tài)化

Android 發(fā)展了這么多年,市面上涌現(xiàn)出許多插件化/熱修復(fù)框架,無論是插件化還是熱修復(fù),都是為了實(shí)現(xiàn)對(duì)主apk以外內(nèi)容的動(dòng)態(tài)化,這些內(nèi)容包括 dex(class)、res(資源)、so(動(dòng)態(tài)庫)等。對(duì)于每一種內(nèi)容,業(yè)界都有許多實(shí)現(xiàn)方案,盡管方案各不相同,但底層原理都差不多,網(wǎng)上也有許多文章和開源項(xiàng)目可以學(xué)習(xí)參考。

名詞解釋

宿主:直接安裝到用戶手機(jī)上的 App,宿主中的代碼在宿主安裝到用戶手機(jī)上的那一刻就定死了,不能再改變了(熱修復(fù)也只是讓錯(cuò)誤的邏輯不走而已,并沒有改變?cè)械拇a)。

插件:獨(dú)立于宿主之外的一個(gè)文件。需要被宿主動(dòng)態(tài)加載的 class、res、so 等的集合。(熱修復(fù)中這部分通常稱為 patch,這里為了方便,就叫插件吧)

java 代碼:為了描述方便,apk 中的 dex 在編譯前一律稱為 java 代碼,編譯后一律稱為 dex(這個(gè)說法不準(zhǔn)確,不要被我誤導(dǎo)了,一般為java / kotlin- > class- > dex )

說到 Android 資源的動(dòng)態(tài)化,思路都大同小異:

  • 為每個(gè)插件創(chuàng)建一個(gè) Resources 或者把插件的資源路徑添加到宿主 AssetManager,從而可以順利的加載到插件資源。
  • 插件編譯時(shí)通過配置 aapt2 參數(shù)對(duì)插件中資源 id 的 packageId 部分進(jìn)行修改,保證插件與宿主資源 id 不沖突。
  • 對(duì)于插件中使用到的宿主資源,利用 aapt2 參數(shù)進(jìn)行資源固定,保證宿主升級(jí)后插件使用到的宿主資源 id 不變。

aapt2 的出現(xiàn)使資源固定、packageId 修改變得容易了很多!

盡管 Android 資源的動(dòng)態(tài)化技術(shù)已經(jīng)十分成熟,但是在實(shí)踐過程中還是有許多不足,比如“資源固定”就經(jīng)常被業(yè)務(wù)同學(xué)吐槽。

2. Android 中的資源介紹

在介紹資源固定之前,首先簡單介紹一下 Android 中資源相關(guān)的基礎(chǔ)知識(shí)。

2.1  Android 中的資源 id

Android 代碼在編譯成 apk 之后,每個(gè)資源都對(duì)應(yīng)一個(gè)唯一的資源 id,資源 id 是一個(gè) 8 位的 16 進(jìn)制 int 值 0xPPTTEEEE :

  • PP :前兩位是 PackageId 字段,系統(tǒng)資源是 01,宿主資源 id 是 7f,其他如廠商自定義的皮膚包、webview 插件資源包會(huì)占用 02、03......,因此 App 資源和系統(tǒng)資源永遠(yuǎn)不會(huì)沖突。市面上的插件框架為了保證插件和宿主資源不沖突,通常會(huì)把插件資源的 PP 改為其他值,如 7e、7d。
  • TT :中間兩位是 TypeId 字段,表示資源的類型,如 anim、drawable、string 等,這塊沒有嚴(yán)格的對(duì)應(yīng)關(guān)系,通常是按照字母順序分配 type 值。
  • EEEE :最后四位是 EntryId 字段,用于區(qū)分同一個(gè) PackageId、同一個(gè) TypeId 下不同 name 的資源,通常也是按照字母順序進(jìn)行分配的。

注意:

  • 資源 id 的分配默認(rèn)是按資源的字母排序進(jìn)行的,也就是說,當(dāng)新增一個(gè) name 為 a 的資源,重新編譯之后,a 后面的同類型的資源 id 值都會(huì)被改變。
  • aapt2 中提供了參數(shù)可以對(duì)資源 id 分配方式進(jìn)行干預(yù),aapt2 會(huì)優(yōu)先按照參數(shù)中配置的對(duì)應(yīng)關(guān)系分配 id,這個(gè)技術(shù)我們稱之為資源固定,也是目前插件化框架在解決資源錯(cuò)亂問題中用的最多的技術(shù)。

2.2  Android 中的資源使用方式

Android 中使用資源通常有兩種方式:

  1. 在 java 代碼中通過 R 的內(nèi)部類進(jìn)行訪問,具體語法為:
[<package_name>].R.<resource_type>.<resource_name>
  1. 在 xml 中通過符號(hào)使用,具體語法為:
@[<package_name>:]<resource_type>/<resource_name>

xml 中也可以通過 ? 代替 @ 的形式引用樣式屬性。也可以引入自定義屬性,如 android:layout_width 。這兩種用法不影響下文的介紹。

那么這兩種方式有什么區(qū)別呢?

從代碼書寫的角度來說,都是通過一個(gè)資源名稱(resource_name)來訪問資源。我們反編譯一下 apk,看看編譯后是什么樣的。

分別在項(xiàng)目 app module、library module、xml 中編寫如下代碼

圖片

我們反編譯一下 apk,看看這三種代碼在 apk 中是如何表現(xiàn)的。

圖片

可以發(fā)現(xiàn) appTest 方法和 xml 中的資源變成了數(shù)字(0x7f0e0069),libTest 方法中的資源依舊是通過 Lcom/bytedance/lib/R$string;->test 訪問的

結(jié)論:

  • 主 module 中引用的資源被編譯成了數(shù)值;
  • 子 module、aar 中通過 R 的內(nèi)部類間接引用數(shù)值;
  • xml 中的資源 id 全部編譯成了數(shù)值。(看上圖中 xml 的屬性—— lay out_width 等依舊是字符串,其實(shí)它背后也是資源 id 數(shù)值,這塊的字符串其實(shí)是沒有用的,甚至在一些包體積優(yōu)化中可以直接去掉)。

那么為什么 libTest 方法中是通過 field 引用,而 appTest 中就變成數(shù)字了呢?

2.3 Android 中資源編譯的簡單流程

假設(shè)有一個(gè)工程,只有一個(gè) app module,通過 maven 倉庫依賴若干三方 aar,項(xiàng)目編譯時(shí)的簡化流程如下圖:

圖片

  1. 下載三方 aar;
  2. 將 app module 和三方 aar 中的資源經(jīng)過 aapt2 進(jìn)行編譯、鏈接,最終生成R.jar和ap_
  • R.jar 包含了最終打入 apk 的所有 R.class,每個(gè)依賴對(duì)應(yīng)一個(gè)。aapt2 也會(huì)默認(rèn)按照字母排序?yàn)槊總€(gè)資源分配唯一的 id 值。注意:新增刪除一個(gè)資源都會(huì)導(dǎo)致它后面的資源 id 改變。aapt2 允許通過配置干預(yù) id 的分配。
  • ap_ 文件中包含了所有編譯好的資源文件。
  1. App module 的 java 文件與 R.jar 一起被 javac 編譯。由于 R.jar 中的 field 都是 final,因此 app module 中通過 R 引用的資源全部被內(nèi)聯(lián)成了數(shù)值。而三方 aar 中由于已經(jīng)是 class,無需進(jìn)行編譯,因此依舊是通過 R 引用來使用資源;
  2. 最后把 app module 編譯出來的 .class、三方 aar 中的 .class 轉(zhuǎn)成 dex,與 ap_ 一起壓縮到 apk 中。

因此就很容易理解為啥 libTest 中依舊是通過 R 來使用資源,而 appTest 中通過數(shù)值直接引用(被內(nèi)聯(lián))。

libTest module 雖然被 app module 通過源碼依賴,但是在資源編譯這塊其實(shí)是類似的,這里不展開介紹。

2.4 總結(jié)

Android 中的資源的無論是通過 java 代碼使用還是 xml 使用,最終都是通過資源 id 值進(jìn)行查找的。

把 apk 拖到 as 中,查看 resources.arsc 文件,可以看到它里面包含了 apk 中所有資源的 id 索引,以及該資源名對(duì)應(yīng)的真正資源或值。很容易想到,App 運(yùn)行起來也是通過資源 id 值經(jīng)過這個(gè)資源表來查找真正的資源內(nèi)容。

3. 插件使用宿主資源

3.1 插件如何使用宿主資源

想象一下,我們想要把 App 的直播功能做成一個(gè)插件動(dòng)態(tài)下發(fā),直播功能所需要的大部分資源都在直播插件中,但是總有一些資源來自宿主,如一些通用的 UI 組件中包含的資源(support/androidx 庫)等。

那么,假設(shè)宿主中有一張圖片名為 icon,直播插件中的 xml 通過 @drawable/icon 引用了這張圖片,同時(shí)也在代碼中通過 R.drawable.icon 引用了它,實(shí)際直播插件中是沒有 icon 這張圖片的,它存在于宿主中。宿主編譯完后,按照前面的知識(shí)點(diǎn),宿主中的 icon 對(duì)應(yīng)的數(shù)值被編譯成 0x7f010001。

插件本身也是一個(gè) apk,根據(jù)前面介紹的知識(shí)點(diǎn),插件編譯完成后,xml 中的 @drawable/icon 會(huì)編成一個(gè)數(shù)值(0x7f010001),java 代碼中的 R.drawable.icon 也會(huì)直接或間接編成一個(gè)數(shù)值(0x7f010001)。當(dāng)這個(gè)插件運(yùn)行在宿主上,按照前面的介紹,插件會(huì)去查找 0x7f010001,發(fā)現(xiàn)可以找到,這樣就正確的使用了宿主資源。

插件編譯時(shí)我們會(huì)做一些處理,使插件中可以引用到宿主 id。

3.2 插件使用宿主資源有什么問題

前文介紹過,新增或刪除一個(gè)資源都可能導(dǎo)致其他許多資源的 id 被改變。

我們的宿主編譯出來后 icon 為 0x7f010001,基于已有的宿主編譯出一個(gè)插件后,插件中引用的 icon 也是 0x7f010001,此時(shí)沒什么問題。

宿主迭代后,新增了一個(gè)新的資源 aicon,按照前面介紹的資源 id 分配規(guī)則,新版本的宿主中 aicon 的 id 值為 0x7f010001,icon 的 id 值被分配為 0x7f010002。老版本的插件下發(fā)到新版本的宿主上時(shí)依舊會(huì)通過 0x7f010001去宿主中找 icon,自然就找錯(cuò)了。運(yùn)氣好一點(diǎn)可能只是圖片展示異常,運(yùn)氣不好點(diǎn)可能就直接 crash 了。

3.3 如何解決這類問題

為了解決這個(gè)問題,業(yè)界目前有一個(gè)通用、穩(wěn)定的方案——資源固定。宿主編譯時(shí)通過 aapt2 提供的參數(shù)對(duì)插件使用到的資源進(jìn)行固定,使宿主每次打包時(shí)這些資源的值永遠(yuǎn)不發(fā)生改變。

資源固定方案的弊端:

  1. 一個(gè)插件對(duì)應(yīng)一個(gè)宿主的情況:
  • 必須把宿主的所有資源都進(jìn)行固定。如果只固定插件使用的資源,當(dāng)一個(gè)宿主有兩個(gè)插件時(shí),兩個(gè)插件各自給宿主固定自己需要的資源,在代碼合并時(shí),很容易引發(fā)沖突,因?yàn)橘Y源固定的值是不允許重復(fù)的;
  • 當(dāng)宿主接入多個(gè)涉及到資源固定的框架,如:插件化、資源熱修復(fù)、游戲重打包框架等,這些框架之間進(jìn)行資源固定時(shí)也需要考慮統(tǒng)一固定,這個(gè)成本是很高的;
  • 資源固定提高了宿主接入框架的成本。
  1. 一個(gè)插件運(yùn)行在多個(gè)宿主的情況:
  • 當(dāng)一個(gè)插件想要運(yùn)行在多個(gè)宿主上,就需要每個(gè)宿主針對(duì)該插件的資源使用情況進(jìn)行資源固定。一旦某個(gè)宿主已經(jīng)對(duì)某個(gè)資源進(jìn)行了固定,導(dǎo)致其與該插件要求的資源固定產(chǎn)生沖突,插件就需要對(duì)該宿主進(jìn)行妥協(xié),根據(jù)該宿主已有的資源固定重新生成固定規(guī)則。這樣就無法實(shí)現(xiàn)一個(gè)插件在多個(gè)宿主上運(yùn)行。我們目前有一個(gè)需求:同一個(gè)插件需要在上千個(gè)宿主上運(yùn)行,如果不能解決這個(gè)問題,可能需要打成百上千個(gè)插件出來,很明顯是不合理的;

  • 資源固定提高了宿主接入框架的成本。

為了解決上述的問題,我們研究了一套新的方案解決資源錯(cuò)亂問題。

4. 免資源固定方案

同一個(gè)版本的插件運(yùn)行在不同版本甚至不同的 App 上時(shí),插件的代碼是固定的,而宿主中的資源 id 是會(huì)改變的,為了解決資源錯(cuò)亂問題,當(dāng)前的思路是保證宿主每次出新版本時(shí)資源 id 不變。那么有沒有辦法在不約束宿主的情況下,讓插件始終跟宿主的資源 id 保持一致呢?

由于插件打包時(shí),宿主是未知的,并且對(duì)于一個(gè)插件跑在多個(gè)宿主的情況,宿主也是多樣的。所以沒法指定讓插件把 id 打成滿足宿主的樣子,而前文也介紹過,插件中引用宿主 id 的地方都是常量。那怎么辦呢?

是否可以在插件運(yùn)行到宿主上時(shí),動(dòng)態(tài)修改插件中的內(nèi)容,實(shí)現(xiàn)插件與宿主 id 值匹配的效果。

比如插件中使用了宿主的資源 icon,對(duì)應(yīng)的 id 值為 0x7f010001。當(dāng)該插件運(yùn)行在一個(gè) icon 為 0x7f010002的宿主上時(shí),由于運(yùn)行時(shí)資源查找都是通過 id 值進(jìn)行的,此時(shí)我們只能知道插件是在找一個(gè) id 為 0x7f010001 的資源。通過某些手段,如果我們可以把 0x7f010001 映射成 icon 這個(gè)字符串,然后利用 Android 系統(tǒng)提供的Resources#getIdentifier方法,動(dòng)態(tài)獲取到當(dāng)前宿主中 icon 對(duì)應(yīng)的資源 id,即可保證插件加載到正確的資源。

這個(gè)工作需要在插件編譯時(shí)、運(yùn)行時(shí)分別做一些工作配合完成實(shí)現(xiàn)。

4.1  插件編譯時(shí)工作

本小節(jié)內(nèi)容基于 agp4.1 介紹,各個(gè)版本有些許差異,但總體思路大同小異。

前面介紹了,插件使用宿主資源主要有兩種情況:1.通過 java 代碼 2.通過 xml。

4.1.1 處理 java 代碼中引用宿主的資源

java 代碼在編譯成 class 之后,對(duì)于引用宿主資源 id 的代碼,有的會(huì)編譯成數(shù)值,有的依舊是通過 R 引用。對(duì)于后者,我們可以很容易找出來,對(duì)于前者就有些困難了,因?yàn)閱渭內(nèi)呙?class 中 0x7f 開頭的數(shù)字,很容易誤判,把一個(gè)無意義的數(shù)字也當(dāng)作資源 id 處理。

前面講了為什么 class 中的資源 id 會(huì)內(nèi)聯(lián)成數(shù)值,那我們不讓它內(nèi)聯(lián)不就好了嗎?只需要在編譯過程中處理 R.jar,移除 class 中所有的 final 字段,就可以保證插件中引用宿主的資源 id 全部通過 R 進(jìn)行引用。

這塊需要對(duì) agp 的工作流程、gradle plugin 的開發(fā)有一定的了解,用到了 asm 字節(jié)碼修改技術(shù)和 agp 提供的 transform api,不了解的同學(xué)可以單獨(dú)查一下,這塊就不詳細(xì)介紹了。

簡單來說就是通過這兩項(xiàng)技術(shù),可以在編譯 apk 時(shí),對(duì) class 文件進(jìn)行修改。

開始實(shí)踐

  1. 由于 R.jar 是在 processResourcesTask 中生成的,因此可以寫一個(gè) gradle plugin,在 processResourcesTask 的 doLast 中獲取到 R.jar,修改 R.jar 中的字節(jié)碼,將 field 中的 id 為 0x7f 開頭的字段的 final 修飾符全部移除。這樣就可以保證插件 class 中所有引用宿主資源的地方都不會(huì)被內(nèi)聯(lián)成數(shù)值;
  2. 經(jīng)過第一步的處理,插件中引用的宿主資源全部通過 R.xx.xx 來引用,但插件 R 中的數(shù)值依舊是無法與宿主對(duì)應(yīng)的。因此我們繼續(xù)寫一個(gè) transform,掃描出插件中通過 R 引用資源的地方,利用 asm 將其從原來的 R 引用修改為方法調(diào)用。插件運(yùn)行時(shí),原本類似 R.drawable.test 的代碼不再是獲取一個(gè)常量數(shù)值,而是調(diào)用一個(gè)方法,內(nèi)部動(dòng)態(tài)計(jì)算當(dāng)前宿主中對(duì)應(yīng)的值。?

圖片

總結(jié):

以上,通過編譯時(shí)的一些處理,即可解決插件 java 代碼中引用宿主資源時(shí)免資源固定的問題。

  • 優(yōu)點(diǎn):無需資源固定。
  • 缺點(diǎn):
  1. 插件中的部分資源不進(jìn)行內(nèi)聯(lián),會(huì)使包體積有非常微小的增加,但是問題不大;
  2. 插件引用宿主資源由原來的常量變成了方法調(diào)用,執(zhí)行效率降低,不過這塊可以通過緩存來解決。同時(shí)插件化本身就是一項(xiàng)黑科技技術(shù),有時(shí)候犧牲一些性能,解決一個(gè)問題還是非常值得的。

4.1.2 處理 xml 代碼中引用宿主的資源

xml 中引用宿主資源的問題僅靠編譯時(shí)是無法解決的,因?yàn)?xml 不像 java 代碼一樣可以執(zhí)行邏輯,前面介紹了,xml 在編譯結(jié)束后,資源全部編成了數(shù)值,而我們?cè)诰幾g時(shí)又無法知道未來運(yùn)行在哪個(gè)宿主,值為多少。所以修改 xml 中資源id的工作只能搬到運(yùn)行時(shí)去搞。當(dāng)然也需要在編譯時(shí)做一些事情,輔助運(yùn)行時(shí)的修改操作。

運(yùn)行時(shí)我們需要修改 apk 的 xml 中 0x7f 開頭的資源,將其數(shù)值改為對(duì)應(yīng)當(dāng)前宿主的正確數(shù)值,而通過 xml,我們只能拿到一個(gè)數(shù)值,因此我們可以在插件編譯時(shí)收集插件 xml 中使用的宿主資源所在的 xml 文件以及它們所對(duì)應(yīng)的資源 name,運(yùn)行時(shí)借助前文提到的mapRes方法即可獲取到需要被修改后的值。

開始實(shí)踐

前文介紹過,aapt2 編譯/鏈接后會(huì)生成一個(gè) ap_ 文件,這個(gè)文件中包含了最終會(huì)進(jìn)入插件中的所有編譯后的資源(包括各種 xml、resources.arsc、AndroidManifest.xml ),我們只需要分析這些文件中引用的 0x7f 開頭的資源,根據(jù) R.txt(aapt2生成的一個(gè)文件)找到對(duì)應(yīng)的資源名,將資源名、id 值、所在文件記錄到一個(gè)文件中,一并打包進(jìn)插件 apk 中。

至于如何掃描這些文件中 0x7f 的資源,我們?cè)诓煌A段使用了不同方式,大家可以自行選擇:

  1. 使用 aapt2 命令 dump 文件信息,分析 dump 后的文本內(nèi)容(我們編譯時(shí)是這么做的,簡單粗暴、性能較差、不夠優(yōu)雅);
  2. 根據(jù)文件格式分析對(duì)文件內(nèi)容進(jìn)行解析,找到 0x7f 開頭的資源(比較優(yōu)雅,效率也高,我們運(yùn)行時(shí)是這樣做的)。

總結(jié):

以上,便生成了一個(gè)文件,內(nèi)部存儲(chǔ)了插件 xml 中使用到的宿主資源的信息。大概長下面這樣:

圖片

前文一直在說 xml 中使用的宿主資源,看上面這個(gè)配置文件發(fā)現(xiàn) fileNames 中怎么會(huì)有 resoureces.arsc ?它明明不是 xml 文件?

其實(shí) Android 資源編譯之后,values 相關(guān)的一些資源文件都不存在了,會(huì)直接進(jìn)入到 resources.arsc 中,layout 這類文件還存在,resoureces.arsc 中 layout 指向的正是各種 layout.xml,而 string 等 value 類型的資源指向的是一個(gè)真實(shí)的內(nèi)容。感興趣的同學(xué)可以通過 Android Studio 打開 apk,觀察一下 resources.arsc 中的結(jié)構(gòu)。

4.2 插件安裝時(shí)的工作

前面介紹了在插件編譯時(shí),給 java 代碼中插入了一些邏輯,實(shí)現(xiàn)了插件動(dòng)態(tài)根據(jù)宿主環(huán)境獲取資源 id 的效果。但是 xml 編譯完之后,資源 id 都直接編譯成了數(shù)字,xml 中也無法插入邏輯,因此我們只能在插件運(yùn)行前,根據(jù)宿主環(huán)境進(jìn)行修改。

插件在宿主中運(yùn)行前都有一個(gè)插件安裝的過程,類似于 apk 在 Android 系統(tǒng)中的安裝,因此只需要在每次插件安裝前,或者宿主升級(jí)后,根據(jù)編譯時(shí)生成的配置文件,結(jié)合 mapRes 方法,對(duì)插件中的 xml、resources.arsc 文件進(jìn)行修改即可。

確定了修改時(shí)機(jī)和修改內(nèi)容,接下來就要詳細(xì)介紹怎么修改這些文件了。

5. 修改 apk 中的資源文件

5.1 如何修改 xml、arsc 文件

Android 中的 layout、drawable、AndroidManifest 等文件在編譯成 apk 后,不再是常規(guī)的 xml 文件了,而是新的一種文件格式 Android Binary XML,我們這里稱之為 axml。那么如何修改 axml 文件呢?

所有的文件都有自己的文件格式,程序在讀取文件時(shí)都是讀的 byte 數(shù)組,然后根據(jù)文件格式解析 byte 數(shù)組中每一個(gè)元素的含義。因此我們只需要了解了 axml 的文件格式,按照規(guī)范解析這個(gè)文件,在 byte 數(shù)組中找到其中表示資源 id 的位置,將原本的資源 id 根據(jù) resMap 方法映射出新的值,然后修改 byte 數(shù)組中對(duì)應(yīng)的部分。(非常幸運(yùn),我們這里修改的只是 axml 文件中的一個(gè) 8 位 16 進(jìn)制數(shù),這個(gè)修改不會(huì)導(dǎo)致文件中內(nèi)容的長度、偏移等信息改變,因此直接替換對(duì)應(yīng)部分的 byte 數(shù)組即可。)

resources.arsc 是 apk 的資源索引表,里面記錄了 apk 中所有的資源,對(duì)于 values 類型的資源,資源對(duì)應(yīng)的內(nèi)容會(huì)全部進(jìn)入到 resources.arsc 中,因此我們也需要對(duì)這個(gè)文件進(jìn)行修改(如一個(gè) style 的 parent 是宿主資源,我們就需要修改它)。修改的方法和 xml 類似,只需要按照規(guī)范解析 byte 數(shù)組,找到要修改內(nèi)容的偏移量,替換即可。

關(guān)于 axml、arsc 的文件格式,網(wǎng)上有很多文章介紹,這里就不詳細(xì)敘述了。

Apktool 是一款強(qiáng)大、開源的逆向工具,它可以把 apk 反編譯成源碼,那它肯定也有讀取 apk 中 axml、arsc 的代碼,不然怎么輸出一個(gè)可以編輯的 xml 源碼文件?所以我們可以直接去扒 apktool 中讀取 axml、arsc 的代碼,當(dāng)讀取到 axml 中屬于宿主的 id 時(shí),記錄一下 byte 數(shù)組的偏移量,直接替換對(duì)應(yīng)位置的 byte 子數(shù)組。

aapt2 為我們提供了 dump 資源內(nèi)容的能力,可以幫助我們直接用“肉眼”去看 axml、arsc 的內(nèi)容,借助這個(gè)工具可以讓我們很方便的確認(rèn)修改內(nèi)容,驗(yàn)證修改是否生效。以 30.0 版本的 build-tools 中的 aapt2 為例,它的命令為aapt2 dump apk路徑 --file 資源路徑?。后面不跟--file 資源路徑,會(huì)直接 dump arsc。

以下是 dump 出來的 arsc,可以看到最后一個(gè) style 的 parent 是一個(gè) 0x7f 開頭的宿主資源。

圖片

以下是 dump 出來的 activity_plugin1.xml,可以看到 TextView 中引用了一個(gè)宿主中的資源作為 backgroud。

圖片

5.2 修改 apk 中的 xml/arsc 文件

以上我們知道了如何修改一個(gè) axml、arsc 文件。插件安裝時(shí)我們拿到的是 apk 文件,那么如何修改 apk 中的 axml、arsc 文件呢?

5.2.1 重壓縮方式修改

Apk 其實(shí)就是一個(gè) zip 文件,修改 apk 中的文件內(nèi)容,首先想到的最簡單的方法就是讀取 zipFile 里面的文件,修改之后重壓縮。

java 為我們提供了一套操作 zipFile 的 api,我們可以輕松的將 zip 文件中的內(nèi)容讀取到內(nèi)存,在內(nèi)存中修改之后利用 ZipOutputStream 重新寫入到新的 zipFile 中。

代碼實(shí)現(xiàn)非常簡單。修改成功后,測試發(fā)現(xiàn)是可行的,那我們的第一步就算是成功了,說明運(yùn)行時(shí)動(dòng)態(tài)修改插件的路子是行的通的。

竊喜之于,發(fā)現(xiàn)修改過程十分耗時(shí)。以公司的直播插件為例(直播插件大約 30 MB,屬于比較大的插件了),在 9.0 及其以上的設(shè)備上耗時(shí)約 8s,在 7~8 的設(shè)備耗時(shí)大約 20~40s,在 7.x 以下設(shè)備大約耗時(shí) 10~20s。盡管插件安裝是在后臺(tái)進(jìn)行,適當(dāng)?shù)脑黾右恍r(shí)間是可以接受的,但是幾十秒的耗時(shí)很明顯不可以接受。那我們只能想別的辦法了。

關(guān)于各個(gè)版本的耗時(shí)差異:

Android7.0 開始,官方使用 ZLIB 來提供 Deflater、Inflater 的實(shí)現(xiàn),優(yōu)化了解壓壓縮算法速度(可以查看 Deflater.java、Inflater.java 的注釋)。但是 7.x/8.x 的 ZipFileInputStream 在讀取數(shù)據(jù)時(shí)有一個(gè) 8192 的 BUFSIZE 限制( 8.x 之后移除了這個(gè)限制),導(dǎo)致在讀取數(shù)據(jù)時(shí)循環(huán)次數(shù)增多,效率反而下降。

7.0 開始,ZipFileInpugStream 在讀取數(shù)據(jù)時(shí)是通過 native 方法 ZipFile_read 進(jìn)行的。以下是 android8.0 和 android9.0 中 ZipFile_read 的部分代碼。

圖片

5.2.2 直接修改 apk 的 byte 數(shù)組

Apk 其實(shí)就是一個(gè) zip 文件,關(guān)于 zip 文件的介紹可以參考 Zip 的官方文檔。

簡單總結(jié)一下,zip 文件是由數(shù)據(jù)區(qū)、中央目錄記錄區(qū)、中央目錄尾部區(qū)組成(高版本的 zip 文件增加了新的內(nèi)容)。

  • 中央目錄尾部區(qū):通過尾部區(qū)我們可以知道 zip 包中文件的數(shù)目、中央目錄記錄區(qū)的位置等信息;
  • 中央目錄記錄區(qū):通過尾部區(qū)我們可以快速找到中央目錄記錄區(qū)中的每一條文件記錄,這些記錄主要描述了 zip 包中文件的基本屬性如文件名、文件大小、是否壓縮、壓縮后的大小、文件在數(shù)據(jù)區(qū)中的偏移等;
  • 數(shù)據(jù)區(qū):數(shù)據(jù)區(qū)用來存放文件真實(shí)的內(nèi)容,根據(jù)中央目錄記錄區(qū)記錄的內(nèi)容,可以快速在數(shù)據(jù)區(qū)找到對(duì)應(yīng)的文件元數(shù)據(jù)以及文件的真實(shí)數(shù)據(jù)(如果壓縮,則是壓縮后的數(shù)據(jù))。

開始干活

了解了 zip 文件的格式后,我們只需要按照文件格式協(xié)議,在 apk 中找到我們需要修改的文件數(shù)據(jù)在 apk 中的偏移量,然后結(jié)合前面修改 axml/arsc 文件的方式,直接修改對(duì)應(yīng)的 byte 數(shù)組即可。借助 java 為我們提供的 RandomAccessFile 工具,我們可以快速的文件的任意位置進(jìn)行讀取/寫入。

修改過程中發(fā)現(xiàn),apk 中的 xml 文件大部分是被壓縮的( res/xml 目錄下的一般不壓縮),這就導(dǎo)致我們從 apk 中拿出來的 byte 數(shù)組是 axml 被壓縮后的數(shù)據(jù),我們要對(duì)這段數(shù)據(jù)進(jìn)行修改,需要先利用 Deflate 算法對(duì)它進(jìn)行解壓( zip 文件中一般都是用的 Deflate 算法),然后進(jìn)行修改再壓縮,但是經(jīng)過我們修改后,可能重新壓縮出來的數(shù)據(jù)就與修改前的數(shù)據(jù)長度不匹配了,如果是縮短還好,修改一下文件元數(shù)據(jù)即可,如果文件長度變長可能會(huì)導(dǎo)致后面文件的偏移量都要改變,牽一發(fā)而動(dòng)全身。

好在插件的打包過程我們是可以侵入的,前面介紹“插件編譯時(shí)工作”時(shí),我們?cè)诰幾g時(shí)拿到了需要修改的文件,因此我們只需要控制 apk 打包時(shí)不要對(duì)這些文件進(jìn)行壓縮(事實(shí)上 Android Target30 也要求 arsc 文件不進(jìn)行壓縮)。這樣就很簡單的解決了問題,當(dāng)然會(huì)導(dǎo)致插件包體積的增加。

最終測試在直播插件中,開啟這個(gè)功能會(huì)導(dǎo)致包體積增加 20kb,對(duì)于接近 30mb 體積的直播插件來說,這個(gè)增量是可以接受的,而且也不會(huì)影響宿主包體積。(這個(gè)增量取決于插件有多少 xml 使用了宿主資源,一般插件的增量應(yīng)該都是小于直播插件的。)

改造完成后,經(jīng)測試,直播插件在各個(gè)版本手機(jī)上修改時(shí)長大約在 300~700ms 之間,修改速度提升了 10~90 倍。大部分插件也比直播插件小,耗時(shí)可以保證在 100ms 之內(nèi)。同時(shí)這個(gè)修改過程僅在插件第一次安裝或者宿主升級(jí)時(shí)做,并且是在后臺(tái)完成,所以是完全可以接受的。

責(zé)任編輯:未麗燕 來源: 字節(jié)跳動(dòng)技術(shù)團(tuán)隊(duì)
相關(guān)推薦

2014-11-06 10:31:55

移動(dòng)營銷

2010-09-16 09:26:57

CSS display

2018-07-25 09:37:53

數(shù)據(jù)中心利用率預(yù)測

2013-09-12 10:21:07

Nubo虛擬化MDM

2013-05-16 11:07:37

Android開發(fā)Android應(yīng)用自動(dòng)化測試

2016-09-22 21:42:48

Android鬧鐘移動(dòng)

2012-03-27 22:22:51

iMC基礎(chǔ)IT資源管理

2022-08-12 13:26:14

內(nèi)聯(lián)崩潰TV 端插件化

2011-08-03 10:26:13

Oracle指定nowait

2012-06-20 14:31:44

2014-11-07 14:30:09

統(tǒng)一通信

2010-01-27 15:36:35

Android錄音失真

2014-07-17 00:42:18

Android應(yīng)用測試方案

2020-09-08 11:06:04

機(jī)器學(xué)習(xí)

2015-03-18 10:35:13

虛擬化監(jiān)測虛擬化策略虛擬化解決方案

2010-06-02 10:21:56

Windows 7虛擬化

2021-02-22 18:08:38

農(nóng)業(yè)物聯(lián)網(wǎng)IOT

2016-03-13 17:49:41

2012-03-29 17:42:32

城域網(wǎng)NAT資源池

2012-05-27 16:21:31

IDC華為
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號(hào)