问题
在一次项目开发中引入广告sdk第三方aar包后发生项目编译失败的情况。由于自身项目有使用到一个线程统一管理的AGP插件,因此在运行编译前会使用javassist处理项目的类文件(插桩、替换线程方法等工作),也就在这个环节中未找到对应的Class对象而发生异常导致项目无法编译通过。
arduino
Execution failed for task ':app:transformClassesWithxxx_xxx_xx_plugin_thread_ThreadOptimizerTransformForDebug'.
> javassist.NotFoundException: com.kwai.video.ksvodplayerkit.KSVodPlayerWrapper
通过编译异常日志可知问题是由于找不到某某类而导致的。然后通过查看依赖的aar包查看时jar包路径下查询并没有找到对应包名,并且在sdk接入说明上没说明需要再依赖其他第三方库等要求。同时可以肯定该报错就是在依赖第三方aar包所引发的问题。

另外也咨询过第三方库开发同学说明问题:其回复是此类是无用且在具体业务中不会影响正常运行(但无用类为啥会在sdk中出现目前还未知)。好在AGP插件有注册白名单的功能:方案可以在配置中将对应包名先做过滤掉处理,不扫描某些包名即可。
但此问题有点奇妙aar包中并不存在该类,但插件扫描类时又能查询到该类就显得非常诡异。当时并想过会是什么原因造成的问题,只觉得这是一个非常神奇的问题,因此想着搞明白原因并且记录学习一下。
这里先直接说问题的主因:aar包内部使用第三方库compileOnly依赖并且import了其中某个类且在主工程中并没有使用implement依赖这个第三方库,因此无法找到这个第三方库中对应的类。
排查原因
通过AGP插件过滤掉对应类之后可编译运行后,查询最终apk的dex文件又可以看到KSVodPlayerWrapper类。

同时该类Find usages在其他类方法中是有执行引用的。

具体类是在core.video.a.d.class中继续搜索其d.class的引用则发现并没有其他地方应用了。


由此推测sdk中打包时包含非内部应该所包含的内容。这里猜测大概率是因为一些公有基础库有相互依赖和调用,在此库中也有所引用但没有剔除干净从而导致携带包含但在外部并不使用的代码。
模拟场景
此外通过
jd-gui工具查看dex2jar文件会查询不到对应KSVodPlayerWrapper类;但通过AndridStudio却可以查询到,如参考上述示例图可知。
为了进一步验证该问题,进行模拟复刻接入第三方SDK包的问题并且看看是否有其他适配方案。
创建aar模块库
这里使用fastjson2模拟这个场景:创建aar模块;分别创建Java和kotlin两种引用使用json的方法类;同时使用implementation引用fastjson2。

这里分别使用java和kotlin两种方式测试最终结果是否存在不同点。
由于fastjson2特殊性解析纯文本是需要加上
JSONReader.Feature.AllowUnQuotedFieldNames才能解析成功。
java
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONObject;
import com.alibaba.fastjson2.JSONReader;
public class JavaTestCompileOnly {
public static void fastjson2Func(){
String text = "{name:\"test\",value:123}";
JSONObject data = JSON.parseObject(text, JSONReader.Feature.AllowUnQuotedFieldNames);
System.out.println("Parsed JSON: " + data.toString());
}
}
kotlin
import com.alibaba.fastjson2.JSON
import com.alibaba.fastjson2.JSONObject
class TestCompileOnly {
companion object{
fun fastjson2Func(){
val text = "{}"
val data: JSONObject = JSON.parseObject(text)
}
}
}
若是生成aar包其实使用implementation或compileOnly是没有什么区别,两者打包都不会包含fastjson2。

查看生成aar包的源代码内容:其中只有调用的测试类,不包含fastjson2相关代码和引用的类方法。

主工程依赖aar运行测试
在主工程中以lib形式依赖aar模块包。

在主工程测试页面分别增加执行aar模块中调用fastjson2的代码。
koltin
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
(rootView as LinearLayout).addView(Button(this).apply {
text = "JavaTestCompileOnly"
isAllCaps = false
setOnClickListener {
JavaTestCompileOnly.fastjson2Func()
}
})
(rootView as LinearLayout).addView(Button(this).apply {
text = "TestCompileOnly"
isAllCaps = false
setOnClickListener {
TestCompileOnly.fastjson2Func()
}
})
}
| Java方法 | Kotlin方法 |
|---|---|
![]() |
![]() |
根据异常可知没有找到fastjson2依赖库中对应类。
查询主工程打包的apk中的class.dex文件可知fastjson2集成也存在问题,只有部分依赖进入到项目中。这和上述情况相似当测试方法中引入了fastjson2的JSON依赖,在最终apk包中确实存在生成的JSON类。但是类存在不完整的情况还是会引发异常情况。

若在主工程中增加fastjson2依赖再编译运行在执行测试方法则不会发生异常报错。
scss
implementation(libs.fastjson2)
同时再观察apk包中Class.dex的fastjson2集成会发现显示的方法数和大小是最终依赖的正确状态,整个fastjson2库都集成到项目中了。

规避异常
像以上用implementation在主工程依赖Fastjson2是表示整个工程需要使用到该功能库才引入的。这也是处理异常情况的一种方案。这里先总结几种规避异常情况可根据项目实际应用择机采取措施。
| 插件功能规避 | 引入缺失库 | 增加替身类 |
|---|---|---|
| 只适用于但编译无法通过时,需要插件内部对其开辟白名单做规避处理;但该方案无法处理运行时可能出现的异常情况 | 对缺失的引用在主工程中进行依赖处理;项目中意外忽略了重要依赖的引用且是必要依赖;如果是非必要依赖其功能在项目中并没有真正使用会增加包大小 | 复刻缺失的类方案让编译运行时能够正常执行到对应类对象规避非必要的异常崩溃问题;替身方法实现可能为空从而导致执行结果没有任何反应。 |
本次问题是比较明确的,当下已知是非必要的功能方法且在实际运行时不会执行。因此这里采用增加替身类来保证项目健壮性规避可能存在的异常。
替身类
在项目中创建编译运行时无法查询到的类对象,这里模拟场景调用fastjson2类无法找到的异常情况。创建和fastjson2依赖库一样的包名路径和类方法,同时保持和原方法一样的方法名和返回值类型,必须和原对象保持一致。在此只实现简单的相应调用方法即可。

java
package com.alibaba.fastjson2;
public class JSON {
public static JSONObject parseObject(String text){
return new JSONObject();
}
}
java
package com.alibaba.fastjson2;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.util.LinkedHashMap;
public class JSONObject extends LinkedHashMap<String, Object> implements InvocationHandler {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
return null;
}
}
至此再运行项目时当调用TestCompileOnly.fastjson2Func()就不会再出现异常崩溃了。
此方案与releaseImplementation和debugImplementation有相似之处,多个环境下都要保持方法都能被正常调用但实际执行的方法内部能力有所不同。
只不过这里增加替身类是为了规避异常而做的适配工作。
总结
到此就能够解释最开始的问题:aar包没有对应的类但在最终打包的apk有相应类。第三方sdk的aar也和模拟场景中一样引用了外部依赖但在主项目中没有最终依赖从而导致类丢失。虽然最终项目(没有特殊插件校验情况下)编译通过了,但是也引入一个不存在在项目中的"空类"。
即使是"空类"可能在内部不会被真实执行到,但其还是存在安全隐患。万一或者可能特殊场景下(hook情况、编译插件预处理等)真的被调用了那必然引发程序崩溃的情况。因此在某些场景下还是需要做一些保护方案以防止不必要异常隐患。

