在上篇博客中,我们初步了解了Android热修复的基本流程,具体可以看我的博客Android热修复(Hot Fix)案例全剖析(一),那么本篇博客,我将为大家全面剖析Android热修复的实现案例。
1.将下载的修复补丁拷贝到应用的内部缓存目录中
在上一篇文章中,我们已经生成了用于修复Bug的classes2.dex补丁包,通常我们会在APP后台子线程中自动调用热修复接口,并下载修复补丁,这里为了方便演示,我们把已经下载好的dex补丁文件放到SD卡中,然后将下载的修复补丁拷贝到应用的内部缓存目录中cacheDir,之所以这样做是因为下一步我们需要使用类加载器ClassLoader在内部缓存中加载classese.dex包。下面是我写的一个将classes2.dex包拷贝到内部缓存目录中的方法。
/**
* 修复方法
*/
private void castielFixMethod() {
// 创建一个内部缓存目录,把我们SD卡中的"classes2.dex"文件拷贝到内部缓存目录中cache
File fileSDir = getDir(MyConstants.DEX_DIR, Context.MODE_PRIVATE);
String name = "classes2.dex";
String filePath = fileSDir.getAbsolutePath() + File.separator + name;
File file = new File(filePath);
if (file.exists()) {// 判断是否已经存在dex文件
Log.i("WY", "已经存在dex文件");
file.delete();
}
// 通过IO流将dex文件写到我们的缓存目录中去
InputStream is = null;
FileOutputStream fos = null;
// 版权所有,未经许可请勿转载:猴子搬来的救兵http://blog.csdn.net/mynameishuangshuai
try {
is = new FileInputStream(Environment.getExternalStorageDirectory());
fos = new FileOutputStream(filePath);
int len = 0;
byte[] buffer = new byte[1024];
while ((len = is.read(buffer)) != -1) {
fos.write(buffer, 0, len);
}
File f = new File(filePath);
Log.i("WY", "filePath:" + f.getAbsolutePath());
if (f.exists()) {
Toast.makeText(this, "新的dex文件已经覆盖", Toast.LENGTH_LONG).show();
}
// 动态加载修复dex包
FixDexUtils.loadFixedDex(this);
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
fos.close();
is.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
2.实现热修复工具类
这里首先给大家普及一下类加载的原理:
在Android系统启动的时候会创建一个Boot类型的ClassLoader实例,用于加载一些系统Framework层级需要的类。由于Android应用里也需要用到一些系统的类,所以APP启动的时候也会把这个Boot类型的ClassLoader传进来。此外,APP也有自己的类,这些类保存在APK的dex文件里面,所以APP启动的时候,也会创建一个自己的ClassLoader实例,用于加载自己dex文件中的类。
ClassLoader去加载Dex文件,首先Dex文件是放在/data/apk/packagename~1/base/apk,由于apk是一个类似于压缩包的东西,Android其实是使用一个优化的临时缓存目录optimizeDir(dex),专门把Dex文件解压进去,这样以后就从这个临时缓存目录中加载,提高效率。
在1代码中我们提到了loadFixedDex()方法,便是我们的核心热修复工具类,我给大家具体讲一下:
ClassLoader有一个简单的实现类-PathClassLoader。该类作为Android的默认的类加载器,本身继承自BaseDexClassLoader,BaseDexClassLoader重写了findClass方法,该方法是ClassLoader的核心。
每个ClassLoader有一个pathList变量,是标识dex文件的路径,我们通过该路径加载dex文件,默认不分包的时候只有一个dex文件,当然谷歌在顶层设计时允许我们有多个dex文件。
ClassLoader去找optimizeDir(dex)目录,然后把目录添加到pathList里面去,接着去找目录下面的所有的dex文件,把这些dex文件当做一个数组放到dexElements中去,这样就可以有多个dex文件。
pathList{
dexElements{
[classes.dex,classes2.dex]
}
}
ClassLoader每加载一个类,它会先找classes.dex,如果找不到就去classes2.dex中找,如果里面又一个dex有问题,比如说classes2.dex出问题了,我们就需要弄一个修复的新的classes2.dex文件放到数组中去,替换掉有问题的;但是classes2.dex中可能有多个类,除了有问题的类,也可能有很多正确的类,我们在替换时没必要把所有的类都替换掉,所以我们只要替换有问题的类。
为此,我们可以采用一个策略,把新的替换的dex文件放到数组的最前面,最终数组的形态为:
[classes2.dex,classes.dex,classes2.dex]
这里解释下,ClassLoader类加载器先加载我们修复的正确的dex文件,然后顺序加载数组中其他的dex元素,到了最后加载到旧的classes2.dex元素,由于前面已经加载了更新的classes2.dex(更新的dex文件中只包含修复的class),那么旧的classes2.dex元素中的有Bug的class就不会再加载,而是只加载其余的没有错误的class。
整个流程其实非常简单,但是如果我们要实现这个过程却有个障碍,那就是由于我们的APK程序可能正在运行,谷歌并没有提供相关的接口方法去实现这一步骤,为此,我们需要使用反射的手段去实现。
1.首先需要反射ClassLoader类,找到里面的pathList变量,然后找到dexElements[]数组,该数组在修复之前只有两个元素,分别是classes.dex和classes2.dex(出错的),假设值数组1;
2.接着我们要往dexElements[]数组中添加classes2.dex文件。
Android中要想实现加载dex文件,需要使用DexClassLoader类加载classes2.dex(补丁),加载到dexElements[]数组中去,假设值数组2。
3.最后,我们需要把两个dexElements[]数组合并,作为一个新数组dexElements[],该数组中包含元素为classes2.dex(补丁),classes.dex和classes2.dex(出错的),完成后将数组返回赋值给系统的ClassLoader。
最后贴出热修复工具类源码
public class FixDexUtils {
private static HashSet<File> loadedDex = new HashSet<File>();
public static void loadFixedDex(Context context) {
if (context == null) {
return;
}
// 首先拿到缓存目录
File fileSDir = context.getDir(MyConstants.DEX_DIR,
Context.MODE_PRIVATE);
File[] listFils = fileSDir.listFiles();
// 遍历缓存文件
for (File file : listFils) {
// 如果文件是以"classes"开始或者以".dex"结尾,说明这是从SDK中拷贝回来的修复包
if (file.getName().startsWith("classes")
|| file.getName().endsWith(".dex")) {
Log.i("WY", "当前dexName:" + file.getName());
loadedDex.add(file);
}
}
doDexInject(context, fileSDir);
}
private static void doDexInject(Context context, File fileDir) {
if (Build.VERSION.SDK_INT >= 23) {
Log.i("WY", "Unable to do dex inject on SDK"
+ Build.VERSION.SDK_INT);
}
// .dex 的加载需要一个临时目录
String optimizeDir = fileDir.getAbsolutePath() + File.separator
+ "opt_dex";
File fopt = new File(optimizeDir);
if (!fopt.exists())
fopt.mkdirs();
try {
// 根据.dex 文件创建对应的DexClassLoader 类
for (File file : loadedDex) {// 循环迭代,用于多个修复包同时注入
DexClassLoader classLoader = new DexClassLoader(
file.getAbsolutePath(), fopt.getAbsolutePath(), null,
context.getClassLoader());
// 注入
inject(classLoader, context);
}
} catch (Exception e) {
e.printStackTrace();
}
}
private static void inject(DexClassLoader classLoader, Context context) {
// 获取到系统的DexClassLoader 类
PathClassLoader pathLoader = (PathClassLoader) context.getClassLoader();
try {
Object dexElements = combineArray(
getDexElements(getPathList(classLoader)),
getDexElements(getPathList(pathLoader)));
Object pathList = getPathList(pathLoader);
setField(pathList, pathList.getClass(), "dexElements", dexElements);
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 通过反射获取DexPathList中dexElements
*/
private static Object getDexElements(Object paramObject)
throws IllegalArgumentException, NoSuchFieldException,
IllegalAccessException {
return getField(paramObject, paramObject.getClass(), "dexElements");
}
/**
* 通过反射获取BaseDexClassLoader中的PathList对象
*/
private static Object getPathList(Object baseDexClassLoader)
throws IllegalArgumentException, NoSuchFieldException,
IllegalAccessException, ClassNotFoundException {
return getField(baseDexClassLoader,
Class.forName("dalvik.system.BaseDexClassLoader"), "pathList");
}
/**
* 通过反射获取指定字段的值
*/
private static Object getField(Object obj, Class<?> cl, String field)
throws NoSuchFieldException, IllegalArgumentException,
IllegalAccessException {
Field localField = cl.getDeclaredField(field);
localField.setAccessible(true);
return localField.get(obj);
}
/**
* 通过反射设置字段值
*/
private static void setField(Object obj, Class<?> cl, String field,
Object value) throws NoSuchFieldException,
IllegalArgumentException, IllegalAccessException {
Field localField = cl.getDeclaredField(field);
localField.setAccessible(true);
localField.set(obj, value);
}
/**
* 合并两个数组
*/
private static Object combineArray(Object arrayLhs, Object arrayRhs) {
Class<?> localClass = arrayLhs.getClass().getComponentType();
int i = Array.getLength(arrayLhs);
int j = i + Array.getLength(arrayRhs);
Object result = Array.newInstance(localClass, j);
for (int k = 0; k < j; ++k) {
if (k < i) {
Array.set(result, k, Array.get(arrayLhs, k));
} else {
Array.set(result, k, Array.get(arrayRhs, k - i));
}
}
return result;
}
}