由于模块化开发,项目引入了阿里ARouter路由组件,由于需要动态修复线上环境app某些异常问题,项目引入了腾讯tinker热修复组件,但是在项目上线这些组件后,出现了一个难以置信的bug,如下图所示:
累计单个版本发生了上万次闪退,单个版本影响用户数千个,但是本地却没法复现,一到线上就有问题,这时就只能依赖bugly记录的线程栈来定位问题了;
闪退方法栈如下:
可以看到是ARouter在执行初始化路由时抛出的异常,执行的方法签名为
com.alibaba.android.arouter.core.LogisticsCenter.com.alibaba.android.arouter.facade.Postcard buildProvider(java.lang.String)(SourceFile:219)
也就是ARouter的LogisticsCenter类中返回值为Postcard类型的 buildProvider方法的第219行抛出的异常,其源码如下:
看起来很正常的从hashMap里get元素,那么为什么会出现异常呢,百思不得其解;
后来通过排查对比所有bugly统计到的异常,发现所有的这些异常都出现在下发了tinker热修复补丁的版本中,而没有下发补丁包的版本从未出现过这种异常,那么为什么tinker热修复会导致ARouter初始化抛异常呢,这时只能先通过反编译补丁包看看能不能找到线索了;
通过对比所有版本下发过的补丁包,发现每个补丁包中都出现了com.alibaba.android.arouter.core.LogisticsCenter这个类:
也就是说build补丁包的时候tinker把ARouter的这个类给diff出来了,通过ARouter的issue页面也看到有人提出过这样的issue https://github.com/alibaba/ARouter/issues/571,
其实也是我的前同事提出的,只是ARouter作者认为ARouter 查找 dex 的位置都是 Android 标准的,目前不会兼容;
但是我想为什么tinker会diff这个类呢,肯定是这个类在编译过程中通过AOP注入了代码,通过对比反编译字节码和源代码发现loadRouterMap()这个方法有不一样,
通过注释也可以知道arouter的gradle插件会注入代码来注册Routers, Interceptors and Providers,那么这个问题真的是tinker diff算法导致的吗,然后通过反编译基准包查看com.alibaba.android.arouter.core.LogisticsCenter这个类,发现无论是基准包还是补丁包,这两个类都进行了代码注入,明明都进行了,那么为什么tinker会dex diff出这个类呢,这里真的可以确定是tinker的问题了吗?答案肯定不是这样的,后来通过仔细对比发现,基准包的这个loadRouterMap()方法体明显比补丁包中的方法体大得多,因为基准包中出现了重复代码注入的现象:
而补丁包中却没有出现重复注入,这时就需要考虑arouter的gradle plugins的代码注入原理了,为什么会重复注入,通过阅读arouter-register的源码,发现它的核心代码注入逻辑是:
通过ASM来操作字节码,利用了gradle 的Transform 这个api,在app编译过程中jar to dex前操作字节码来实现代码注入,乍一看好像没什么问题,但是,既然代码存在重复注入的问题,那么classList这个变量里面的值肯定存在重复的变量了;
通过gradle插件断点调试方式,发现编译过程中Transform 的回调方法被调用了多次,进一步分析发现是多渠道打包的问题,例如build.gradle中新增productFlavors中的变体,然后打包时通过gradlew assemble命令执行gradle打包流程,这时gradle就会编译出各个变体包(渠道包),有多少个渠道(有多少个变体)Transform的回调方法就会被回调多少次,每次回调arouter-register都会扫描ARouter注解生成器生成的java文件编译成的字节码,classList就会添加这些被扫描到的java类名,直接添加没做任何判断,所以就出现了重复现象;
既然找到原因了,那就自己修改逻辑编译一个arouter-register插件替换原来的插件做测试:
interfaces.each { itName ->
if (itName == ext.interfaceName) {
//fix repeated inject init code when Multi-channel packaging
if (!ext.classList.contains(name)) {
ext.classList.add(name)
}
}
}
在原来的代码基础上加入了去重,然后编译一个本地gradle插件,通过本地maven依赖添加的build.gradle中替换掉原来的arouter-register插件,通过先编译基准包,再编译补丁包的方式,发现补丁包中不在有com.alibaba.android.arouter.core.LogisticsCenter这个类,将修改提交到开发分支,准备灰度最后上线;
上线后,通过数个月的线上分析,发现再也没有出现过ARouter初始化时导致的java.lang.IncompatibleClassChangeError异常,问题解决;
综上所述,问题原因在于多渠道打包时,ARouter的arouter-register插件存在重复注入问题,而我们做线上修复时往往可能只打单个渠道的补丁包,这时tinker发现两个方法存在不一致(多渠道基准包注入多次,补丁包只注入一次),所以就diff出了LogisticsCenter这个类,才导致了只有下发了补丁包的版本才会出现这个异常。
针对这个问题,我提交了pr给ARouter,具体可查看:https://github.com/alibaba/ARouter/pull/613