ClassLoader和热修复

Android源码来自28.0.2

ClassLoader

参考Android工程师进阶 34讲
1.每个ClassLoader加载的Class路径不同,
2.ClassLoader加载class主要是通过loadClass方法

ClassLoader

    protected Class loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
            // First, check if the class has already been loaded
            //首先,先判断自己是否曾经加载过这个类,
            //如果曾经加载过,直接返回之前加载的Class
            Class c = findLoadedClass(name);
            if (c == null) {
                //如果没有加载过,就开始加载
                try {
                    if (parent != null) {
                        //如果有parent(ClassLoader)
                        //把这个class交给parent去加载
                        c = parent.loadClass(name, false);
                    } else {
                        //如果parent为空,说明当前classloader是bootstrap class loader
                        //执行findBootstrapClassOrNull方法,
                        //不过ClassLoader#findBootstrapClassOrNull方法默认返回null
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                    // ClassNotFoundException thrown if class not found
                    // from the non-null parent class loader
                }

                if (c == null) {
                    // If still not found, then invoke findClass in order
                    // to find the class.
                    //  如果parent为空或者parent加载不了class
                    //那就自己加载
                    c = findClass(name);
                }
            }
            return c;
    }

PathClassLoader

image.png

从log可以看出,加载MainActivity的是PathClassLoader。
而PathClassLoader继承自BaseDexClassLoader,BaseDexClassLoader继承自ClassLoader
, PathClassLoader和BaseDexClassLoader都没有重写loadClass方法。PathClassLoader仅仅是重写了两个构造方法。
所以PathClassLoader执行loadClass的逻辑是:
1.PathClassLoader自己是否曾经加载过目标class,如果加载过,就直接返回。如果没加载过,执行步骤2
2.执行BootClassLoader#loadClass. PathClassLoader的parent是BootClassLoader,不为空,所以交给执行BootClassLoader#loadClass(步骤3)
3.在BootClassLoader#loadClass里,

    @Override
    protected Class loadClass(String className, boolean resolve)
           throws ClassNotFoundException {
        //同ClassLoader,也是先看自己是否曾经亲自加载过
        Class clazz = findLoadedClass(className);
    
        if (clazz == null) {
            //如果没有,就执行Class.classForName去加载
            //而Class.classForName是native方法
            clazz = findClass(className);
        }

        return clazz;
    }
    @Override
    protected Class findClass(String name) throws ClassNotFoundException {
        return Class.classForName(name, false, null);
    }

BootClassLoader#loadClass最后是使用java.lang.Class#classForName加载该class。平时我们调用Class.forName方法时,最终也是走向了这个native方法。
到这里,ClassLoader的双亲委托就清楚了,ClassLoader双亲委托逻辑是:先交给parent(注意,这个parent并不是继承的那个父类,而是设置进来的另一个ClassLoader,单链表),如果parent不能加载class,那自己再加载,如果自己也不能加载,就返回null。可以理解是单链表组成的一串ClassLoader,每个ClassLoader里都有一个parent来指向上一个ClassLoader,如果一个ClassLoader的parent是null,那么,这个就是链表头,每次ClassLoader加载class,它就先找parent,让parent去加载,parent加载不了(返回null),自己才加载,如果自己也加载不了,就返回null。

如果这里返回null,则会执行PathClassLoader的findClass方法,自己来加载class. PathClassLoader并没有重写findClass方法, 但是BaseDexClassLoader重写了该方法,所以,PathClassLoader会执行BaseDexClassLoader#findClass方法

BaseDexClassLoader#findClass

    private final DexPathList pathList;
    public BaseDexClassLoader(String dexPath, File optimizedDirectory,
            String librarySearchPath, ClassLoader parent) {
        super(parent);
        //创建PathClassLoader的时候就会初始化pathList 
        this.pathList = new DexPathList(this, dexPath, librarySearchPath, null);

        if (reporter != null) {
            reporter.report(this.pathList.getDexPaths());
        }
    }
    @Override
    protected Class findClass(String name) throws ClassNotFoundException {
        List suppressedExceptions = new ArrayList();
        //查找class的逻辑实际上交给了pathList
        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;
    }

从上面代码可以看出,查找加载Class的逻辑实际上是由pathList完成的。

