1. 程式人生 > >【騰訊Bugly乾貨分享】Redex初探與Interdex:Andorid冷啟動優化

【騰訊Bugly乾貨分享】Redex初探與Interdex:Andorid冷啟動優化

導語

早在去年10月份,facebook就釋出了介紹redex的文章,這個據說可以直接對apk做處理,既提高啟動效能,又可減少安裝包的利器讓安卓開發者們都心動不已。直到今年4月,redex終於開源了,我們也第一時間對redex做了研究(有觀眾可能要說我騙人,這都11月了怎麼還第一時間呢?好把這個總結是拖了很久才寫),雖然由於坑多,最終沒有接入到專案構建中,但受Interdex啟發,在應用冷啟動速度優化方面有了新的收穫。

PS:本篇提到的冷啟動速度優化,不包括Android 5.0及以上系統

一、redex的使用與坑

1.安裝與使用

使用redex的第一個坑就是環境。很遺憾的是這個工具不支援windows系統(用mac開發的壕請忽略),只好裝虛擬機器來跑ubuntu。解決了系統,就可以按照github上的官方指引一步步來了,這裡需要安裝茫茫多的依賴庫和解決若干環境問題,幸好各種典型issue已經有了解決方案,這裡不再贅述。

2.優化原理與配置

Redex的優化項眾多,並且可以很方便的修改配置檔案來選擇需要執行的優化,預設的配置檔案如下

根據官方的介紹文件,redex的優化主要有以下幾項:

A.內聯。
簡單說就是去除一些多級呼叫的中間層級,舉個例子:

func1 -> static func2 -> static func3

優化後就是

func1 -> static func3

這樣可以減少函式呼叫時間和位元組碼。除了靜態方法呼叫,物件引用也有類似優化。

B.刪除無用程式碼,移除空類。

C.對於只有一個實現類的介面或父類,直接用實現類代替。

D.SynthPass


翻譯不能,官方例子,內部類B訪問外部類A的private static變數,compile後其實是通過生成額外的acces方法來幫助內部類訪問外部類私有成員。這個優化可以去除額外生成的位元組碼,方法相當於把變數的作用域改成public。

E.字串縮減,包括提供位元組碼層面的混淆能力,類似Proguard,以及DEX檔案中metadata的優化,可以有效縮減安裝包大小。

F.Interdex
需要使用者提供程式啟動時載入類序列作為配置檔案,按此順序調整dex中類的順序,可以有效提升冷啟動速度,提升幅度在30%左右。優化的原理facebook推測是優化讀取IO和記憶體(按研究的結果來看其實另有原因,後面再說)

3.實踐中遇到的坑

如此多的優化項累加,想來效果應該非常可觀。但殘酷的現實是,經過對手Q安裝包的處理驗證,redex中還存在不少bug和坑,接入使用的價效比不高:

A.IlegalAccessError

這是redex的一個bug,原因是在內聯優化中,移除中間層的方法時沒有考慮作用域,比如:

Func1 -> public static func2 -> private static func3

會被優化成:

Func1 -> private static func3

而呼叫類又不能訪問其他類的私有方法,導致拋異常(這個問題有不少issue,近期redex似乎已經修復了,還未驗證)。

B.NoClassDefError

一個比較詭異的問題,執行時報這個錯,但反編譯Dex檔案,這個類是存在的,懷疑是redex的bug,github也有少部分類似的issue,原因未明。

C.NoSuchMethodError

一個坑。因為手Q裡很多業務是以外掛機制執行的,部分外掛是非獨立的,也就是和手Q工程一起編譯,並且會引用手Q程式碼,在編譯完成後,這些外掛也分別打包好存放在手q的apk裡。這樣會導致的問題是:
redex在做優化時可能會把手Q部分方法移除,如果外掛剛好引用了這個方法,就出現NoSuchMethodError了。

D.Interdex

這個優化項會完全打亂原有的dex分佈,甚至dex的數量也會發生改變,用來校驗分dex是否注入成功的Foo類,以及補丁patch也被打亂,對啟動時分dex注入,補丁等邏輯都有很大影響。

E.簽名

redex執行後需要對apk重新簽名,而手Q在簽名之後還有一些優化邏輯。

