位元組碼檢測的解決方案
如果能在APK編譯期間,通過自動化工具對所有JAR、AAR包中每個類做一遍檢測,檢測其中呼叫的方法、屬性的使用是否存在引用問題,將檢測出疑似問題的地方在編譯時進行提示,有必要的情況下直接報錯終止編譯,並輸出錯誤日誌來提醒開發人員檢查,防止問題流入線上出現執行時異常。
原理:各子倉的Java類(或Kotlin類)在編譯成AAR或JAR後,AAR、JAR中會有所有類的Class檔案,我們實際上就是需要對編譯後生成的Class檔案進行分析。
如何對Class檔案進行位元組碼分析?
這裡推薦使用 JavaAssist 或 ASM,我們知道Android編譯過程主要通過Gradle來控制的,要想分析Class檔案位元組碼,我們需要實現自己的Gradle Transform,在Transform裡對Class位元組碼進行分析,這裡我們直接做成Gradle外掛。
在編譯期間自動分析Class位元組碼是否存在方法引用、屬性引用、類引用找不到或者當前類無權訪問的問題,發現問題停止編譯,並輸出相關日誌,提醒開發人員分析,並支援對外掛的配置。
到這裡,整個方案的主體框架就比較清晰了,如下圖所示:
3.1 方法和屬性引用檢測原理
方法和屬性引用問題的識別:
如何識別一個方法引用存在問題?
該方法被刪除,找不到相關方法名;
找不到方法簽名相同的方法,主要是指方法的入引數量、入參型別無法匹配;
方法是非public方法,當前類無許可權訪問該方法。
如何識別一個屬性(欄位)引用存在問題?
該屬性被刪除,找不到相關屬性、欄位;
屬性是非public屬性,當前類無許可權訪問該屬性。
許可權修飾符說明:
方法和屬性引用的位元組碼檢測:我們可以利用JavaAssist、ASM等支援位元組碼操作的庫來實現對所有類中方法、屬性的掃描,並分析方法呼叫、屬性引用是否存在引用問題。
3.2 方法和屬性引用檢測實戰
以下程式碼均已Kotlin編寫,實現Gradle Plugin、Transform具體過程省略,直接上檢測功能的程式碼。方法、欄位引用檢測:
// Gradle Plugin、自定義Transform的部分這裡不做贅述
// 方法引用檢測
// 遍歷每個類中的 每個方法 (包括構造方法 addBy Qihaoxin)
classObj.declaredBehaviors.forEach { ctMethod ->
//遍歷當前類中所有方法
ctMethod.instrument(object : ExprEditor() {
override fun edit(m: MethodCall?) {
super.edit(m)
//每個方法呼叫都會回撥此方法,在此方法中進行檢測
//引用檢查功能
try {
//這裡不是每個方法都需要校驗的,過濾掉 我們不需要處理的 系統方法,第三方sdk方法 等等 只校驗我們自己的業務邏輯程式碼
if (ctMethod.declaringClass.name.isNeedCheck()) {
return
}
if (m == null) {
throw Exception("MethodCall is null")
}
//不需要檢查的包名
if (m.className.isNotWarn() || classObj.name.isNotWarn()) {
return
}
//method找不到,底層會直接拋異常的,包括方法刪除、方法簽名不匹配的情況
m.method.instrument(ExprEditor())
//訪問許可權檢測,該方法非public,且對當前呼叫這個方法的類是不可見的
if (!m.method.visibleFrom(classObj)) {
throw Exception("${m.method.name} 對 ${classObj.name} 這個類是不可見的")
}
} catch (e: Exception) {
e.message?.let {
errorInfo += "--方法分析 Exception Message: ${e.message} \n"
}
errorInfo += "--方法分析異常發生在 ${ctMethod.declaringClass.name} 這個類的${m?.lineNumber}行, ${ctMethod.name} 這個方法 \n"
errorInfo += "------------------------------------------------\n"
isError = true;
}
}
/**
* 成員變數呼叫的分析主要有:
* 變數直接被刪掉後找不到的問題
* private變數的只能定義該變數的類試用
* protected變數的可被類自己\子類\同包名的訪問
* */
override fun edit(f: FieldAccess?) {
super.edit(f)
try {
if (f == null) {
throw Exception("FieldAccess is null")
}
//不需要檢查的包名
if (f.className.isNotWarn() || classObj.name.isNotWarn()) {
return
}
//這裡不用判空,如果field找不到(這個屬性被刪掉了),底層會直接拋異常NotFoundException
val modifiers = f.field.modifiers
if (ctMethod.declaringClass.name == classObj.name) {
//只處理定義在本類中的方法,不然基類裡的方法也會被處理到--會出現本類實際沒訪問基類裡的private變數但報錯的問題
if (ctMethod.declaringClass.name == classObj.name) {
if (!f.field.visibleFrom(classObj)) {
throw Exception("${f.field.name} 對 ${classObj.name} 這個類是不可見的")
}
}
}
} catch (e: Exception) {
e.message?.let {
errorInfo += "--欄位分析 Exception Message: ${e.message} \n"
}
errorInfo += "--欄位分析異常發生在 ${classObj.name} 該類在 ${f?.lineNumber}行,使用 ${f?.fieldName} 這個屬性時\n"
errorInfo += "------------------------------------------------\n"
isError = true
}
}鄭州看心理醫生多少錢http://www.hyde8025.com/
})
}
在以上程式碼實現中,是遍歷了所有的方法,對方法內的方法呼叫、欄位訪問進行了檢測。那麼全域性變數如何檢查呢?
class BillActivity {
...
private String mTest1 = CreateNewAddressActivity.TAG;
private static String mTest2 = new CreateNewAddressActivity().getFormatProvinceInfo("a","b", "c");
...
}
例如以上程式碼中,mTest1屬性的值以及mTest2屬性的值應該如何做檢測?這個問題困擾筆者良久。在JavaAssist、ASM中均未能找到獲取屬性當前值的相關的Api、也未能找到Class位元組碼直接分析屬性值的相關思路以及資料。
在研究了Class位元組碼相關知識,並做了大量的實驗,打了大量的Log後,解決思路才慢慢浮出水面。