如果能在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
}
})
}
在以上代码实现中,是遍历了所有的方法,对方法内的方法调用、字段访问进行了检测。那么全局变量如何检查呢?
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后,解决思路才慢慢浮出水面。