本文來自於騰訊bugly開發者社區,未經做者贊成,請勿轉載,原文地址:http://dev.qq.com/topic/583b9e3ee8992c2c2df6e6acjava
早在去年10月份,facebook就發佈了介紹redex的文章,這個聽說能夠直接對apk作處理,既提升啓動性能,又可減小安裝包的利器讓安卓開發者們都心動不已。直到今年4月,redex終於開源了,咱們也第一時間對redex作了研究(有觀衆可能要說我騙人,這都11月了怎麼還第一時間呢?好把這個總結是拖了好久才寫),雖然因爲坑多,最終沒有接入到項目構建中,但受Interdex啓發,在應用冷啓動速度優化方面有了新的收穫。git
PS:本篇提到的冷啓動速度優化,不包括Android 5.0及以上系統github
使用redex的第一個坑就是環境。很遺憾的是這個工具不支持windows系統(用mac開發的壕請忽略),只好裝虛擬機來跑ubuntu。解決了系統,就能夠按照github上的官方指引一步步來了,這裏須要安裝茫茫多的依賴庫和解決若干環境問題,幸虧各類典型issue已經有了解決方案,這裏再也不贅述。ubuntu
Redex的優化項衆多,而且能夠很方便的修改配置文件來選擇須要執行的優化,默認的配置文件以下windows
根據官方的介紹文檔,redex的優化主要有如下幾項:微信
A.內聯。
簡單說就是去除一些多級調用的中間層級,舉個例子:app
func1 -> static func2 -> static func3
優化後就是less
func1 -> static func3
這樣能夠減小函數調用時間和字節碼。除了靜態方法調用,對象引用也有相似優化。函數
B.刪除無用代碼,移除空類。工具
C.對於只有一個實現類的接口或父類,直接用實現類代替。
D.SynthPass
翻譯不能,官方例子,內部類B訪問外部類A的private static變量,compile後實際上是經過生成額外的acces方法來幫助內部類訪問外部類私有成員。這個優化能夠去除額外生成的字節碼,方法至關於把變量的做用域改爲public。
E.字符串縮減,包括提供字節碼層面的混淆能力,相似Proguard,以及DEX文件中metadata的優化,能夠有效縮減安裝包大小。
F.Interdex
須要使用者提供程序啓動時加載類序列做爲配置文件,按此順序調整dex中類的順序,能夠有效提高冷啓動速度,提高幅度在30%左右。優化的原理facebook推測是優化讀取IO和內存(按研究的結果來看其實另有緣由,後面再說)
如此多的優化項累加,想來效果應該很是可觀。但殘酷的現實是,通過對手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構建環境搭建上也會有很大的工做量。
想直接接入redex成本較大,但要咱們直接放棄這些優化空間,心裏也是拒絕的。那麼咱們可否參考facebook的思路,嘗試自行實現一些優化項呢?
在redex中,大部分優化原理都須要解析dex格式,從中還原出引用、繼承關係,加以分析,工做量巨大。但Interdex比較例外,這個優化不須要去分析類引用,它只須要調整Dex中類的順序,把啓動時須要加載的類按順序放到主dex裏,這個工做咱們徹底能夠在編譯過程當中實現,並且這個優化能夠提高啓動速度,優化效果從facebook公佈的數據來看也比較可觀,性價比高。
根據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作排序,修改這個方法便可實現咱們的目的。
一番折騰後,終於實現將啓動時加載的類按順序放到主dex中了,趕快用專項測試跑下數據,啓動過程actLoginA的耗時減小了30%左右,提高效果仍是比較明顯的,數值上與facebook的結論也比較接近。
惋惜沒能高興過久,當我把改動上傳到rdm,用rdm構建的release包作專項測試時,發現並無什麼效果。此時心裏是有點懵x的,難道是專項測試時偶現了偏差?仍是測試時用的參照包和我本地包不是一個version?
仍是我眼花看錯了,實際沒效果?
怎麼辦,前一天寫日報好像已經把優化30%的結果同步出去了,過了一天還能撤回郵件嗎?
冷靜,這個時候不能着急,總之先冷靜下來找找哪裏有時光機。
通過反覆、仔細的驗證,能夠確認的事實是,rdm構建的release包無明顯優化,本地debug包和rdm構建的debug包,都有明顯優化。
手q最終發佈的包必然是release包,只對debug包生效的優化並無什麼做用。而且這個優化的原理咱們也沒有弄清楚,facebook的理論主要是優化IO和內存帶來的速度提高,但前面也提過,從dex文件的結構來看,這個解釋並不能讓人信服。因此還要繼續分析,若是弄明白了爲何release包不生效,也許就能夠推測出優化原理。
首先懷疑的是混淆。Release構建中會作混淆,不少類名都會變化,而咱們優化時用的類加載序列是原始類名,因此在release構建時不能正確的調整順序。嘿嘿,應該是緣由了把,這個好修復,混淆是在dx以前執行的,只要混淆後拿到混淆表,把類加載序列裏的類名替換成混淆後的便可。修改後再次測試,結果仍沒什麼變化。
再找緣由,release構建有作ZipAlign優化而debug沒有,是否是這個影響?驗證後排除。
繼續懷疑,是否是release包類加載順序變了?這個按說是不太可能,但抱着死馬當活馬醫的心態試了下,果不其然是匹死馬,排除。
finally,在和hyim、大龍兩位老司機討論時發現了新的嫌疑人,插樁。當時手q使用的熱補丁是classloader方案:反射修改classloader的DexPathList。這個方案爲了解決加載補丁類時verify出錯的問題,須要對全部的類進行插樁,而插樁邏輯只有在release構建纔會執行。在relesse構建中去掉插樁邏輯,再次測試,actLoginA終於有了提高。
插樁的目的是避免安裝時虛擬機作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條件了,因此插不插樁區別不大。
Interdex優化確實能夠明顯提高應用冷啓動速度,原理也比較簡單:把互相引用的類儘可能放在同個dex,增長類的pre-verify。這個思路其實不只僅能夠用在啓動上,一些其餘的關鍵場景也可能用相似方法提高性能。不過這個優化與修改classloader.DexPathList的熱補丁方案有衝突,想要兩者兼得須要選擇其餘補丁方案。
好比zhekai的新方案詳見
QFix探索之路——手Q熱補丁輕量級方案
redex仍是一個很好的工具,有不少優化項能夠挖掘,小型app相對來講應該更容易接入,大型項目會遇到更多的坑,直接接入不易,但也能夠從中瞭解到新的思路。贊開源精神。
保持懷疑和好奇。再牛x的項目,也不能全部理論都是對的,仍是要多實踐。好比Interdex中調整類順序,在這個優化項自己是沒什麼用,而整個研究中這部分是最花費時間的。
(固然長遠來看,瞭解dx執行和自定義dx實現,瞭解dex文件結構都是挺有用的,這波不虧)
更多精彩內容歡迎關注bugly的微信公衆帳號:
騰訊 Bugly是一款專爲移動開發者打造的質量監控工具,幫助開發者快速,便捷的定位線上應用崩潰的狀況以及解決方案。智能合併功能幫助開發同窗把天天上報的數千條 Crash 根據根因合併分類,每日日報會列出影響用戶數最多的崩潰,精準定位功能幫助開發同窗定位到出問題的代碼行,實時上報能夠在發佈後快速的瞭解應用的質量狀況,適配最新的 iOS, Android 官方操做系統,鵝廠的工程師都在使用,快來加入咱們吧!