深度剖析 | 阿里熱修復如何精簡優化補丁資源?

這一年,關於Sophix熱修復咱們陸續作了不少優化和改進,包括:數組

兼容最新Android版本至Android P dp3app

JIT混合編譯的兼容函數

第三方加固的全面兼容工具

新增穩健接入方式性能

三星低版本特殊機型的兼容優化

補丁工具加速與初始化檢查ui

資源補丁深度優化編碼

其餘穩定性和性能的改進spa

Sophix熱修復中的資源修復咱們在《深刻探索Android熱修復技術原理》(在阿里技術公衆號,回覆「熱修復」,便可免費下載)書中已經有過介紹,主要思想就是將新增和修改的資源打包到補丁資源包中,以0x66的包名來從新編排這些資源。對比其餘熱修復須要替換完整資源包,Sophix的增量的資源補丁方案能作到資源補丁最小化,而且運行時無需合成完整資源,實現了性能與空間的最優化。設計

在此基礎上,咱們繼續改進了資源補丁,對resources.arsc中的字符串池進行裁剪,在不損耗運行時性能的狀況下讓補丁包大小精簡到了極致。

resources.arsc結構

resources.arsc文件集結了全部帶id的資源項,其粗略概貌能夠由如下這張圖展示:

clipboard.png

這裏咱們不須要太關注細節,只大體說明一下。每一個arsc文件的開頭是一個類型爲RES_TABLE_TYPE的ResTable_header結構頭,它指定了這個arsc文件所包含的其餘結構,通常來講,只有一個全局字符串池和其餘包資源塊,一般狀況下(Android Studio默認編譯出來的)也僅有一個包,包id爲0x7f,也就是說該包下的全部資源編號都是0x7fXXXXXX。

咱們發現,每一個包中還有兩個字符串池,分別是類型字符串池和資源項字符串池,這兩個字符串池和全局字符串池又有怎樣的關係呢?

類型字符串池只表示類型對應的名稱,像layout、string、color、integer等這些字符串,在arsc中只有一個類型id(好比0、一、二、3等)來表示他們。下面還有例子會詳細解釋。類型字符串池是比較獨立的,並且所佔空間很小,與其餘結構也沒有太大關聯。

而資源項字符串池中存儲的是鍵字符串,與全局字符串池中存儲的是值字符串相對應。這裏的鍵和值就是咱們一般理解中鍵值對(Key-Value)的鍵和值。之因此值字符串放在全局,應該是Android在設計之初打算在一個resources.arsc中的各個包中進行資源值的複用,然而因爲目前默認只有一個0x7f包,天然也沒有複用這一說了。

只看這個結構會比較抽象,咱們舉個例子,對於如下這個字符串資源:

clipboard.png

假設這個資源在編譯進arsc以後,對應的id爲0x7f010000

此時arsc中0x7f包中類型字符串池是

clipboard.png

0x7f包中鍵字符串池是

clipboard.png

arsc文件中的全局值字符串池是

clipboard.png

那麼,在解析這個資源項的時候,因爲它的包id爲0x7f,就會找到這個0x7f包中來解析,類型id爲0x01,表示類型字符串池的第0x01個字符串,也就是這裏的string類型,剩下的0x0000,表示該類型的第0個資源項。

咱們從第0個資源項中解析出它是一個字符串類型的資源(這裏省略解析過程),而且獲得他的key值爲0x1,value的值爲0x3。而從前面列出的信息中能夠看到,鍵字符串池第1個字符串爲app_name,值字符串池的第3個字符串爲MyDemo。由此就能夠獲得這個MyDemo資源的完整信息了。

這裏咱們能夠看出,一個資源中佔空間最大的正是字符串池,其餘結構只是一些索引數字,所佔空間很小,所以若是能對字符串池進行精簡,將節省不少空間。

字符串池的構造

首先,咱們得先弄清字符串池的結構是怎樣的,它的關鍵入口是ResStringPool_header這個結構頭,系統會以經過這個結構頭解析出完整的字符串池。

clipboard.png

接下來咱們從StringPool解析過程的系統源碼入手,探尋其具體的構造。核心解析邏輯在ResStringPool::setTo,簡單起見,如下代碼去掉了與主流程無關的檢查代碼:

clipboard.png

這裏很清楚地展現瞭解析的過程,對ResStringPool的各個字段進行賦值。

clipboard.png

其中有幾個比較重要的字段:

mEntries:字符串偏移數組指針

mStringPoolSize:字符串個數

mStrings:字符串塊的起始地址

mEntryStyles:樣式偏移數組指針

mStylePoolSize:樣式個數

mStyles:全部樣式的存儲的起始地址

mEntries與mEntryStyles保存是都是每一個字符串在字符串塊中的偏移,字符串塊就是全部字符串的集合,以0分割開,經過偏移能夠得到具體的某個字符串值,這個過程體如今另外一個ResStringPool::stringAt函數:

clipboard.png

這裏須要注意的一點是,字符串池中的字符串能夠以UTF8或者UTF16編碼來存儲,不一樣編碼中的保存偏移的方式有所不一樣。這裏僅看UTF16的狀況,參數idx表示咱們要獲取的第幾個字符串,mEntries[idx/sizeof(uint16_t)能夠得到第idx個字符串在字符串池中的偏移off,而後由mStrings+off就能夠得到這個字符串實體的起始位置,接着就能夠由decodeLength方法獲得真正的字符串值。

style即表示字符串的樣式,後面咱們會詳細講到。

經過這個解析過程,咱們能夠獲得這張結構圖,其很好地體現出字符串池的構造:

clipboard.png

精簡思路

咱們的資源補丁方案中,補丁中只包含新增和修改的資源,而生成補丁須要一個新包APK和一箇舊包APK,毫無疑問,這兩種加入補丁包的資源實際上都是屬於生成補丁時的新包中的資源,所以直接拿新包APK中resources.arsc的完整字符串池就能夠做爲補丁的字符串池,咱們最先的資源補丁就是直接採用這種方式。這麼作有一個好處,就是新增和修改的資源用到的字符串索引徹底不須要修改,就能夠正常獲取到字符串池的具體值。可是,因爲字符串池是從完整的新包中直接拿過來的,所以,裏面非新增和修改的資源所用的字符串也直接包含在了其中,而這些字符串對於補丁,是多餘的。所以,咱們須要精簡去除的,正是這些無用的字符串。

具體來講,主要分爲三個步驟:

首先,咱們須要肯定要留下的是哪些字符串。

接着,從新編排留下的有效字符串,使其緊湊對齊,而且從新計算各個字符串相對起始位置的偏移。

最後,修正全部引用字符串的地方,使得補丁資源能夠正確地引用到重排過的字符串。

肯定要留下的字符串

須要留下的字符串,無疑就是補丁資源中使用的字符串,而補丁資源中使用的字符串,就是咱們經過比較新包和舊包,獲得的新增和修改的資源所用到的字符串。具體來講,咱們已經經過比較獲得了一個映射表,裏面記錄了全部新包資源到補丁資源的id映射關係,以下所示:

clipboard.png

這裏須要處理兩個字符串池,全局的值字符串池0x7f和包中的鍵字符串池,其中的無用的字符串和樣式都須要去掉。

對於0x7f包中的鍵字符串,咱們須要收集表中全部資源的鍵,也就是這些資源項的名稱,獲得一個字符串索引值的列表,這個時候獲得的列表,因爲是新包字符串池的索引,所以是零散分佈的。

clipboard.png

咱們能夠直接爲每一個收集到的鍵的字符串索引從新指定一個索引值,由此獲得一張新包索引到補丁包索引的映射表:

clipboard.png

對於全局值字符串池的處理也是相似,不一樣地方在於,咱們須要進一步解析每一個資源項,獲得其對應的具體字符串值,仍然是以這個資源爲例:

clipboard.png

咱們須要找到的,就是app_name在0x7f包鍵字符串的索引,以及MyDemo在全局值字符串中的索引。
另外,咱們還須要處理樣式。樣式是字符串的特殊格式,好比下面的這個資源

clipboard.png

這裏的Demo字符串就擁有加粗的樣式,而某個字符串對應的樣式的在樣式表中的索引值與這個字符串在字符串池中的索引值是同樣的。aapt在編譯的時候也會將帶有樣式的資源所有放到字符串池的最前面。好比有五個字符串具備樣式,這五個字符串就會被默認放到字符串池的前五個,而樣式表也只有五個樣式,分別對應了這前五個字符串。而從第六個字符串之後,就沒有樣式了。

因此,這裏咱們還須要調整樣式表,把收集到的字符串所對應的樣式也一同移動到對應位置。此外,樣式字符串,也就是例子中的b字符串實際上也是保存在字符串池中的,所以,當使用到某個樣式的時候,還須要將該樣式的字符串索引添加到咱們的索引映射表中並從新編排。

從新編排與調整偏移值

咱們用一張示意圖來描述這個編排過程:

clipboard.png

其中深色offset entry的表示補丁中實際有效的字符串所對應的偏移值,能夠看到,其中的新包中entries按照前面安排的映射關係移動到了補丁entries的相應位置,而且entries的偏移值也根據新排布的字符串位置進行了調整。下方的字符串塊strings和樣式塊styles的內容也只保留有效部分,這樣,全部有效字符串緊貼在了一塊兒,並去除了新包中其餘無用的資源,大幅節省了空間。

最後須要從新構造字符串的頭部ResStringPool_header結構,使得其中的各個字段(stringCount、styleCount、stringsStart、stylesStart等)填入正確的值。

這樣,一個有效的補丁字符串池就完整構建好了。這個重排的過程對於鍵值兩種字符串池是徹底相同的。

修正資源引用處

字符串池構建完畢了之後,還須要對資源中使用到這些字符串的地方進行從新索引。顯然,只須要根據這個映射表:

clipboard.png

把原來的老索引值修正爲新索引值就好了。具體來講,就是將資源文件結構中的ResTable_entry(表明資源項)和Res_value(表明具體資源的值)中,類型爲ResStringPool_ref的字段的index值修正過來便可。

clipboard.png

因爲咱們壓縮優化的是resources.arsc中的字符串池,所以須要完整地遍歷每一個補丁資源項,把相應的index作替換。而xml中的資源不須要相應修改,由於xml中使用到的只有arsc裏面的資源id,感知不到id對應的字符串是什麼,因此只要在arsc中處理好,xml天然就能找到id所持有的正確的字符串。

總結

經過這三個步驟,便實現了字符串池的精簡。固然處理過程當中還有有不少零碎的問題,好比引用類型資源的處理、Map資源項和字符串池各個塊的拼接等等,這些都須要十分細緻地處理好,不然都會致使運行時解析格式失敗而崩潰。本文沒有述及這些繁瑣的問題,也是爲了避免由於它們而擾亂了主要處理邏輯,當搞定了主幹後,回頭再收拾這些細枝末節就顯得遊刃有餘了。

精簡後效果是很明顯的,不過具體仍是取決於原始APK中資源字符串的數量以及補丁資源中實際有效的字符串的數量,若是資源字符串較多的話會有很是顯著的優化。咱們遇到最極端的一個例子是,精簡以前帶資源的補丁有4M大小,而精簡以後直接變爲23K!因而可知一斑。

目前Sophix最新版本打包工具的高級選項中已默認開啓這個優化資源補丁選項,馬上使用就能爲你的資源熱修復補丁瘦身。

clipboard.png

固然,還有一些其餘選項開關,是爲了打包的靈活性而設置的,其中有些強烈建議打開的選項咱們已經默認開啓了。

Sophix熱修復中還有許多技術優化點,咱們也在去年7月推出了《深刻探索Android熱修復技術原理》免費電子書,詳細講解了代碼、資源、動態庫的熱修復實現(在阿里技術公衆號,回覆「熱修復」,便可下載)。值此一週年之際,咱們與電子工業出版社合做,計劃在近期出版該書的印刷紙質版,並新增了一些篇章,以方便你們翻閱,敬請期待。

本文做者: 萬壑
閱讀原文
本文來自雲棲社區合做夥伴「阿里技術」,如需轉載請聯繫原做者。

相關文章
相關標籤/搜索