DexPathList

    private Element[] dexElements;
    DexPathList(ClassLoader definingContext, String dexPath,
            String librarySearchPath, File optimizedDirectory, boolean isTrusted) {
        //---------------------省略
        this.dexElements = makeDexElements(splitDexPath(dexPath), optimizedDirectory,
                                           suppressedExceptions, definingContext, isTrusted);
        //---------------------省略
    }
    public Class findClass(String name, List suppressed) {
        for (Element element : dexElements) {
            Class clazz = element.findClass(name, definingContext, suppressed);
            if (clazz != null) {
                return clazz;
            }
        }

        if (dexElementsSuppressedExceptions != null) {
            suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions));
        }
        return null;
    }

可以看到DexPathList#findClass的逻辑就是遍历数组dexElements,而数组dexElements是在构造方法中通过makeDexElements方法生成的。
而这个dexElements的内容,可以打印BaseDexClassLoader#pathList内容如下:


image.png

pathList的dexElements里只有一个元素,zip file "/data/app/com.houtrry.hotfixsample-2.apk",也就是当前程序的apk文件。

热修复

市场上的热修复可以分为java层实现和native层实现。
native层:andfix sophix(不需要重启app)
Java层:Tinker(需要重启app)
本文讨论的是Java层实现。

Java层实现主要有三种方案
1.在DexPathList的dexElements中插入dex
2.自定义ClassLoader加载dex
3.利用ClassLoader的双亲委托机制,把PathClassLoader的parent替换成自己的ClassLoader, 在这个ClassLoader中加载dex

一般来说,方案1和3比较常见

方案 在DexPathList的dexElements中插入dex

原理

DexPathList#findClass通过遍历数组dexElements查找class,那么,是否可以把修复好的class文件放到dexElements中,并且放到dexElements中的apk前面呢?这样,每次加载目标class,都会先遍历到处于前面的已经修复了问题的dex,而有问题的dex在apk中,就没有加载的机会,从而实现热修复。

  1. 而怎么把修复好的dex加到dexElements中呢?通过反射就可以实现。
  2. 那什么时候执行这个逻辑呢?当然是越早越好,因为加载晚了,可能会出现问题class已被加载的情况,这种情况下,即使dexElements中修复好的dex位于前面也没有机会执行了,只能重启app后才能生效。app中最早的应该就是Application#attachBaseContext方法了,因此,我们在这个方法里执行dexElements的插入逻辑。
  3. dex的来源应该是我们下载到本地的,下载完成后,app重启进入Application#attachBaseContext执行dexElements的插入逻辑即可生效。
  4. dex的生成方法可以查看d8使用说明
    拿Utils.java举例,生成dex主要有2步:
    ①javac Utils.java 生成Utils.class文件
    ②./d8 Utils.class 生成classes.dex文件,这个就是想要的dex文件了。
    注意:d8文件在\sdk\build-tools下,比如\sdk\build-tools\28.0.2\d8.bat;注意步骤②时d8的路径。

