Android 热修复技术

今天动手实践了一下安卓上的热修复机制,现在总结一下、

在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的问题,后面自己实现了再来更新!