接上一篇Android中的ClassLoader剖析,完成一个最简单的热修正的例子,意在解说原理。
上一篇咱们明白了Android加载类的原理,这一篇咱们来进行实践。

咱们回顾一下Android ClassLoader的原理并理一下思路:

  1. Android使用了PathClassLoader进行类的加载,实践的加载类是它的父类BaseDexClassLoaderDexPathList来进行加载的,load进来的dex会放在它的成员变量ArrayList[DexFile] elements里边,需求加载的时分按次序依次查找并加载。
  2. 系统给咱们提供了一个叫做DexClassLoader的类来加载非系统的Dex,咱们能够用这个类来加载咱们自己的dex文件,只需求给它指定一个能够拜访到的dex文件途径。

So,咱们要做的便是把咱们需求替换的class打成dex或许包括classes.dex的jar或许zip(Android终究拜访的依旧是classes.dex文件), 并且把DexClassLoader的DexPathList的elements取出来放到PathClassLoader的DexPathList的elements的最前面。

完成一个根本的Android Demo

//MainActivity.java
@Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        TextView tv = findViewById(R.id.sample_text);
        tv.setText(Util.getShow());
    }
//Util.java
public class Util {
    public static String getShow() {
        return "Mr Wrong!";
    }
}

代码很简单,Util.getShow()回来一个字符串,并显现在UI上,所以它应该是这么显现的。

Android最简单的热修复原理解析

把需求修正的class打成dex

咱们就把Util.getShow()办法修正一个回来值

public class Util {
    public static String getShow() {
        return "Mr Right";
    }
}

然后咱们需求把这个类打到dex里边。
咱们能够run一下之后去这个目录下面找咱们的Util.class

testHotFix/app/build/intermediates/javac/debug/compileDebugJavaWithJavac/classes/com/hook/yocn/testhotfix/Util.class

假如找不到咱们也能够自己用javac指令生成一个

javac Util.java //会在本目录生成一个Util.class文件

然后咱们需求把这个Util.class生成dex

dx --dex --output Util.dex Util.class

dx指令在Android SDK下,我本地目录为/Users/yocn/Library/Android/sdk/build-tools/28.0.3,方便起见,能够把这个目录配置到环境变量里边。

Android最简单的热修复原理解析

现在咱们的目录结构是这样的,假如在testhotfix目录下执行上面生成dex的办法,我这儿是会报错:
MacBook:testhotfix yocn$ dx --dex --output Util.dex Util.class
PARSE ERROR:
class name (com/hook/yocn/testhotfix/Util) does not match path (Util.class)
...while parsing Util.class
1 error; aborting

所以我来到java目录下执行

dx --dex --output Util.dex com/hook/yocn/testhotfix/Util.class

发现main/java目录下生成了一个Util.dex文件。

Android最简单的热修复原理解析

咱们把这个dex文件放到SD卡根目录下。
adb push Util.dex /mnt/sdcard/

然后编写代码:


public class MainActivity extends Activity {
    private static String TAG = "yocnAA";
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        TextView tv = findViewById(R.id.sample_text);
        try {
            init();
        } catch (IllegalAccessException | NoSuchFieldException | ClassNotFoundException e) {
            e.printStackTrace();
        }
        tv.setText(Util.getShow());
    }
    private void init() throws IllegalAccessException, NoSuchFieldException, ClassNotFoundException {
        ClassLoader pathClassLoader = getClassLoader();
        //SD卡根目录
        String dexPath = Environment.getExternalStorageDirectory().getAbsolutePath();
        //寄存加载进来的dex的目录
        String optimizedDirectory = getFilesDir().getAbsolutePath() + File.separator + "odex";
        //dexPath:dex或许包括dex的jar/apk文件途径,多个需求用File.pathSeparator分离隔
        //optimizedDirectory:odex的被寄存的途径,能够为null,这儿就能看出来区别,PathClassLoader设置的是null,DexClassLoader设置的是非null。
        //libraryPath:本地库的文件途径,多个需求用File.pathSeparator分离隔
        //parent:父classloader
        DexClassLoader dexClassLoader = new DexClassLoader(dexPath + "/Util.dex", optimizedDirectory, null, pathClassLoader);
        //得到DexClassLoader的DexPathList,加载dex到Element数组里边  /data/user/0/com.yocn.testhotfix/files
        Object dexDexPathList = getPathList(dexClassLoader);
        Object newElementArray = getDexElements(dexDexPathList);
        //获取PathClassLoader加载的dex
        Object pathDexPathList = getPathList(pathClassLoader);
        Object oldElementArray = getDexElements(pathDexPathList);
        //把DexClassLoader的element[]放到PathClassLoader的element[]里边并且是头部,加载的时分优先加载
        Object resultElementArray = combineArray(newElementArray, oldElementArray);
        //从头设置进去
        setField(pathDexPathList, pathDexPathList.getClass(), "dexElements", resultElementArray);
    }
    /**
     * 反射给目标中的属性从头赋值
     */
    private static void setField(Object obj, Class<?> cl, String field, Object value) throws NoSuchFieldException, IllegalAccessException {
        Field declaredField = cl.getDeclaredField(field);
        declaredField.setAccessible(true);
        declaredField.set(obj, value);
    }
    /**
     * 反射得到目标中的属性值
     */
    private static Object getField(Object obj, Class<?> cl, String field) throws NoSuchFieldException, IllegalAccessException {
        Field localField = cl.getDeclaredField(field);
        localField.setAccessible(true);
        return localField.get(obj);
    }
    /**
     * 反射得到类加载器中的pathList目标
     */
    private static Object getPathList(Object baseDexClassLoader) throws ClassNotFoundException, NoSuchFieldException, IllegalAccessException {
        return getField(baseDexClassLoader, Class.forName("dalvik.system.BaseDexClassLoader"), "pathList");
    }
    /**
     * 反射得到pathList中的dexElements
     */
    private static Object getDexElements(Object pathList) throws NoSuchFieldException, IllegalAccessException {
        return getField(pathList, pathList.getClass(), "dexElements");
    }
    /**
     * 数组合并
     */
    private static Object combineArray(Object arrayLhs, Object arrayRhs) {
        Class<?> componentType = arrayLhs.getClass().getComponentType();
        // 得到左数组长度(补丁数组)
        int i = Array.getLength(arrayLhs);
        // 得到原dex数组长度
        int j = Array.getLength(arrayRhs);
        // 得到总数组长度(补丁数组+原dex数组)
        int k = i + j;
        // 创立一个类型为componentType,长度为k的新数组
        Object result = Array.newInstance(componentType, k);
        System.arraycopy(arrayLhs, 0, result, 0, i);
        System.arraycopy(arrayRhs, 0, result, i, j);
        return result;
    }
}

然后咱们把App跑起来,假如看到下面的图阐明成功了。

Android最简单的热修复原理解析

咱们还能够把SD卡目录下的Util.dex文件改个姓名或许删掉,再从头运行app,看是不是又变回原来的Mr Wrong!了。

参阅: 热修正——浅显易懂原理与完成