這個時候redex可配置優化項的方便之處就體現出來了。遇到問題時,可以把可疑的優化項遮蔽掉,繼續驗證。可即使如此,遮蔽到最後悲催的發現可用優化項已經不多,優化的效果也不太明顯(安裝包可以減少100k左右,啟動速度方面因為interdex需要較大改動,未嘗試)。僅存的幾個優化項沒經過更細緻的測試也可能存在隱患,而就算只使用這少數優化,在編譯指令碼修改和rdm構建環境搭建上也會有很大的工作量。

二、Interdex,冷啟動速度優化

想直接接入redex成本較大,但要我們直接放棄這些優化空間,內心也是拒絕的。那麼我們能否參考facebook的思路,嘗試自行實現一些優化項呢?

在redex中,大部分優化原理都需要解析dex格式,從中還原出引用、繼承關係,加以分析,工作量巨大。但Interdex比較例外,這個優化不需要去分析類引用,它只需要調整Dex中類的順序,把啟動時需要載入的類按順序放到主dex裡,這個工作我們完全可以在編譯過程中實現,而且這個優化可以提升啟動速度,優化效果從facebook公佈的資料來看也比較可觀,價效比高。

1.如何實現Interdex

根據interdex官方介紹的原理,我們可以知道要實現這個優化需要解決三個問題:如何獲取啟動時載入類的序列?如何把需要的類放到主dex中?如何調整主dex中類的順序?

A.如何獲取啟動時載入類的序列?

redex中的方案是dump出程式啟動時的hprof檔案,再從中分析出載入的類,比較麻煩。這裡我們採用的方案是hook住ClassLoader.findClass方法,在系統載入類時日誌打印出類名,這樣分析日誌就可以得到啟動時載入的類序列了。

B.如何把需要的類放到主dex中?

redex的做法應該是解析出所有dex中的類,再按配置的載入類序列,從主dex開始重新生成各個dex,所以會打亂原有的dex分佈。而在手q中,分dex規則是編譯指令碼中維護的,因此我們可以修改分包邏輯,將需要的類放到主dex。

C.如何調整主dex中類的順序?

開源就是好。Android編譯時把.class轉換成.dex是依靠dx.bat,這個工具實際執行的是sdk中的dx.jar。我們可以修改dx的原始碼,替換這個jar包,就可以執行自定義的dx邏輯了。簡單說下具體修改方法:

這裡需要對dex的檔案格式做一定了解,不再細說,網上有一篇很好的文章,有興趣可以瞭解下
Android逆向之旅—解析編譯之後的Dex檔案格式

借網上的一張圖,dex檔案的基本結構如下:

從dex的檔案格式我們可以知道,dex被據劃分為多個section,一個類的完整資訊也被分散到各個section裡。想從dex中解析一個類必須要先從classDef段找到類定義,從中找到類包含的各種資訊的偏移地址,再從對應地址去讀取資料,所以要調整dex的類排列順序,理論上只需要對classDef段修改即可。

(從這裡看其實類的排列順序對讀取時的記憶體影響應該不大,因為在dex中類的資料並不是連續儲存的)

在dx執行時,最終將dex資料寫入到檔案也是以section為單位逐個寫入,並且每個section寫入前都會執行orderItems做排序,修改這個方法即可實現我們的目的。

2.優化效果

一番折騰後,終於實現將啟動時載入的類按順序放到主dex中了,趕快用專項測試跑下資料,啟動過程actLoginA的耗時減少了30%左右,提升效果還是比較明顯的,數值上與facebook的結論也比較接近。

可惜沒能高興太久,當我把改動上傳到rdm,用rdm構建的release包做專項測試時,發現並沒有什麼效果。此時內心是有點懵x的,難道是專項測試時偶現了誤差?還是測試時用的參照包和我本地包不是一個version?

還是我眼花看錯了,實際沒效果?

怎麼辦,前一天寫日報好像已經把優化30%的結果同步出去了,過了一天還能撤回郵件嗎?

冷靜,這個時候不能著急,總之先冷靜下來找找哪裡有時光機。

經過反覆、仔細的驗證,可以確認的事實是,rdm構建的release包無明顯優化,本地debug包和rdm構建的debug包,都有明顯優化。

3.為啥release不生效?

