前言
本章節我們將圍繞《支付寶 App 構建優化解析》另啟新系列,細分拆解客戶端在“程式碼管理”、“證照管理”、“版本管理”、“構建打包”等維度的具體實現方案展開討論,帶領大家進一步瞭解支付寶在 App 構建模組下的持續優化。
本節將主要記錄通過對支付寶 Android 包大小進行壓縮,來改善執行效率和質量。
背景
包大小的重要性已經不需要多說,包大小直接影響使用者的下載,留存,還有部分廠商預裝強制要求必須小於一定的值。但是隨著業務的迭代開發,應用會越來越大,安裝包會不停的膨脹,所以包大小縮減是一個長期的治理過程。
方案
支付寶也一直在優化包大小的方向上努力,我們引入了很多方案。比如:proguard 程式碼混淆,圖片從 png 到 tinypng 到 webp,引入 7zip 壓縮方案等。本方案是有別於上面這些常規的方案,是通過直接刪 dex 中的無用資訊,達到支付寶包大小瞬間減小 2.1M 的目的,並且不影響整個的執行邏輯和效能,甚至還能降低一點執行記憶體。
方案介紹
-
引言
在講詳細方案前得稍微說說整個 Java 系的除錯邏輯。JVM 執行時載入的是 .class 檔案,Android 為了使包大小更緊湊,並且執行更高效發明了 dalvik 和 art 虛擬機器,兩種虛擬機器執行的都是 .dex 檔案(當然 art 虛擬機器還可以同時執行 oat 檔案,不在本文章討論範圍)。所以 dex 檔案裡面資訊的內容和 class 檔案包含的資訊是完全一致的,不同的是 dex 檔案對 class 中的資訊做了去重,一個 dex 包含了很多的 class 檔案,並且在結構上有比較大的差異,class 是流式的結構,dex 是分割槽結構,各個區塊間通過 offset 索引。後面就只提 dex 的結構,不再提 class 的結構。dex 的結構可以用下面這張圖表示:
dex 檔案的結構其實非常清晰,分幾個大塊,header 區,索引區,data 區,map 區。本優化方案優化刪除的就是 data 區中的 debugItems 區域。
-
debugItem 幹嗎用?
首先得知道 debugItem 裡面存了什麼?裡面主要包含兩種資訊:
- 函式的引數變數和所有的區域性變數
- 所有的指令集行號和原始檔行號的對應關係有什麼用呢:第一點其實很明顯,既然叫 debugItem,那麼肯定就是 debug 的時候用的嘍,我們平時在用 IDE 進行斷點和單步除錯的時候都會用到這個區域。第二點作用那就是上報 crash 或者主動獲取呼叫堆疊的時候用的,因為虛擬機器真正執行的時候是執行的指令集,上報堆疊會上報 crash 的對應原始檔行號,此時正是通過這個 debugItem 來獲取對應的行號,可以用下面的截圖比較直觀的瞭解:
上圖是一個比較常見的 crash 資訊,紅框中的行號便是通過查詢這個 debugItem 來獲取的。
-
debugItem 有多大?
在支付寶的場景下,debug 包有 4-5M,release 包有 3.5M 左右,佔 dex 檔案大小的比例在 5.5% 左右,和 google 官方的資料是一致的。如果能把這部分直接去掉,是不是很誘人!
-
debugItem 能直接去掉嗎?
顯然不能,如果去掉了,那所有上報的 crash 資訊就會沒有行號,所有的行號都會變成 -1,會被噴的找不到北。其實在 proguard 的時候就是有配置可以去掉或保留這個行號資訊,-keep SourceFile, LineNumberTable 就是這個作用,為了方便定位問題,基本所有的開發都保留了這個配置。所以,方案的核心思路就是去掉 debugItem,同時又能讓 crash 上報的時候能拿到正確的行號。至於 IDE 除錯,這個比較好解決,我們只要處理 release 包就行了,debug 包不處理。
方案一
核心思路也比較簡單,就是行號查詢離線化,讓本來存放在 App 中的行號對應關係提前抽離出來存放在服務端,crash 上報的時候通過提前抽離的行號表進行行號反解,解決 crash 資訊上報無行號,無法定位的問題。思路雖然簡單,實現的時候還是有點複雜,推動上線也比較曲折,方案經過幾次調整,大概的方案可以用下面一張圖來抽象:
如上圖,核心點有四個:
- 修改 proguard,利用 proguard 來刪除 debugItem (去掉 -keep lineNumberTable),在刪除行號表之前 dump 出一個臨時的 dex。
- 修改 dexdump,把臨時的 dex 中的行號表關係 dump 成一個 dexpcmapping 檔案(指令集行號和原始檔行號對映關係),並存至服務端。
- hook app runtime 的 crash handler,把 crash 時的指令集行號上報到反解平臺。
- 反解平臺通過上報指令集行號和提前準備好 dexpcmapping 檔案反解出正確的行號。
上面這套方案大概花了兩個多星期,擼出了整個 demo,其它幾個改造點都不是很難,難點還是在指令集行號的上報。我們知道所有的 crash 最終都是會有一個 throwable 物件,裡面儲存了整個堆疊資訊,經過反覆的閱讀原始碼和嘗試,發現我要的指令集行號其實也在這個物件裡面。可以用下面一幅簡單的圖示意:
在列印 crash 堆疊資訊前,每個 throwable 都會呼叫art虛擬機器提供的一個 jni 方法,返回一個內部的物件叫 stackTrace 儲存在 Throwable 物件中,這個 stackTrace 物件裡面儲存的便是整個方法的呼叫棧,當然也包括指令集行號,後續獲取實際的堆疊資訊時會再呼叫一個 art 的 jni 方法,把這個 stackTrace 方法丟過去,底層通過這個 stackTrace 物件中的指令集行號反解出正式的原始檔行號。好了,其實很簡單,反射獲取下這個 Throwable 中的 stackTrace 物件,拿到指令集行號,然後,上報。這裡要注意的一個點,比較噁心,每個虛擬機器的實現都不一樣,首先內部物件的名字,有些叫 stackTrace,有些叫 backstrace,然後這個內部物件的型別也非常有,有些是 int 陣列,有些是 long 陣列,有些是物件陣列,但是都會有這個指令集行號,需要針對不同的虛擬機器版本使用不同的方法去解析這個物件,大概要相容4種虛擬機器,4.x, 5.x, 6.x, 7.x,7.x 虛擬機器之後的就統一了。
方案二
上面這套方案其實挺完美的,沒有什麼相容性問題,刪除是直接利用 proguard,獲取指令集行號直接在 java 層獲取,不需要各種 hook,如果只需要處理 crash 的上報,方案一足夠了,但是在支付寶有很多場景是遠遠不夠的。比如:
- 效能,CPU,記憶體異常時呼叫棧。
- native crash 時的 Java 呼叫棧。
上面這些 case 都會涉及到堆疊資訊,方案一中通過反射呼叫 throwable 中的 stackTrace 內部物件根本搞不定,需要換種方法。最開始的思路是嘗試 hook art 虛擬機器,每天翻原始碼,看看可以 hook 的點,最後還是放棄了,一個是擔心相容性問題,另一個是 hook 的點太多,比較慌。最後換了一種思路,嘗試直接修改 dex 檔案,保留一小塊 debugItem,讓系統查詢行號的時候指令集行號和原始檔行號保持一致,這樣就什麼都不用做,任何監控上報的行號都直接變成了指令集行號,只需修改 dex 檔案。可以用下面的示意圖表示:
如上圖:本來每一個方法都會有一個 debugInfoItem,每一個 debuginfoItem 裡面都有一個指令集行號和原始檔行號的對映關係,我做的修改其實非常簡單,就是把多餘的 debugInfoItem 全部刪掉了,只留了一個 debugInfoItem,所有的方法都指向同一個 debugInfoItem,並且這個 debugInfoItem 中的指令集行號和原始檔行號保持一致,這樣不管用什麼方式來查行號,拿到的都是指令集行號。
其中也踩過很多坑,其實光留一個 debugInfoItem 是不夠的,要相容所有虛擬機器的查詢方式,需要對 debugInfoItem 進行分割槽,並且 debugInfoItem 表不能太大,遇到過一個坑就是 androidO 上進行 dex2oat 優化的時候,會頻繁的遍歷這個 debugInfoItem,導致 AOT 編譯比較慢,最後都通過 debugInfoItem 分割槽解決了。
這個方案比較徹底,不用改 proguard,也不用 hook native。不過如果只需要處理 crash 的行號問題,那還是首推方案一,這個方案改動有點大,前期也是每天研究 dex 的檔案結構,摳每一個細節,有比較大的把握時才敢改。
小結
目前該方案已經在支付寶正式上線,前面經過好幾輪的外灰驗證,還是比較穩定的。支付寶整體包大小減少了 2.1M 左右,真實的 dex 大小減少 3.5M 左右。
通過本節內容,我們初步瞭解了支付寶在 Android 客戶端如何通過包大小壓縮以提升 App 執行效率和質量。由於篇幅限制,很多技術要點我們無法一一展開。而相應的技術核心,我們同樣應用在了 mPaaS 並對外輸出,歡迎大家上手體驗:
關於 Android 端包大小壓縮的設計思路和具體實踐,同樣期待你們的反饋,歡迎一起探討交流。
往期閱讀
《開篇 | 模組化與解耦式開發在螞蟻金服 mPaaS 深度實踐探討》