高效開發(fā)之編譯插樁
?Labs 導讀
插樁技術(shù)非常有趣也很有價值,學會這項技術(shù)以后,我們就可以隨心所欲地操控代碼,滿足不同場景的需求。很多框架都離不開這個技術(shù),如常見的ButterKnife 注解框架,數(shù)據(jù)庫 ORM 框架、APM性能監(jiān)控、埋點統(tǒng)計等。
和家親是一款智慧家庭綜合服務(wù)入口APP??蛻舳说男阅苤苯佑绊懹脩趔w驗,在這次的和家親APP性能優(yōu)化尤其是啟動至首屏專項優(yōu)化中,使用了Gradle+ASM編譯插樁技術(shù)實現(xiàn)apk全局耗時方法統(tǒng)計,本文以此為例讓你認識“插樁”這個效率利器。
Part 01 編譯插樁
顧名思義,所謂的編譯插樁就是在代碼編譯期間修改已有的代碼或者生成新代碼。
在學習插樁之前,你首先需要了解相關(guān)基礎(chǔ)技術(shù),包括Android打包大致流程、class字節(jié)碼文件結(jié)構(gòu)、gradle Transform task及ASM字節(jié)碼操作框架等。后面會做簡單介紹,若要詳細了解,你可以仔細閱讀參考文獻。
下圖為android編譯插樁示意圖。
字節(jié)碼(Bytecode):“.class”文件的是 Java 字節(jié)碼、“.dex”文件的是 Dalvik 字節(jié)碼。我們這里的ASM插樁方法是操作Java 字節(jié)碼。
使用場景:對于代碼監(jiān)控、代碼修改以及代碼分析這三個場景,一般采用操作字節(jié)碼的方式,如無埋點統(tǒng)計上報、輕量級AOP等。應用到在Android中,可以用來做用行為統(tǒng)計、方法耗時統(tǒng)計等功能。
Part 02 ASM字節(jié)碼框架
ASM是一個java字節(jié)碼操縱框架,它能被用來動態(tài)生成類或者增強既有類的功能。
2.1 class文件
了解ASM框架使用之前,必須先了解下class文件格式,一個完整的 class字節(jié)碼文件包括:
- 魔數(shù)與class文件版本
- 常量池
- 訪問標志
- 類索引、父類索引、接口索引
- 字段表集合
- 方法表集合
- 屬性表集合
為方便查看字節(jié)碼文件,kotlin代碼android studio 有自帶工具tools--show kotlin bytecode,java代碼可以安裝jclasslib查看。
2.2 ASM框架使用
ASM的架構(gòu)主要是采用了訪問者模式來設(shè)計,所謂訪問者模式就是封裝一些作用于某種數(shù)據(jù)結(jié)構(gòu)中的各元素的操作,它可以在不改變數(shù)據(jù)結(jié)構(gòu)的前提下定義作用于這些元素的新的操作。具體在ASM框架中應用就是將.class類文件的內(nèi)容從頭到尾掃描一遍,每次掃描到類文件相應的內(nèi)容時,都會調(diào)用ClassVisitor內(nèi)部相應的方法。該方法會返回一個對應的字節(jié)碼操作對象(比如,visitMethod()返回MethodVisitor實例),通過修改這個對象,就可以修改class文件相應結(jié)構(gòu)部分內(nèi)容,最后將這個ClassVisitor字節(jié)碼內(nèi)容覆蓋原來.class文件就實現(xiàn)了類文件的代碼切入。
2.3 ASM工具
工欲善其事,必先利其器。在使用 ASM 插入字節(jié)碼時,如果你不熟悉字節(jié)碼相關(guān)語法和規(guī)則可能對于插入字節(jié)碼代碼束手無策了。幸好 ASM官方開發(fā)了一款I(lǐng)DE插件,可以將Java代碼 轉(zhuǎn)換成 ASM 字節(jié)碼類型代碼,這樣再使用ASM插入字節(jié)碼時就比較方便了。利用插件ASM bytecode outline輕松查看字節(jié)碼及對應ASM框架代碼。
Part 03 Gradle Transform
Gradle Transform 是 Android 官方提供的在apk編譯打包的流程中將 .class 文件到 .dex 轉(zhuǎn)換這一階段用來修改 .class 文件的一套標準 API。這一應用現(xiàn)在主要集中在字節(jié)碼查找、代碼注入等。
3.1 Transform原理
Transform是android gradle api中的一部分,它可以在android項目的.class文件編譯為.dex文件之前,得到所有的.class文件,在Transform中處理。使用Transform API, 我們完全可以不用去關(guān)注相關(guān)task的生成與執(zhí)行流程, 它讓我們可以只聚焦在如何對輸入的類文件進行處理。
每個Transform其實都是一個gradle task,Android編譯器中的TaskManager將每個Transform串連起來,第一個Transform接收來自javac編譯的結(jié)果,以及已經(jīng)拉取到在本地的第三方依賴(jar、aar),還有resource資源。這些編譯的中間產(chǎn)物,在Transform組成的鏈條上流動,每個Transform節(jié)點可以對class進行處理再傳遞給下一個Transform。我們常見的混淆,Desugar等邏輯,它們的實現(xiàn)如今都是封裝在一個個Transform中,而我們自定義的Transform,會插入到這個Transform鏈條的最前面。
自定義的transform在build控制臺可以看到對應的task,輸出內(nèi)容可以在build\intermediates\transforms\對應目錄找到。
3.2 Transform自定義實現(xiàn)
想要自定義transform,必須實現(xiàn)以下幾個方法:
- getName():返回transform名稱標識
- getInputTypes(): 輸入類型包括倆種,CLASSES 和 RESOURCES分別代表java的class文件和資源文件
- getScopes(): 定義Transform需要處理那些輸入文件
- isIncremental(): 表示是否支持增量編譯,支持增量編譯,可以節(jié)省一些編譯的時間和資源,一個好的transform都應該支持增量編譯
- Transform(): 主要方法,入?yún)ransformInvocation是一個接口,提供一些關(guān)于輸入的基本信息,利用這些接口就可以獲得編譯流程中的class文件進行操作
在apk打包過程中,除了自定義的Transform,還有系統(tǒng)提供原生的一些Transform,每個 Transform 在處理完之后交給下一個 Transform,是一個鏈式結(jié)構(gòu)。下圖為自定義Transform實現(xiàn)apk打包流程中字節(jié)碼插樁的流程示意圖,簡單來說就是以下幾步:
- 篩選符合條件的 Class 文件,其中 Class 有兩種可能的文件來源:jar包和特定目錄;
- 利用ASM框架讀取 Class 文件包含的類信息(例如接口、注解等)進一步篩選符合條件的 Class 文件;
- 對最終符合條件的 Class 做處理(修改字節(jié)碼、插樁等);
- 將產(chǎn)物拷貝至 Transform 的輸出目錄,作為下一個 Transform 的輸入;
Part 04 實戰(zhàn):APK函數(shù)耗時插樁
和家親是智慧家庭綜合服務(wù)入口APP,隨著用戶量的激增,客戶端的性能問題愈加明顯,啟動性能作為APP使用體驗的門面,啟動耗時較長很可能削減用戶使用APP的興趣。在這次的啟動至首屏專項優(yōu)化中,需要查找啟動過程耗時方法并優(yōu)化,由于業(yè)務(wù)復雜及SDK接入眾多,雖然也有原生工具profile,但是用過的都知道存在不易捕獲尤其是啟動階段,且無法輸出調(diào)用堆棧等問題。需要實現(xiàn)一個快速排查高耗時方法的工具,此次優(yōu)化通過Gradle TransForm+ASM方式實現(xiàn)了編譯插樁全局耗時方法統(tǒng)計,輔助啟動優(yōu)化分析,最終啟動到首屏展示耗時從4.5s將至3.2s,啟動提速30%,效果顯著。
4.1 實現(xiàn)思路
在性能優(yōu)化階段,需要函數(shù)耗時統(tǒng)計以解決啟動慢、卡頓等問題。對Android打包過程和自定義Gradle插件了解后發(fā)現(xiàn),java文件會先轉(zhuǎn)化為class文件,然后再轉(zhuǎn)化為dex文件。而通過Gradle插件提供的Transform API,可以在編譯成dex文件之前得到class文件。得到class文件之后,便可以通過ASM對字節(jié)碼進行修改,即可完成字節(jié)碼插樁,插入時間統(tǒng)計打印代碼,大于閾值則輸出調(diào)用堆棧。主要實現(xiàn)以下功能:
- 自定義Gradle插件
- 處理class,在方法出口及入口插入耗時統(tǒng)計
- 文件替換
創(chuàng)建一個buildsrc模塊
在 Android 工程中,buildSrc 是 gradle默認的插件目錄,編譯 gradle的時候會自動識別這個目錄,因此在 buildSrc 下編寫的插件,我們可以直接進行引用。通常我們會使用這種方式進行插件的調(diào)試。創(chuàng)建buildSrc 目錄,配置plugin插件相關(guān)配置及依賴(新版本Gradle plugin已經(jīng)支持kotlin語言編寫)。
注冊Transform
想要使用gradle-transform-api,我們必須要先實現(xiàn)一個gradle插件,然后在插件中注冊一個Transform,同時需要在gradle-plugins目錄的.properties文件聲明插件實現(xiàn)者如:
implementation-class=com.xxx.xxx.SystemTracePluginTest
獲取所有class文件
transform()通過參數(shù)inputs獲取所有class文件,包括源碼編譯后的class文件及三方的jar包。
字節(jié)碼修改及文件寫回
經(jīng)過上面的步驟,我們已經(jīng)到輸入文件,也確定了輸出路徑,現(xiàn)在我們只要來處理這些文件,然后輸出到輸出路徑就可以了。這里需要注意的是,就算你不想修改某個class文件,你也應該將它原樣拷貝過去,否則這個文件就丟失了。
利用ASM框架,在遍歷到方法出口及入口即onMethodEnter、onMethodExit回調(diào)中插入耗時統(tǒng)計字節(jié)碼,相應的字節(jié)碼可以用上面的工具jclaslib或者asm codeoutline查看得到。(以下代碼只是部分示例,細節(jié)完善如之針對部分包名統(tǒng)計、getset方法排除等未在次列出)
應用插件完成插樁
app工程apply plugin ‘pluginname’ ,Gradle task會有對應task name 輸出則Transform task執(zhí)行,運行apk,可以看到插入的自定義耗時統(tǒng)計方法輸出,比如小編在耗時統(tǒng)計方法加入了邏輯,耗時超過自定義閾值logcat打印日志及堆棧信息。
通過插樁的形式,使用apk的時候可以非常清晰的統(tǒng)計出耗時方法,還有調(diào)用堆棧,方便后續(xù)性能優(yōu)化。能夠彌補傳統(tǒng)的profile工具性能分析的一些不足,比如只能捕獲短時間,需要自己尋找長耗時方法等問題。
Part 05 結(jié)語
編譯插樁這個技術(shù)應用場景越來越多,涉及的知識較多,但是相信在你熟悉Android打包流程、class字節(jié)碼文件結(jié)構(gòu)、Gradle Transform API、ASM之后,相信你會覺得插樁so easy,android開發(fā)高手課之編譯插樁又get了一個新技能!在性能優(yōu)化過程中,已經(jīng)不止一次用到編譯插樁的技術(shù)了,除了方法耗時統(tǒng)計,我們還使用插樁加hook代理的方式做大圖監(jiān)控,網(wǎng)絡(luò)監(jiān)控、線程優(yōu)化等工作,例如網(wǎng)絡(luò)數(shù)據(jù)監(jiān)控 的實現(xiàn),就是在 網(wǎng)絡(luò)層通過 hook 網(wǎng)絡(luò)庫方法和自動化注入攔截器的形式,實現(xiàn)網(wǎng)絡(luò)請求的全過程監(jiān)控,包括獲取握手時長,首包時間,DNS 耗時,網(wǎng)絡(luò)耗時等各個網(wǎng)絡(luò)階段的信息。大圖監(jiān)控則是通過hook各大圖片加載庫如Glide、picasso在圖片加載過程增加監(jiān)聽計算圖片大小,針對大圖過濾輸出等。讓我們一起學習“插樁”這個效率利器吧。
參考文獻
[1]https://rebooters.github.io/2020/01/04/Gradle-Transform-ASM-%E6%8E%A2%E7%B4%A2/
[2]https://cloud.tencent.com/developer/article/1399805
[3]https://time.geekbang.org/column/intro/142?tab=catalog?