天天看點

位元組碼引用檢測原理與實戰

本章内容将介紹如何使用位元組碼分析技術在編譯時自動對APP内類、方法、字段的引用進行檢測,并輸出檢測結果供開發人員确認問題,防止由于引用問題導緻的運作時崩潰流入線上,為APP的品質與穩定性保駕護航。

本章中的位元組碼重點研究Java 位元組碼,Java位元組碼(Java bytecode)是Java虛拟機執行的一種指令格式。可以通過javap -c -v xxx.class(Class檔案路徑) 指令來檢視一個Class對應的位元組碼檔案,如下圖所示:

位元組碼引用檢測原理與實戰

位元組碼檢測本質就是對.java或.kt檔案編譯後生成的Class檔案進行相關的分析和檢測。在正式介紹位元組碼分析在引用檢測上的原理與實戰前,先介紹下位元組碼引用檢測的技術預研背景。

整個預研背景需要先從筆者負責的APP--内銷官網APP的軟體架構講起。

内銷官網APP目前共12個子倉,子倉分别獨立編譯成AAR檔案供APP工程使用,軟體架構圖如下圖所示:

位元組碼引用檢測原理與實戰

APP以下,上層淺藍色為業務層,中間綠色為元件層,最下層深藍色為基礎架構層:

業務層:位于架構最上層,根據業務線劃分的業務子產品(比如商城、社群、服務),與産品業務相對應。

元件層:是APP的一些基礎功能(比如登入、自更新)和業務公用的元件(比如分享、位址管理、視訊播放),提供一定的複用能力。

基礎架構層:通過跟業務完全無關的基礎元件(比如三方架構、自行封裝的通用能力),提供完全的複用能力。

官網APP目前主要分3條業務線,多業務版本并行開發是常态,是以子產品化非常必要。

官網APP子產品化的子倉均已AAR形式供APP使用,且存在上層AAR依賴下層AAR的情況。

官網APP子產品化分倉優化工作穿插在各業務版本中,各業務版本并行開發,底層倉庫難免有修改。

官網APP各業務版本并行開發時,一般隻會新拉取目前版本需要修改代碼的倉庫,其他倉庫均繼續依賴老版本的AAR。

假設以下場景:

官網APP5.0版本開發過程中,由于HardWare倉沒有業務修改,是以繼續使用上個版本4.9.0.0的HardWare(版本開發過程中一般隻會重新拉取需要修改的倉庫,無需修改的倉庫會繼續使用老版本),但Core倉有代碼修改,是以拉取了新的5.0分支,并修改了相關代碼,删除了CoreUtils類中的某個fun1方法,如下圖所示:

位元組碼引用檢測原理與實戰
注:硬體檢測子產品v4.9.0.0版本AAR中用到了核心倉 CoreUtils.class中的fun1方法,其他倉包括主APP工程均未使用到該fun1方法。

請大家思考下,以上場景項目編譯是否會有問題?

答:編譯無問題

APP主倉依賴的是4.9.0.0版本的HardWare倉編譯後的AAR檔案,這個AAR檔案早在4.9版本就編好沒動,是以HardWare倉沒有編譯問題;

APP主倉依賴的是5.0.0.0版本的Core倉,HardWare依賴的是4.9.0.0版本的Core倉,最終編譯會取Core倉的高版本5.0.0.0版本參與APP工程編譯,App倉沒有使用被删除的fun1方法,也不存在編譯問題。

以上場景項目編譯完成後運作過程中是否會有問題?

答:有問題。

在APP運作到HardWare倉調用了CoreUtils類中fun1方法的情況下就會出現運作時崩潰:Method Not Found。

因為最終參與APP工程編譯的是5.0.0.0版本的Core倉,該版本已經删除了fun1方法,是以會出現運作時錯誤。

真實案例:

1)找不到方法

位元組碼引用檢測原理與實戰

2)找不到類

位元組碼引用檢測原理與實戰

所幸以上問題均在開發、測試階段發現并及時修複掉了,如果流到線上,就是運作到某功能時的必崩場景,将會非常嚴重。

如果你負責的APP的所有module均是源碼依賴,一般情況下如果存在引用問題,編譯器會進行提示,是以一般情況下無需擔心(除非依賴的底層sdk存在引用問題),但如果是類似官網這樣的軟體架構,則需要重點注意。

本地測試過程中已出現過引用問題導緻的運作時異常,這種運作時異常的檢測隻靠人工是不夠的,必須要有自動化的檢測工具來進行檢查。傳統的findBugs、Lint等是代碼靜态檢測工具,是無法檢測出這種潛在的引用問題導緻的運作時異常的,靜态代碼檢測無法解決此問題。是以自研自動化的檢測工具迫在眉睫!

