應用性能優化之 VerifyClass
序
爲了加速應用冷啓動過程且不過度涉及業務改動,本文從虛擬機加載類的過程中找到優化項,且與業界的方案作了對比,並實現了半自動化的分析功能。類在使用或實例化之前需要被加載到虛擬機中並進行初始化。整個過程如下圖所示:主要由 LoadingClass 和 InitializingClass 兩部分組合。
LoadingClass 旨在把 Class 從 Dex 加載到虛擬機中,但不涉及類的使用或執行流程。InitializingClass 旨在保證使用類前已經經過了初始化流程,此流程嵌入類的使用或執行過程中。
加載類
DefineClass 主要通過 SetupClass、InsertClass 以及 LoadClass 將一個類加載到虛擬機中,最後返回 mirror:Class 對象指針。
-
SetupClass:設置類的訪問標誌以及 ClassLoader。
-
InsertClass:將類插入到對應 ClassLoader 的 ClassTable 中,以便查找。
-
LoadClass:將類的屬性及方法加載到類中。
類初始化
類的屬性或方法在使用前必須經過類的初始化。
-
InitializeClass:覈驗類、初始化父類、接口方法以及靜態屬性。
-
VerifyClass:覈驗類的合法性,在下一節詳細分析。
覈驗類
VerifyClass 使用 VerifyClassUsingOatFile 或 PerformClassVerification 方法之一去核查 Class。其中 PerformClassVerification 就包含了 Systrace 中耗時 VerifyClass 的 Tag,如下圖所示:
-
VerifyClassUsingOatFile:通過 Oat 文件中的 Class 狀態位去核驗 Class,當狀態位等於 kStatusVerified 時,覈查流程到此爲止,直接快速返回。否則需要進入耗時的 PerformClassVerification 流程。
-
PerformClassVerification:主要覈驗類中的直接方法和虛方法。
-
ComputeWidthsAndCountOps:判斷 PC 值與 dalvik 指令數是否相等。
-
ScanTryCatchBlocks:檢查 Try 語句開始地址、結束地址以及 try 開始操作符的合法性。檢查 catch 中 handler 語句開始操作符的合法性。
-
VerifyInstructions:檢查各種 dalvik 指令,同時將 GC 檢查點插入到括號、switch、throw 指令中。
-
VerifyCodeFlow:檢查每條 dalvik 指令的寄存器以及參數的合法性。
提前發現
從上面的分析可以看出,應該儘可能讓覈查走 VerifyClassUsingOatFile 流程,即通過 Oat 文件狀態位覈查成功。Oat 文件中類的狀態位是什麼以及爲什麼狀態位不等於 kStatusVerified 是問題的突破點。
通過 oatdump 命令去 dump 相應的 odex 文件,可以查看類的狀態位,操作方式如下:
VLOG 默認是不會被打印的,需要動態開啓,開啓的方式可以通過:art::gLogVerbosity.class_linker = true 而打開,因爲本項目需要看到 dex2oat 和其他進程的打印情況,本人是在系統源碼中進行編譯生成的 so,然後,通過 ptrace 注入 so 到 Zygote 的,此方法需要 root 設備,如果只需要查看本進程,應不需要這麼麻煩,具體方法還未探索,但思路應該是一致的。舉例如下,本人碰到的問題是 AppCompat 包中的類不能被覈驗通過。
解決方案
將 Runtime 對象中的 verify_設置成 verifier::VerifyMode::kNone。
-
需要通過 Runtime 對象首地址遍歷查找 verify_屬性,魔改廠商可能帶來兼容性問題。
-
缺少 VerifyClass 過程,可能會後置發現非法指令問題。
-
對 zygote 中值 verify_進行修改將造成 cow 內存消耗。
-
將多出 EnsureSkipAccessChecksMethods 一步處理邏輯,將類中每個函數 flag 進行修改,此處邏輯沒有對單個類進行處理,所以,每個類的每個函數的 flag 都將被無謂修改,如下圖所示:
- 直面問題本身,通過 VLOG 的輸出信息,去修正源碼,具體到本案例,是由於 AppCompat 庫中使用了系統不支持的語句,如下圖所示:
-
本 App 運行環境是在 8.1(API27)上,TextView 沒有方法 setFirstBaselineToTopHeight,所以,因爲指令非法導致類覈驗失敗。(注意 Build.VERSION.SDK_INT 是不會被編譯優化的,它本身是 final 類型,但它的取值是等於 SystemProperties.getInt("ro.build.version.sdk", 0),所以,必須運行時,才能確定)。本人嘗試瞭如下方法:
-
將系統源碼 sdk 中的 Build.VERSION.SDK_INT 值設置成 27 進行編譯出新的 sdk,然後,將此 sdk 覆蓋源生的 android.jar,希望編譯時將 appcompat 中的 Build.VERSION.SDK_INT >= 28 判斷邏輯優化掉,但實際 aar 不會參與 sdk 的編譯,此項只能優化項目自身的邏輯。
-
將 appcompat 源碼下載下來,去掉非法指令,重新編譯成 aar 使用。
-
直接在 android8.1 源碼中編譯 support v7 包使用。
以上兩種方法,能定製自己所需的 aar,甚至能裁剪資源,但碰到了致命的問題:新生成的 aar 不能發佈到 maven 了,這樣的話,需要推動業務修改包名,另一個問題是,如果是項目中的第三方 aar 依賴了 appcompat 的話,問題又會出現。所以,最終通過製作 ASM 插件,將 Build.VERSION.SDK_INT 值設置成固定 27,問題解決了,且使得本項目中 apk size 減少了 22K。
如果是應用需要兼容多個不同版本的 ROM,也可以按照 ROM 版本的不同,使用 App Bundle 下發 “最合適” 的 App。
平臺化
圖片過大,截圖處理
爲了降低方案實施難度,現已將方案平臺化,只要將 apk 拖入網頁中即可看到類覈驗不通過的原因。
轉自:掘金 大力智能技術
https://juejin.cn/post/6951225539990388750
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/rHMC8AmzkxhkY10B3bzPWA