该文章首发于美丽无线
本文章包含三部分:
-
业界各方案简介;
-
蘑菇街HotFix:Q-Zone篇,介绍ART Runtime对Q-Zone方案的限制;
-
蘑菇街HotFix:Aceso篇,介绍Aceso在InstantRun方案上的各种优化。
-
业界各方案简介
在Dalvik时代,只有Dexposed跟Q-Zone两家的方案,进入ART时代后各种Android热修复方案如雨后春笋般冒出来。
![](https://img.haomeiwen.com/i2492081/f47bf5dc844e8dc7.png)
1.5 AndFix
其原理是替换了dex_cache中的目标ArtMethod,但是由于Android 7.0的Inline的优化,导致AndFix不能支持Android 7.0,阿里那边是采用的动态部署进行Android 7.0上的HotFix的。AndFix的原理后面会继续分析。
1.6 Amigo
Amigo的基本原理是直接下发新的Dex,资源,so包到客户端,然后开启一个新的ClassLoader加载新的Dex,开启一个新的AssetManager加载新的Resources,可谓暴力而简单。缺点是流量跟磁盘占用比较大,下发一次HotFix相当于安装了两遍APK。
- 蘑菇街HotFix-Q-Zone篇
蘑菇街这边开始做HotFix方案的时候,当时业界只有Q-Zone跟Dexposed方案,后来是采用了Q-Zone的方案的,但是对Q-Zone方案心里不是很有底,所以去看了下ART虚拟机的方法加载机制,从虚拟机层面弄清楚了其可靠性。大致总结如下:
- Dalvik上解释执行:
1.1. field通过field name查找
1.2. method通过签名查找
1.3. 注入前被加载的类不能被patch
1.4. const变量会被优化为常量,不能被patch
1.5. 下载patch后下次启动才能生效 - ART实现AOT优化,
2.1. field/部分method通过偏移查找,修改类结构将带来找错或者找不到的问题。
这样,只要限定死不修改类结构,即只修改函数体,那么理论上Q-Zone方案是不会有问题的。
2.1 Method调用类型
为了确认Q-Zone方案在ART上的可靠性,我们研究了所有类型Method的调用方式,如下:
![](https://img.haomeiwen.com/i2492081/54ca16f615c86859.png)
3.5 为支持super.method()增加的包大小
InstantRun是在一个叫override类去调用被修类的各种方法。但遇到如super.method的情况,它是处理不了的,因为没有办法调用另外一个对象的super.xxx。
为了解决这个问题,InstantRun在原类中增加一个超大的代理函数。这个函数中有一个switch,switch的每个case对应了一个父类方法,也就是说这个类的父类有多少个方法,那么这个switch就有多少个case。然后根据传入的参数来决定要调用父类的哪个方法。
![](https://img.haomeiwen.com/i2492081/b0681dca588fe315.png)
![](https://img.haomeiwen.com/i2492081/062d57282000d644.png)
我们借鉴了Robust的方案,将需要调用super.method()的地方通过字节码处理工具将作用对象换为原对象,并且将override的父类改为被HotFix类的父类,就能够调用原有对象的super方法。例如 假设JustTest类的父类是activity,那在override类也要继承activity,并且将调用super.toString的地方,将作用对象换为JustTest的实例。这里的justTest.super()是伪代码,代表了它字节码层面是这个含义。
![](https://img.haomeiwen.com/i2492081/429f9a47f88a3750.png)
3.6 为兼容super(), this()增加的包大小
InstantRun为了在override类中调用原有类的super()方法和 this()方法,会在每个类中增加一个构造方法。然后在override类中会生成两个方法args 和bodys ,arg返回一个字符串,代表要调用哪个this或super方法,body中则是原来构造函数中的除this或super调用的其他逻辑。当一个类的构造方法被HotFix时,在它的构造方法中会先调用override类的args,将args返回的字符串传递给新生成的构造方法,在新生成的构造方法里会根据字符串决定要调用哪个函数。之后再调用body方法。
![](https://img.haomeiwen.com/i2492081/ee3443c5a7fa5d41.png)
为了兼容this调用,InstantRun会让每个类额外的增加一个构造方法。我们最后选择放弃对构造函数的支持,因为使用InstantRun的增加一个构造函数的方案会使得包大小额外增加1m,另外最重要的构造函数被修的概率很低,咨询了下负责发HotFix的同学,以前从来没有修过构造函数。所以权衡之下,我们决定放弃对构造函数的支持。
3.8 HotFix粒度
我们将HotFix的粒度从class级别降为了method级别,并要求写HotFix的同学在写HotFix代码时,在需要修复的method方法上申明一个annotation。
将HotFix的粒度从class级别降为了method级别有三个好处:
- 如果HotFix出了问题,能尽量降低其带来的负面影响,
- 减少HotFix带来的性能消耗,
- 减少即时生效时的时间窗口大小。
3.9 HotFix类的按需加载问题
InstantRun原有机制是可能导致被HotFix类提前加载到虚拟机中的,这会导致一些问题(比如说一个类的静态方法中有用到mgapplicatiion类的sApp这个静态变量,如果我们加载HotFix的时候,sApp还没赋值,那就会NPE)。
![](https://img.haomeiwen.com/i2492081/a34c8ebf8ad3252c.png)
3.10 性能问题
因为要在override类的对象中去访问原有类的属性,所以必定会涉及到访问权限问题。
InstantRun会在编译期间将:
- 所有的protected以及默认访问权限的方法和字段改为public。可以不用反射,直接访问。
- 对于private的方法,InstantRun会将被HotFix类的所有private方法拷贝到override类中,当override中的方法需要调用被hotfix类的私有方法时,直接调用override类的私有方法就好了。但是对于framework层的protected方法和字段(如activity的protected方法),就只能通过反射去调用(因为我们不能修改framework层的访问权限)。对于所有的private字段也是通过反射去访问。
因此,存在一定的性能问题,我们通过两个手段来优化这个问题:
- 用一个全局的Lru Cache存储查找反射过的字段与方法,这样尽量保证反射只被调用一次,
- 全局将private字段改为public,做到对private字段都以public方式直接访问;
- 最后剩下Framework中的private方法还有JNI的private方法需要反射调用,这些对性能会有影响。
由于篇幅有限,还有不少坑这里不一一列举了。
最后,Aceso经过线上几个版本的验证已经完全稳定,我们决定开源分享出来,欢迎围观。开源地址:
https://github.com/meili/Aceso
网友评论