如果能在APK編譯期間,通過自動化工具對所有JAR、AAR包中每個類做一遍檢測,檢測其中調用的方法、屬性的使用是否存在引用問題,将檢測出疑似問題的地方在編譯時進行提示,有必要的情況下直接報錯終止編譯,并輸出錯誤日志來提醒開發人員檢查,防止問題流入線上出現運作時異常。

原理:各子倉的Java類(或Kotlin類)在編譯成AAR或JAR後,AAR、JAR中會有所有類的Class檔案,我們實際上就是需要對編譯後生成的Class檔案進行分析。

如何對Class檔案進行位元組碼分析?

這裡推薦使用 JavaAssist 或 ASM,我們知道Android編譯過程主要通過Gradle來控制的,要想分析Class檔案位元組碼,我們需要實作自己的Gradle Transform,在Transform裡對Class位元組碼進行分析,這裡我們直接做成Gradle插件。

在編譯期間自動分析Class位元組碼是否存在方法引用、屬性引用、類引用找不到或者目前類無權通路的問題,發現問題停止編譯,并輸出相關日志,提醒開發人員分析,并支援對插件的配置。

到這裡,整個方案的主體架構就比較清晰了,如下圖所示:

位元組碼引用檢測原理與實戰

方法和屬性引用問題的識别:

如何識别一個方法引用存在問題?

該方法被删除,找不到相關方法名;

找不到方法簽名相同的方法,主要是指方法的入參數量、入參類型無法比對;

方法是非public方法,目前類無權限通路該方法。

如何識别一個屬性(字段)引用存在問題?

該屬性被删除,找不到相關屬性、字段;

屬性是非public屬性,目前類無權限通路該屬性。

權限修飾符說明:

位元組碼引用檢測原理與實戰

方法和屬性引用的位元組碼檢測:我們可以利用JavaAssist、ASM等支援位元組碼操作的庫來實作對所有類中方法、屬性的掃描,并分析方法調用、屬性引用是否存在引用問題。

以下代碼均已Kotlin編寫,實作Gradle Plugin、Transform具體過程省略,直接上檢測功能的代碼。方法、字段引用檢測:

在以上代碼實作中,是周遊了所有的方法,對方法内的方法調用、字段通路進行了檢測。那麼全局變量如何檢查呢?

例如以上代碼中,mTest1屬性的值以及mTest2屬性的值應該如何做檢測?這個問題困擾筆者良久。在JavaAssist、ASM中均未能找到擷取屬性目前值的相關的Api、也未能找到Class位元組碼直接分析屬性值的相關思路以及資料。

在研究了Class位元組碼相關知識,并做了大量的實驗,打了大量的Log後,解決思路才慢慢浮出水面。

我們先來看下BillActivity的一段位元組碼:

位元組碼引用檢測原理與實戰

在這裡我們找到了定義的mTest1這個全局變量,然後大家可以注意到,右邊Method中出現了一個init方法,實際上Java 在編譯之後會在位元組碼檔案中生成 init 方法,稱之為執行個體構造器,該執行個體構造器會将語句塊,變量初始化,調用父類的構造器等操作收斂到 init 方法中。那我們的mTest2這個全局變量呢?

搜尋後發現mTest2實際上是在static代碼塊中,這裡似乎mTest2指派并沒有被方法包裹,如下圖所示:

位元組碼引用檢測原理與實戰

實際上通過查閱大量資料後得知,Java 在編譯之後會在位元組碼檔案中生成 clinit 方法,稱之為類構造器,類構造器會将靜态語句塊,靜态變量初始化,收斂到 clinit 方法中。上圖通過javap檢視Class位元組碼中未顯示clinit方法是因為javap未對此進行相關的适配展示而已。

通過實驗Log發現mTest2的初始化确實出現在clinit方法中,且在ASMPlugin的ByteCode中檢視跟上圖相同的位元組碼,展示為帶有clinit方法辨別的位元組碼,如下圖所示:

位元組碼引用檢測原理與實戰

研究到這裡,我們實際也就知道了mTest1和mTest2的指派實際都發生在init和clinit方法中。是以我們前面周遊類中所有方法來檢測方法和屬性的引用檢查是可以覆寫到全局變量的。

問題到這裡似乎已經全部完美解決了,但我在全局變量的代碼這裡看了幾眼後,又發現了新的問題:

我們前面隻關心了TAG這個屬性和getFormatProvinceInfo這個方法的引用是否存在問題,但我們沒有對CreateNewAddressActivity這個類本身做引用檢查,假設這個類是private的,這裡依然會有問題。是以我們引用檢查不能忘記對類引用的檢查。