手q最終釋出的包必然是release包,只對debug包生效的優化並沒有什麼作用。並且這個優化的原理我們也沒有弄清楚,facebook的理論主要是優化IO和記憶體帶來的速度提升,但前面也提過,從dex檔案的結構來看,這個解釋並不能讓人信服。所以還要繼續分析,如果弄明白了為什麼release包不生效,也許就可以推測出優化原理。

首先懷疑的是混淆。Release構建中會做混淆,很多類名都會變化,而我們優化時用的類載入序列是原始類名,所以在release構建時不能正確的調整順序。嘿嘿,應該是原因了把,這個好修復,混淆是在dx之前執行的,只要混淆後拿到混淆表,把類載入序列裡的類名替換成混淆後的即可。修改後再次測試,結果仍沒什麼變化。

再找原因,release構建有做ZipAlign優化而debug沒有,是不是這個影響?驗證後排除。

繼續懷疑,是不是release包類載入順序變了?這個按說是不太可能,但抱著死馬當活馬醫的心態試了下,果不其然是匹死馬,排除。

finally,在和hyim、大龍兩位老司機討論時發現了新的嫌疑人,插樁。當時手q使用的熱補丁是classloader方案:反射修改classloader的DexPathList。這個方案為了解決載入補丁類時verify出錯的問題,需要對所有的類進行插樁,而插樁邏輯只有在release構建才會執行。在relesse構建中去掉插樁邏輯,再次測試,actLoginA終於有了提升。

4.優化原理

插樁的目的是避免安裝時虛擬機器做pre-verify,讓類打上CLASS_ISPREVERIFIED標識。這會導致Interdex優化失效,而系統做pre-verify是為了提升效能,再結合Interdex的實現,綜合來看interdex真正的優化原理就比較明顯了:

將啟動時載入的類放到主dex,提升了這些類的內聚,讓更多的類滿足pre-verify的條件,在安裝時就做了校驗和優化,以減少首次載入的耗時,從而優化冷啟動耗時。

(這個結論也再次證明dex中類排列順序應該不影響效能,因為打不打pre-verify只看類引用關係。去掉啟動類排序邏輯後再次驗證,確實仍有明顯優化效果)

而插樁會導致所有類必然不能打上pre-verify,所以不管怎麼調整類分佈,都沒用。

一個小疑問:手Q剛開始用熱補丁時,為啥沒有發現明顯的actLoginA下降?

原因:手q有多個分dex,並且之前主要是按包名來做分dex,所以主dex中除了主依賴集外,剩餘的很多類可能都已經不滿足pre-verify條件了,所以插不插樁區別不大。

三、總結

  1. Interdex優化確實可以明顯提升應用冷啟動速度,原理也比較簡單:把互相引用的類儘量放在同個dex,增加類的pre-verify。這個思路其實不僅僅可以用在啟動上,一些其他的關鍵場景也可能用類似方法提升效能。不過這個優化與修改classloader.DexPathList的熱補丁方案有衝突,想要二者兼得需要選擇其他補丁方案。

  2. redex還是一個很好的工具,有很多優化項可以挖掘,小型app相對來說應該更容易接入,大型專案會遇到更多的坑,直接接入不易,但也可以從中瞭解到新的思路。贊開源精神。

  3. 保持懷疑和好奇。再牛x的專案,也不能所有理論都是對的,還是要多實踐。比如Interdex中調整類順序,在這個優化項本身是沒什麼用,而整個研究中這部分是最花費時間的。
    (當然長遠來看,瞭解dx執行和自定義dx實現,瞭解dex檔案結構都是挺有用的,這波不虧)

更多精彩內容歡迎關注bugly的微信公眾賬號:

騰訊 Bugly是一款專為移動開發者打造的質量監控工具,幫助開發者快速,便捷的定位線上應用崩潰的情況以及解決方案。智慧合併功能幫助開發同學把每天上報的數千條 Crash 根據根因合併分類,每日日報會列出影響使用者數最多的崩潰,精準定位功能幫助開發同學定位到出問題的程式碼行,實時上報可以在釋出後快速的瞭解應用的質量情況,適配最新的 iOS, Android 官方作業系統,鵝廠的工程師都在使用,快來加入我們吧!