android的热修复网上也是玲琅满目,写这篇文章仅是备忘,大神请绕道。
看了一些热修复的帖子,现在总结下目前主流的热修复技术有两中,基于java虚拟机的热修复(hotfix)和基于Classloader的热修复(tinker)。当然还有其他的目前阿里的Sophix据说是非侵入式,但是不是今天研究重点(没有开源,我也不知道原理)
先看下修复的比较吧。
一,继续虚拟机的热修复
这个 的原理需要了解一点java虚拟机的类加载机制。我们都知道java虚拟机内存模型中有方法区,堆区,占区
我们写好的类的class字节码就在方法区中,方法区中为每个类生成一个方法表,hotfix会用到这张表;new出来的对象就保存在
堆区中;对象调用方法会从方法区中的方法表找到方法压倒方法栈中成为占帧。
那么修复用的原理就是把方法区中的方法字节码替换成已经修改好的,然后去执行。原理很简单实现起来还是很麻烦的,需要在c层去处理。在底层会有一个叫ArtMethod的对象保存方法的描述 ,我们要做的就是替换这个对象,具体实现后续补充吧。
二,基于ClassLoad的修复实现
原理:在android中有两个常用ClassLoader,PathClassLoader加载已安装apk中class,DexClassLoader加载未安装apk或者aar中class.两个有一个共同的父类,BaseDexClassLoader,在BaseDexClassLoader->DexPathList->Element[] dexElements
中存储着apk或者aar中所有dex的集合。class加载类是从头遍历这个集合找到class就返回不会再往下找,这样我们就可以把修改好的dex查在数组的前边,让类加载器选择我们修改好的class(不知道算不算是一个bug)。
具体实现:
if (context == null) {
return;
}
File filesDir = context.getDir("odex", Context.MODE_PRIVATE);
File[] listFiles=filesDir.listFiles();
for (File file : listFiles) {
if(file.getName().startsWith("classes")||file.getName().endsWith(".dex")){
Log.i("INFO", "dexName:"+file.getName());
loadedDex.add(file);
}
}
String optimizeDir = filesDir.getAbsolutePath() + File.separator + "opt_dex";
File fopt = new File(optimizeDir);
if (!fopt.exists()) {
fopt.mkdirs();
}
for (File dex : loadedDex) {
DexClassLoader classLoader = new DexClassLoader(dex.getAbsolutePath(), fopt.getAbsolutePath(), null, context.getClassLoader());
PathClassLoader pathClassLoader = (PathClassLoader) context.getClassLoader();
try {
// -----------------------系统的ClassLoader------------------------------------
Class baseDexClazzLoader=Class.forName("dalvik.system.BaseDexClassLoader");
Field pathListFiled=baseDexClazzLoader.getDeclaredField("pathList");
pathListFiled.setAccessible(true);
Object pathListObject = pathListFiled.get(pathClassLoader);
Class systemPathClazz=pathListObject.getClass();
Field systemElementsField = systemPathClazz.getDeclaredField("dexElements");
systemElementsField.setAccessible(true);
Object systemElements=systemElementsField.get(pathListObject);
// ------------------自己的ClassLoader--------------------------
Class myDexClazzLoader=Class.forName("dalvik.system.BaseDexClassLoader");
Field myPathListFiled=myDexClazzLoader.getDeclaredField("pathList");
myPathListFiled.setAccessible(true);
Object myPathListObject =myPathListFiled.get(classLoader);
Class myPathClazz=myPathListObject.getClass();
Field myElementsField = myPathClazz.getDeclaredField("dexElements");
myElementsField.setAccessible(true);
Object myElements=myElementsField.get(myPathListObject);
// ------------------------融合-----------------------------
Class<?> sigleElementClazz = systemElements.getClass().getComponentType();
int systemLength = Array.getLength(systemElements);
int myLength = Array.getLength(myElements);
int newSystenLength = systemLength + myLength;
// 生成一个新的 数组 类型为Element类型
Object newElementsArray = Array.newInstance(sigleElementClazz, newSystenLength);
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));
}
}
// ---------------------------融合完毕 将新数组 放到系统的PathLoad内部---------------------------------
Field elementsField=pathListObject.getClass().getDeclaredField("dexElements");;
elementsField.setAccessible(true);
elementsField.set(pathListObject,newElementsArray);
这样就完成了dex的插入替换。
比较以上两种方案比较喜欢第二种,应为第一种只能修改方法,第二种可以整个类替换,切适配性很好(只要java不修改这个bug在哪都能用)。