Android Tinker
什么是热修复
在热修复出现之前,一个已经上线的app中如果出现了bug,即使是一个非常小的bug,不及时更新的话有可能存在风险,若要及时更新就得将app重新打包发布到应用市场后,让用户再一次下载,这样就大大降低了用户体验,当热修复出现之后,这样的问题就不再是问题了。
实现原理
Tinker 实现原理:
在 App 运行到一半的时候,所有需要发生变更的 Class 已经被加载过了,在Android 上是无法对一个 Class 进行卸载的。而 Tinker 的方案,都是让 Classloader 去加载新的类。如果不重启,原来的类还在虚拟机中,就无法加载新类。因此,只有在下次重启的时候,在还没走到业务逻辑之前抢先加载补丁中的新类,这样后续访问这个类时,就会 Resolve 为新的类。从而达到热修复的目的。
Tinker 的实现过程更像是在 Qzone 热修复方案上做优化。核心点是性能最优,消耗最低。
经 Tinker 开发人员调研,Qzone 的方案最大挑战在于性能,即Dalvik平台存在插桩导致的性能损耗,Art平台由于地址偏移问题导致补丁包可能过大的问题。为了避免这两个问题,根据 Instant Run 的全量替换新的 Dex 的思路,于是决定将新旧两个Dex 的差异放到补丁包中。
经过调研,BsDiff 算法对 Dex 支持效果不太好,所以,Tinker 开发团队人员自研了 DexDiff 算法。
最终, BsDiff 加载 so 和部分资源文件,DexDiff 加载 Dex文件,以达到性能最优。但是这个方案也有缺点,就是占用 ROM 较大。好吧!现在手机内存都不小,多几十 M 可以接受。
- 优点
补丁包较小,消耗较小;
开发透明,文档丰富。
- 缺点
占用 ROM 较大;
需要重启才能生效。
核心思想
它的核心思想就是根据classLoader的加载机制在应用程序启动的时候把修复好的dex包加在有bug的dex包的前面实现对有bug的类的替换。
首先我们需要在代码里把这个类的bug给修复,然后打出修复后的apk包,并把这个类放入修复后的apk的特定dex里(注:把class放入特定的dex并做出这个拆分包是一项略微麻烦的操作,这里我们只需要知道要把这个dex拿到去替换就行,同时tinker也给我们提供了工具),这样我们就能拿到修复好的含有Test类的dex了,接着就是如何把修复好的dex包放到用户手机上,让classloader去加载修复好的dex了。把dex放入用户手机这一步肯定需要一个放dex的服务器,然后app启动的时候根据版本去服务器请求是否有dex,如果有就下载下来放入特定的目录,然后apk下次启动的时候就可以把修复好的dex插入dexElements数组的前面,这样应用程序通过PathClassLoader去加载类就会优先找到修复好的dex里面的Test类,这样bug就被修复了。
修复步骤
首先拿到修复好的dex文件,创建一个DexClassLoader去加载这个dex文件,拿到系统的classLoader,通过反射获取到它的dexElements数组,然后把dexClassLoader的dexElements插入系统classLoader的dexElements前面,这样我们的系统再去找这个Test类,就会优先找到我们修复包里面的Test类,便达到修复bug的目的。
代码:
public void loadDex(Context context) {
//dex表示已经拿到修复好的dex文件
File dex = context.getDir("dexpath", Context.MODE_PRIVATE);
String optimizeDir = dex.getAbsolutePath() + File.separator + "opt_dex";
File fopt = new File(optimizeDir);
//创建一个DexClassLoader去加载这个dex
DexClassLoader dexClassLoader = new DexClassLoader(dex.getAbsolutePath(), fopt.getAbsolutePath(), null, context.getClassLoader());
//系统的classLoader
PathClassLoader pathClassLoader = (PathClassLoader) context.getClassLoader();
try {
//1.先获取到dexClassLoader里面的DexPathList类型的pathList
Class myDexClazzLoader=Class.forName("dalvik.system.BaseDexClassLoader");
Field myPathListFiled=myDexClazzLoader.getDeclaredField("pathList");
myPathListFiled.setAccessible(true);
Object myPathListObject =myPathListFiled.get(dexClassLoader);
//2.通过DexPathList拿到dexElements对象
Class myPathClazz=myPathListObject.getClass();
Field myElementsField = myPathClazz.getDeclaredField("dexElements");
myElementsField.setAccessible(true);
Object myElements=myElementsField.get(myPathListObject);
//3.拿到应用程序使用的类加载器的pathList
Class baseDexClazzLoader=Class.forName("dalvik.system.BaseDexClassLoader");
Field pathListFiled=baseDexClazzLoader.getDeclaredField("pathList");
pathListFiled.setAccessible(true);
Object pathListObject = pathListFiled.get(pathClassLoader);
//4.获取到系统的dexElements对象
Class systemPathClazz=pathListObject.getClass();
Field systemElementsField = systemPathClazz.getDeclaredField("dexElements");
systemElementsField.setAccessible(true);
Object systemElements=systemElementsField.get(pathListObject);
//5.新建一个Element[]类型的dexElements实例
Class<?> sigleElementClazz = systemElements.getClass().getComponentType();
int systemLength = Array.getLength(systemElements);
int myLength = Array.getLength(myElements);
int newSystenLength = systemLength + myLength;
Object newElementsArray = Array.newInstance(sigleElementClazz, newSystenLength);
//6.按着先加入dex包里面elment的规律依次加入所有的element,这样就可以保证classLoader先拿到的是修复包里面的Test类。
for (int i = 0; i < newSystenLength; i++) {
if (i < myLength) {
Array.set(newElementsArray, i, Array.get(myElements, i));
}else {
Array.set(newElementsArray, i, Array.get(systemElements, i - myLength));
}
}
//7.将新的dexElements数组放入系统的classLoader里面。
Field elementsField=pathListObject.getClass().getDeclaredField("dexElements");
elementsField.setAccessible(true);
elementsField.set(pathListObject,newElementsArray);
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (NoSuchFieldException e) {
e.printStackTrace();
}
}
- 首先拿到修复好的dex文件
- 创建一个DexClassLoader去加载这个dex文件
- 获取到dexClassLoader里面的DexPathList类型的pathList
- 通过DexPathList拿到dexElements对象
- 拿到应用程序使用的类加载器的pathList
- 获取到系统的dexElements对象
- 新建一个Element[]类型的dexElements实例
- 按着先加入dex包里面elment的规律依次加入所有的element,这样就可以保证classLoader先拿到的是修复包里面的类。
- 将新的dexElements数组放入系统的classLoader里面。