实现

  1. 在Application#attachBaseContext中,通过反射,在dexElements中插入dex
    /**
     * 在ClassLoader中的dexElements数组中(数组0号位)插入我们自己的dex
     *
     * @param application
     */
    public static void preformHotFix(@NonNull Application application) {
        if (!hasDex(application)) {
            return;
        }
        try {
            //第一步:获取当前ClassLoader中的dexElements(dexElementsOld)
            ClassLoader classLoader = application.getClassLoader();
            Class clsBaseDexClassLoader = Class.forName("dalvik.system.BaseDexClassLoader");
            Field pathListField = clsBaseDexClassLoader.getDeclaredField("pathList");
            pathListField.setAccessible(true);
            Object pathList = pathListField.get(classLoader);
            Class clsDexPathList = Class.forName("dalvik.system.DexPathList");
            Field dexElementsField = clsDexPathList.getDeclaredField("dexElements");
            dexElementsField.setAccessible(true);
            Object[] dexElementsOld = (Object[]) dexElementsField.get(pathList);
            System.out.println("dexElementsOld: " + dexElementsOld);

            int sizeOfOldDexElement = dexElementsOld.length;
            List dexFileList = getDexFileList(getDexDir(application));

            Method[] declaredMethods = clsDexPathList.getDeclaredMethods();
            for (Method method :
                    declaredMethods) {
                System.out.println("method: " + method);
            }

            //第二步:生成包含hot fix dex文件的dexElements(dexElementsNew)
            //当然,也可以用另外一种方式: new 一个PathClassLoader加载dex文件夹下的dex,
            //                          然后反射获取到这个PathClassLoader中dexElements的值,
            //                          也就是我们这里需要的,反射逻辑可以参考第一步
            //PathClassLoader pathClassLoader = new PathClassLoader(getDexPath(getDexDir(application)), classLoaderParent);
            //注意:makeDexElements在不同版本中可能会有变化,注意log提示,做好兼容, 这里只是测试.
            //      具体的兼容逻辑可以参考腾讯tinker的com.tencent.tinker.loader.SystemClassLoaderAdder#installDexes
            Method makeDexElementsMethod = clsDexPathList.getDeclaredMethod("makeDexElements",
                    List.class, File.class, List.class, ClassLoader.class);
            makeDexElementsMethod.setAccessible(true);
            Object[] dexElementsNew = (Object[]) makeDexElementsMethod.invoke(null, dexFileList, null,
                    new ArrayList(), classLoader);
            int sizeOfNewDexElement = dexElementsNew.length;
            System.out.println("sizeOfNewDexElement: " + sizeOfNewDexElement + ", sizeOfOldDexElement: " + sizeOfOldDexElement);
            if (sizeOfNewDexElement == 0) {
                return;
            }
            //第三步:合并两个dexElements
            //注意:dexElementsNew中的元素需要放到dexElementsOld元素的前面
            //数组拷贝逻辑可以参考DexPathList#addDexPath方法
//            Object[] dexElements = new Object[sizeOfNewDexElement + sizeOfOldDexElement];
            //注意:这里不要像直接像上面那样直接new Object[]数组,而是使用Array.newInstance方法(参考自tinker的com.tencent.tinker.loader.shareutil.ShareReflectUtil#expandFieldArray)
            //直接new Object[]数组的话,在执行下面dexElementsField.set的时候会报错java.lang.RuntimeException: Unable to instantiate application com.houtrry.hotfixsample.HotFixApplication: java.lang.IllegalArgumentException: field dalvik.system.DexPathList.dexElements has type dalvik.system.DexPathList$Element[], got java.lang.Object[]
            Object[] dexElements = (Object[]) Array.newInstance(dexElementsOld.getClass().getComponentType(), sizeOfNewDexElement + sizeOfOldDexElement);

            System.arraycopy(dexElementsNew, 0, dexElements, 0, sizeOfNewDexElement);
            System.arraycopy(dexElementsOld, 0, dexElements, sizeOfNewDexElement, sizeOfOldDexElement);
            System.out.println("dexElements: " + dexElements);
            //第四步:替换dexElements
            dexElementsField.setAccessible(true);
            dexElementsField.set(pathList, dexElements);
            System.out.println("pathList: " + pathList);
        } catch (ClassNotFoundException | NoSuchFieldException | IllegalAccessException | NoSuchMethodException | InvocationTargetException e) {
            e.printStackTrace();
        }
    }

日志中可以看到,DexPathList中有hotFixCopy.dex和base.apk,且hotFixCopy.dex在base.apk前面,也即是期望效果。

注意:

  1. 不同Android版本中makeDexElements可能会稍有不同(主要是参数不同),因此,需要考虑兼容
  2. 获取dexElements可以不通过反射makeDexElements的方式,通过new PathClassLoader(dexPath, null),把生成dexElements的逻辑交给PathClassLoader,然后反射获取PathClassLoader中的dexElements即可获取
  3. 创建合并后DexElement数组容器时,如果使用new Object[]的方式
Object[] dexElements = new Object[sizeOfNewDexElement + sizeOfOldDexElement];

会在执行

dexElementsField.set(pathList, dexElements);

替换dexElementsField值的时候报错,异常信息如下

