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

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

阿里妹導讀:Sophix 是阿里推出的史上首個非侵入式移動熱更新解決方案,自去年推出已有一年的時間了。這一年來,阿里集團內外成千上萬的app踴躍接入。由於接入簡便,操作流暢,功能可靠,資源佔用極小,Sophix得到了廣大開發者的好評,網上也出現了大量開發者親身實踐的接入文章。

今天,我們選取了其中一個改進點——資源補丁的精簡優化,來詳細介紹一下 Sophix 背後的技術。

這一年,關於Sophix熱修復我們陸續做了很多優化和改進,包括:

  • 兼容最新Android版本至Android P dp3
  • JIT混合編譯的兼容
  • 第三方加固的全面兼容
  • 新增穩健接入方式
  • 三星低版本特殊機型的兼容
  • 補丁工具加速與初始化檢查
  • 資源補丁深度優化
  • 其他穩定性和性能的改進

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

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

resources.arsc結構

resources.arsc文件集結了所有帶id的資源項,其粗略概貌可以由以下這張圖展現:

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

這裡我們不需要太關注細節,只大致說明一下。每個arsc文件的開頭是一個類型為RES_TABLE_TYPEResTable_header結構頭,它指定了這個arsc文件所包含的其他結構,一般來說,只有一個全局字符串池和其他包資源塊,通常情況下(Android Studio默認編譯出來的)也僅有一個包,包id為0x7f,也就是說該包下的所有資源編號都是0x7fXXXXXX

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

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

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

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

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

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

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

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

0x7f包中鍵字符串池是

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

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

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

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

我們從第0個資源項中解析出它是一個字符串類型的資源(這裡省略解析過程),並且得到他的key值為0x1,value的值為0x3。而從前面列出的信息中可以看到,鍵字符串池第1個字符串為app_name,值字符串池的第3個字符串為MyDemo。由此就可以得到這個

MyDemo資源的完整信息了。

這裡我們可以看出,一個資源中佔空間最大的正是字符串池,其他結構只是一些索引數字,所佔空間很小,因此如果能對字符串池進行精簡,將節省很多空間。

字符串池的構造

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

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

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

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

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

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

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

  • mEntries:字符串偏移數組指針
  • mStringPoolSize:字符串個數
  • mStrings:字符串塊的起始地址
  • mEntryStyles:樣式偏移數組指針
  • mStylePoolSize:樣式個數
  • mStyles:所有樣式的存儲的起始地址

mEntriesmEntryStyles保存是都是每個字符串在字符串塊中的偏移,字符串塊就是所有字符串的集合,以\0分割開,通過偏移可以獲得具體的某個字符串值,這個過程體現在另一個ResStringPool::stringAt函數:

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

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

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

通過這個解析過程,我們可以得到這張結構圖,其很好地體現出字符串池的構造:

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

精簡思路

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

具體來說,主要分為三個步驟:

  1. 首先,我們需要確定要留下的是哪些字符串。
  2. 接著,重新編排留下的有效字符串,使其緊湊對齊,並且重新計算各個字符串相對起始位置的偏移。
  3. 最後,修正所有引用字符串的地方,使得補丁資源可以正確地引用到重排過的字符串。

確定要留下的字符串

需要留下的字符串,無疑就是補丁資源中使用的字符串,而補丁資源中使用的字符串,就是我們通過比較新包和舊包,得到的新增和修改的資源所用到的字符串。具體來說,我們已經通過比較得到了一個映射表,裡面記錄了所有新包資源到補丁資源的id映射關係,如下所示:

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

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

對於0x7f包中的鍵字符串,我們需要收集表中所有資源的鍵,也就是這些資源項的名稱,得到一個字符串索引值的列表,這個時候得到的列表,由於是新包字符串池的索引,因此是零散分佈的。

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

我們可以直接為每個收集到的鍵的字符串索引重新指定一個索引值,由此得到一張新包索引到補丁包索引的映射表:

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

對於全局值字符串池的處理也是類似,不同地方在於,我們需要進一步解析每個資源項,得到其對應的具體字符串值,仍然是以這個資源為例:

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

我們需要找到的,就是app_name在0x7f包鍵字符串的索引,以及MyDemo全局值字符串中的索引

另外,我們還需要處理樣式。樣式是字符串的特殊格式,比如下面的這個資源

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

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

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

重新編排與調整偏移值

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

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

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

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

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

修正資源引用處

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

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

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

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

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

總結

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

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

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

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

當然,還有一些其他選項開關,是為了打包的靈活性而設置的,其中有些強烈建議打開的選項我們已經默認開啟了。

Sophix熱修復中還有許多技術優化點,我們也在去年7月推出了《深入探索Android熱修復技術原理》免費電子書,詳細講解了代碼、資源、動態庫的熱修復實現(在阿里技術公眾號,回覆“

熱修復”,即可下載)。值此一週年之際,我們與電子工業出版社合作,計劃在近期出版該書的印刷紙質版,並新增了一些篇章,以方便大家翻閱,敬請期待。

最後,手淘基礎平臺部EMAS平臺誠招Android高級開發工程師/專家,歡迎各位優秀靠譜的小夥伴加入,

查看職位詳情:https://job.alibaba.com/zhaopin/position_detail.htm?positionId=46817

或者發送簡歷至[email protected]


每天一篇技術文章,

看不過癮?

發現更多AI乾貨。


分享到:


相關文章: