聊聊Android 热修复Nuwa有哪些坑
然后我说了Nuwa有坑,有人就问Nuwa到底有哪些坑,这篇文章对自己在Nuwa上走过的坑做一个总结,如果你遇到了其他坑,欢迎留言,我会统一加到文章中去。当然有些也不算是Nuwa的坑,算是ClassLoader这种方式进行热修复暴露出来的问题吧。
坑一、混淆有哪些坑
excludeClass没有参考混淆产物mapping.txt,导致无法exclude掉一些不需要处理的类在不混淆的情况下,Nuwa在这一方面是没有什么问题的,但是一旦混淆了,有些类你不想让他注入字节码,它却注入了,这是为什么呢,原因是Nuwa处理的是混淆后的jar,混淆后的jar包名和类名发生了变化,你再使用配置进去的excludeClass是无法主动不进行字节码注入处理的,除非你加进去的是混淆后的类名,但是在没混淆前,我们是根本不知道混淆后的类名的,有人说,我可以先混淆一遍,混淆完了查看一下mapping文件,找到对应的混淆后的类名,加到excludeClass中去,可以是可以,难道你不觉得蛋疼吗,而且这样也很有可能出现差错。那么有没有更好的方法呢?当然有。
混淆后在outputs目录下会产生一个mapping.txt文件,我们能不能解析这个文件,将混淆后的类还原为原来的类名呢,这个文件的大致内容就像下面这样。
android.support.graphics.drawable.AnimatedVectorDrawableCompat$1 -> android.support.a.a.c:
android.support.graphics.drawable.AnimatedVectorDrawableCompat this$0 -> a
629:629:void (android.support.graphics.drawable.AnimatedVectorDrawableCompat) ->
632:633:void invalidateDrawable(android.graphics.drawable.Drawable) -> invalidateDrawable
637:638:void scheduleDrawable(android.graphics.drawable.Drawable,java.lang.Runnable,long) -> scheduleDrawable
642:643:void unscheduleDrawable(android.graphics.drawable.Drawable,java.lang.Runnable) -> unscheduleDrawable
android.support.graphics.drawable.AnimatedVectorDrawableCompat$AnimatedVectorDrawableCompatState -> android.support.a.a.d:
int mChangingConfigurations -> a
android.support.graphics.drawable.VectorDrawableCompat mVectorDrawable -> b
java.util.ArrayList mAnimators -> c
android.support.v4.util.ArrayMap mTargetNameMap -> d
473:503:void (android.content.Context,android.support.graphics.drawable.AnimatedVectorDrawableCompat$AnimatedVectorDrawableCompatState,android.graphics.drawable.Drawable$Callback,android.content.res.Resources) ->
507:507:android.graphics.drawable.Drawable newDrawable() -> newDrawable
512:512:android.graphics.drawable.Drawable newDrawable(android.content.res.Resources) -> newDrawable
517:517:int getChangingConfigurations() -> getChangingConfigurations
android.support.graphics.drawable.AnimatedVectorDrawableCompat$AnimatedVectorDrawableDelegateState -> android.support.a.a.e:
android.graphics.drawable.Drawable$ConstantState mDelegateState -> a
424:426:void (android.graphics.drawable.Drawable$ConstantState) ->
430:434:android.graphics.drawable.Drawable newDrawable() -> newDrawable
439:443:android.graphics.drawable.Drawable newDrawable(android.content.res.Resources) -> newDrawable
448:452:android.graphics.drawable.Drawable newDrawable(android.content.res.Resources,android.content.res.Resources$Theme) -> newDrawable
457:457:boolean canApplyTheme() -> canApplyTheme
462:462:int getChangingConfigurations() -> getChangingConfigurations
仔细观察一下,还是挺有规律的,第一行是原始类名对应的混淆类名,中间用->分割,之后是原始变量名对应的混淆后的变量名,还是用->分割,但是开头缩进了四个空格。最后是方法的混淆,最前面是方法的行数,使用:分割,两个数字分割后再跟一个:,后面就是原始方法名对应的混淆方法名,也是使用->分割。方法和变量都是有类型的。你是不是想到怎么解析了,没错,正则表达式,别急着写代码,在写代码之前我们先看看有没有造好的轮子可以用用,在github上搜一下proguard,没结果。。。再换个关键字,retrace,为什么是retrace呢,因为proguard自带了一个脚本叫retrace,可以从混淆后的异常信息还原为原始类的异常信息。结果出来了,在code中选择java,第一页的最后一条就是。这里我把这个仓库fork到自己的仓库中去了,见地址https://github.com/lizhangqu/retrace
当然不能完完全全的直接用,其实我们用得到的就三个类,一个是ClassMapping.java,一个是MethodMapping.java,还有一个是Retrace.java,至于如何改造,靠你自己了,源码都摆在你面前了你还不会改造?改造后的结果就是传入混淆后的全类名,返回原始的全类目,这样跟excludeClass进行对比就能正确处理了。
没有被修改的类被却被打进了patch,为什么?我们修改了一个复杂一点的类,准备打patch了,发现被打进patch的类怎样不是一个,还包含了一大堆其他的类,为什么呢?打修复包时利用正式包的mapping,修复bug,修改了原先的类,改变的类会改变,但有些类没有改变也会因为混淆的关系产生变化(混淆会剔除一些无用的方法,打修复包时那些无用的方法可能会加上),这就造成了有些类没有修改,但也会出现在修复包中。当然这种情况出现的概率还是挺大的,但是出现的类的数量就不一定了,有多了一个的,也有多了一坨的。。。。怎样解决。。。无解,多就多了呗。。。最多也就是patch包大小变大了。只能尽量避免这种情况的发生,比如打修复包的时候不要修改原有的缩进,一不小心手贱重新进行格式化,可能原来的代码没有格式化,你这么一格式化,整个类都发生了变化,包括这个类的内部类,这样patch的类的数量就会爆增。所以打patch的时候应该尽可能的减少代码的改动。
坑二、Application直接引用的类无法打Patch
为什么会出现这种现象?
出现这种现象的原因是Application类我们没有引用hack.apk,为什么不引用呢,因为在加载Application类之前我们还没加载hack.apk,引用了就会报找不到类的异常,于是这个类不能打,并且在加载hack.apk前用的类都不能引用hack.apk。于是就导致了Application类被打上了那个标记进行了校验。然后Application直接引用的类就无法打patch了,一打patch就会报那个异常Class ref in pre-verified class resolved to unexpected implementation
如何解决这个问题?
直接引用的类不能打patch,但是间接引用的可以打呀,把直接引用的类改成间接引用就ok了,怎么做呢?新建一个中间类,比如PatchUtil,里面有一个init方法,入参是Application,把原来在Application中的逻辑全都转移到PatchUtil中去,然后Application引用PatchUtil类进行调用,最终将一大推直接引用的类变成了间接引用,同时PatchUtil变成了直接引用的类,于是原来一大坨不能打patch的类变成了一个类不能打patch,还是值得的。
坑三、字节码注入的坑
注入失败的原因是什么(混淆和私有构造函数)?
如果代码不混淆,字节码注入是没问题的,所以这个原因还是混淆导致的,混淆之后,很多类没有了< init >,或者< init > 变成了 < clinit >,为什么会这样呢,我估计是剔除了无用方法导致的。还有一个特殊的情况就是私有构造函数,比如单例的情况下就存在只有一个私有构造函数,私有构造函数字节码中没有 < init >,甚至更绝,也没有 < clinit >,这种情况是百分百注入不进去的,而Nuwa的逻辑是判断name是不是等于< init >并且在构造函数的末尾。但是实际测试情况是绝大多数的类混淆之后字节码中都没有< init >或者< clinit >。
如何去解决注入失败的问题?
能不能插一个成员变量呢,实际测试结果是不能。。。具体原因我也不清楚。那么没有构造函数就给它插一个构造函数,但是却又不能显示的插一个构造函数,因为这种情况也可能是有问题的,比如原来就有一个私有构造函数,你再插一个公有构造函数,肯定是有问题的,于是就演变成了给它插一段静态初始化的代码就可以了,在这段代码中直接引用Hack.class。就像这样子
static{
System.out.println(com.package.Hack.class);
}
至于这段代码怎么插。。。我表示用asm插我真的不会插,所以我把Nuwa插字节码的那段代码从使用asm插字节码替换成了用javassist插,至于怎么插,见后文。
如何修改注入的字节码使其找不到类也不会报错Nuwa原来的字节码注入是在构造函数中注入一段这样的代码
System.out.println(Hack.class);
这段代码有什么问题呢,仔细用脑子想一下,万一有些类在加载Hack.class之前就使用了,并且我们一不小心给他注入了这段代码,那么程序运行就会立马crash,于是,我们想能不能不让这段代码执行呢,答案是可能的,通过一个if语句,让它永远进不去这个if语句就可以了,下面是一种方