今天动手实践了一下安卓上的热修复机制,现在总结一下、
在Android中要想实现热更新实际上就是对class文件的替换,将目标文件替换成新的class文件即可,但是在Android中并没有直接使用class文件,而是将class重新打包在了一个dex文件中,然后从这个dex文件中去load class.
在Android中,系统为我们提供了这么一个类,叫做DexClassLoader, 从这个类的注释中我们可以看到这个类可以从包含了dex文件的jar和 apk文件中去载类,但是这个类实际上什么都没有做,只是继承了BaseDexClassLoader,BaseDexClassLoader中重写findClass方法
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
List<Throwable> suppressedExceptions = new ArrayList<Throwable>();
Class c = pathList.findClass(name, suppressedExceptions);
if (c == null) {
ClassNotFoundException cnfe = new ClassNotFoundException("Didn't find class \"" + name + "\" on path: " + pathList);
for (Throwable t : suppressedExceptions) {
cnfe.addSuppressed(t);
}
throw cnfe;
}
return c;
}
从上面的代码可以看出,实际上BaseDexClassLoader自己也什么都没有做,而是交给了已 pathList 这个对象去完成加载的任务,pathList 是一个DexPathList 对象,它的findClass实现如下,
public Class findClass(String name, List<Throwable> suppressed) {
for (Element element : dexElements) {
DexFile dex = element.dexFile;
if (dex != null) {
Class clazz = dex.loadClassBinaryName(name, definingContext, suppressed);
if (clazz != null) {
return clazz;
}
}
}
if (dexElementsSuppressedExceptions != null) {
suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions));
}
return null;
}
它实际上会先遍历一个内部一个dexElements数组,然后从数组元素Elment中加载class对象,在这里我们最终找到了我们的classd对象是什么时候加载的了,既然知道了是什么时候加载的,我们就可以通过替换的方式来改变class了,在虚拟机加载一个class的时候,如果一个类已经被加载过了那么后面就不会再加载它,所以可以将包含补丁class的 Elment 放在数组的最前面,让虚拟机先加载补丁class, 这样需要被替换的class就不会被加载了,从而实现了更新。
实际上PathClassLoader也是BaseDexClassLoader的子类,系统就是使用了这个类来实现加载,PathClassLoader和上面提到的DexClassLoader区别如下, 在创建BaseDexClassLoader的对象时候需要填一个 optimizedDirectory 参数,optimizedDirectory是用来缓存我们需要加载的dex文件的,并创建一个DexFile对象,如果它为null,那么会直接使用dex文件原有的路径来创建DexFile对象。同时optimizedDirectory必须是一个内部存储路径,无论哪种动态加载,加载的可执行文件一定要存放在内部存储。DexClassLoader可以指定自己的optimizedDirectory,所以它可以加载外部的dex,因为这个dex会被复制到内部路径的optimizedDirectory;而PathClassLoader没有optimizedDirectory,所以它只能加载内部的dex,这些大都是存在系统中已经安装过的apk里面的。
因为这些类和变量我们在SDK中都无法直接的获取,所以只能通过反射的方式来获取。
下面是核心代码:
package com.baidu.myhotfix;
import android.content.Context;
import android.util.Log;
import java.lang.reflect.Array;
import java.lang.reflect.Field;
import dalvik.system.BaseDexClassLoader;
import dalvik.system.DexClassLoader;
/**
* Created by liuwei64 on 2017/1/14.
*/
public class HotFixUtil {
private static final String TAG = "HotFixUtil";
public static void init(Context context) {
ClassLoader classLoader = context.getClassLoader();
try {
if (classLoader instanceof BaseDexClassLoader) {
Log.i(TAG, "get baseclassloader instance");
Field field = BaseDexClassLoader.class.getDeclaredField("pathList");
field.setAccessible(true);
Object dexPathList = getValue(BaseDexClassLoader.class, classLoader, "pathList");
Object dexElements = getValue(dexPathList.getClass(), dexPathList, "dexElements");
DexClassLoader cl = new DexClassLoader("/storage/sdcard0/classex_dex.jar",
context.getCacheDir().getAbsolutePath(),
null, context.getClassLoader());
Object dexPathListNew = getValue(BaseDexClassLoader.class, cl, "pathList");
Object dexElementsNew = getValue(dexPathListNew.getClass(), dexPathListNew, "dexElements");
Object newElements = combineArray(dexElementsNew, dexElements);
setValue(dexPathList.getClass(), dexPathList, "dexElements", newElements);
}
} catch (Exception ex) {
Log.e(TAG, ex.getMessage());
}
}
public static Object getValue(Class clz, Object instance, String fileName) {
try {
Field field = clz.getDeclaredField(fileName);
field.setAccessible(true);
return field.get(instance);
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
public static void setValue(Class clz, Object instance, String fileName, Object value) {
try {
Field field = clz.getDeclaredField(fileName);
field.setAccessible(true);
field.set(instance, value);
} catch (Exception e) {
e.printStackTrace();
}
}
public static Object combineArray(Object arrayFirst, Object arraySecond) {
if (arrayFirst == null || arrayFirst == null) {
return null;
}
int firstLen = Array.getLength(arrayFirst);
int secondLen = Array.getLength(arraySecond);
int totalLen = firstLen + secondLen;
Object result = Array.newInstance(Array.get(arrayFirst, 0).getClass(), totalLen);
for (int i = 0; i < totalLen; i++) {
if (i < firstLen) {
Array.set(result, i , Array.get(arrayFirst, i));
} else {
Array.set(result, i , Array.get(arraySecond, i - firstLen));
}
}
return result;
}
}
实际上这里还有一个问题要解决,就是CLASS_ISPREVERIFIED的问题,后面自己实现了再来更新!