如何識别一個類引用存在問題?

該類被删除,找不到相關類;

類是非public的,目前類無權限通路該類。

類引用檢查

到這裡本次位元組碼引用檢測的原理以及實戰就介紹完了。

在内銷官網的buildSrc中實作了引用檢測功能後,得知其他APP很多都已做了子產品化,聯想到其他APP可能也采用類似官網的子產品化架構,也會存在類似痛點,反思目前技術實作并不具備通用的接入能力,深感這件事其實并沒有做完,在解決自身APP痛點後需要橫向賦能其他APP,解決大團隊所面臨的痛點,所有才有了後面的獨立Gradle插件。

如果需要在編譯期間進行引用檢測的APP子產品,歡迎大家接入我開發的這款位元組碼引用檢測的Gradle插件。

1)獨立Gradle插件,友善所有APP接入; 2)支援常用的開發配置項,支援插件功能開關、異常跳過等配置; 3)對Java、Kotlin編譯後的位元組碼進行引用檢查,能在CI、Jenkins上編譯APK包發現引用問題時,編譯報錯并輸出引用問題的具體資訊供開發分析、解決。
1)方法引用檢測; 2)屬性(字段)引用檢測; 3)類引用檢測; 4)插件支援常用配置,可開可關。

比如能檢測出Class Not Found \Method Not Found或者Field Not Found 的問題。整個插件在編譯期間運作時間很短,以内銷官網APP為例,該插件在APP編譯期間運作時間在 2.3秒左右,速度很快,不必擔心會增加編譯耗時。

在主工程根目錄build.gradle中添加依賴:

在APP工程的build.gradle中使用插件并設定配置資訊:

Enable:是否打開引用檢查功能,如果為false,則不進行引用檢查

StrictMode:嚴苛模式開啟時,發現引用異常直接中斷編譯(嚴苛模式關閉時,隻會将異常資訊打在編譯過程的日志中,發現引用問題不會終止編譯)。

建議:Jekins或CI上打Release包時build.gradle中配置的enable和strictMode都設定為true。

Check:需要檢測的包名,一般隻配置檢查目前APP包名即可,如需對依賴的第三方sdk等做檢查,可根據需要進行配置。

NotWarn:發現引用問題不報錯的白名單,在開發人員檢查插件報錯的問題并認定實際不會導緻崩潰後,可将目前引用不到的類名配置在這裡,可跳過檢查。如A類引用不到B類中的某個方法,可将B類的類名配置在這裡,将不會報錯。

内銷官網APP将org.apache.http以及com.core.videocompressor.VideoController加入到了不報錯白名單中。org.apache.http 實際用的是Android系統中的包,該包并沒有參與APK編譯,如果不加該配置項,則會報錯,但實際運作不會出錯。

位元組碼引用檢測原理與實戰

com.core.videocompressor.VideoController 該項不加的話會報錯:FileProcessFactory中引用不到CompressProgressListener類。排查下FileProcessFactory代碼,FileProcessFactory類的138行 調用了convertVideo方法,最後一個listner參數傳的null。

位元組碼引用檢測原理與實戰

該類的位元組碼Class檔案如下,會自動對converVideo最後一個入參null進行強制類型轉換:

位元組碼引用檢測原理與實戰

而這個CompressProgressListener并不是public的,是預設的package。而且FileProcessFactory類與CompressProgressListener不在同一個package下,是以會報錯。但實際運作時并不會崩潰,是以需要将其類名加入到不報錯的白名單中。

如果在插件使用過程中遇到不應報錯的案例,可以通過白名單控制進行跳過,同時希望将案例回報給我,我這邊對案例進行分析并對插件進行疊代更新。

預研過程中由于位元組碼知識較深,且網絡上類似位元組碼插樁、進行代碼生成的的教程較多,但做位元組碼分析的資料太少,是以需要熟悉位元組碼知識并在實踐中慢慢實驗和摸索,細節也需慢慢打磨。

在預研過程中積極思考解決方案的通用性和可配置性,最終開發出通用的Gradle插件,積極推動其他子產品接入,借此次寶貴的機會進行橫向技術賦能,争取大團隊的成功。

目前已有兩個APP接入插件,插件會持續維護并疊代,等插件穩定後規劃內建到CI、Jenkins上。歡迎有需求的APP接入引用檢測的Gradle插件,希望能幫助到存在引用檢測痛點的APP和團隊。

作者:vivo官網商城用戶端團隊-Qi Haoxin

分享 vivo 網際網路技術幹貨與沙龍活動,推薦最新行業動态與熱門會議。