2020-05-03 19:51:02.099 3507-3507/com.houtrry.hotfixsample E/AndroidRuntime: FATAL EXCEPTION: main
    Process: com.houtrry.hotfixsample, PID: 3507
    java.lang.RuntimeException: Unable to instantiate application com.houtrry.hotfixsample.HotFixApplication: java.lang.IllegalArgumentException: field dalvik.system.DexPathList.dexElements has type dalvik.system.DexPathList$Element[], got java.lang.Object[]
        at android.app.LoadedApk.makeApplication(LoadedApk.java:802)
        at android.app.ActivityThread.handleBindApplication(ActivityThread.java:5377)
        at android.app.ActivityThread.-wrap2(ActivityThread.java)
        at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1545)
        at android.os.Handler.dispatchMessage(Handler.java:102)
        at android.os.Looper.loop(Looper.java:154)
        at android.app.ActivityThread.main(ActivityThread.java:6119)
        at java.lang.reflect.Method.invoke(Native Method)
        at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:886)
        at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:776)
     Caused by: java.lang.IllegalArgumentException: field dalvik.system.DexPathList.dexElements has type dalvik.system.DexPathList$Element[], got java.lang.Object[]
        at java.lang.reflect.Field.set(Native Method)
        at com.houtrry.hotfixsample.HotFixManager.preformHotFix(HotFixManager.java:97)
        at com.houtrry.hotfixsample.HotFixApplication.attachBaseContext(HotFixApplication.java:17)
        at android.app.Application.attach(Application.java:189)
        at android.app.Instrumentation.newApplication(Instrumentation.java:1008)
        at android.app.Instrumentation.newApplication(Instrumentation.java:992)
        at android.app.LoadedApk.makeApplication(LoadedApk.java:796)
        at android.app.ActivityThread.handleBindApplication(ActivityThread.java:5377) 
        at android.app.ActivityThread.-wrap2(ActivityThread.java) 
        at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1545) 
        at android.os.Handler.dispatchMessage(Handler.java:102) 
        at android.os.Looper.loop(Looper.java:154) 
        at android.app.ActivityThread.main(ActivityThread.java:6119) 
        at java.lang.reflect.Method.invoke(Native Method) 
        at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:886) 
        at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:776) 

方案 把PathClassLoader的parent替换成自己的ClassLoader

原理

利用双亲委托机制,给当前parent换成可以加载自己dex的ClassLoader。
原本的ClassLoader路径:PathClassLoader==>BootClassLoader
替换后的ClassLoader路径:PathClassLoader==>自定义ClassLoader==>BootClassLoader

实现

    /**
     * 给ClassLoader安排一个新的parent
     * 在这个新parent中加载我们自己的dex
     * 简单理解就是在单链表中间插入第三个元素
     *
     * @param application
     */
    public static void preformHotFix2(@NonNull Application application) {
        if (!hasDex(application)) {
            return;
        }
        try {
            ClassLoader classLoader = application.getClassLoader();
            //第一步:反射获取到当前ClassLoader的parent
            Class clsBaseDexClassLoader = Class.forName("java.lang.ClassLoader");
            Field parent = clsBaseDexClassLoader.getDeclaredField("parent");
            parent.setAccessible(true);
            //第二步:创建新的PathClassLoader
            // 这个PathClassLoader的parent是当前CLassLoader的parent
            //path指向我们dex文件夹下的dex文件
            ClassLoader classLoaderParent = classLoader.getParent();
            PathClassLoader pathClassLoader = new PathClassLoader(getDexPath(getDexDir(application)), classLoaderParent);
            //第三步:把classLoaderParent作为当前classLoader的parent
            //这样,根据双亲委托机制,当前ClassLoader加载class的时候,
            // 会将class交给它的parent(也就是我们创建的pathClassLoader来加载)
            //如果我们的pathClassLoader可以加载这个class(意味着该class能在dex中找到,也就是我们需要修复的class)
            //这样系统的ClassLoader就没有机会加载有问题的class,问题得到修复
            parent.set(classLoader, pathClassLoader);
        } catch (ClassNotFoundException | NoSuchFieldException | IllegalAccessException e) {
            e.printStackTrace();
        }
    }

这里,我们创建PathClassLoader作为自定义parent。
PathClassLoader的第一个参数:字符串,dex路径。文件可以是dex/apk/zip。多个文件字符串之间用“:”分号隔开
PathClassLoader的第二个参数:ClassLoader,也就是指定ClassLoader的parent。这里我们用默认ClassLoader的parent作为自定义ClassLoader的parent。

执行后日志如下


可以看到,当前ClassLoader还是原来的PathClassLoader,加载的dex是base.apk。
parent是我们自定义的ClassLoader,其dex正是我们期望的hotFixCopy.dex。
parent的parent是BootClassLoader,也正是没改之前的PathClassLoader的parent。
demo地址HotFixSample

tinker源码分析

//TODO 待完善

你可能感兴趣的:(ClassLoader和热修复)