android ClassLoader实现热修复

作者: 求闲居士 | 来源:发表于2019-12-17 15:30 被阅读0次

    1. 前言

    在用公司的框架进行开发时,最大的特点是模块纯java开发,打成dex包进行模块更新,而不用更新app。


    image

    这个算是热修复框架里的java multidex方式实现的,接下来看看ClassLoader如何实现的热修复。

    2. 原理

    首先来看看实现功能需要哪些原理。

    2.1 Dalvik,JIT与ART

    Java是编译-解释语言,即程序员编译之后不可以直接编译为机器码,而是会编译成字节码(在Java程序中为.class文件,在Android程序中为.dex文件)。然后我们需要将字节码再解释成机器码,使之能被CPU解读。这第二次解释,即从字节码解释成机器码的过程,是程序安装或运行后,在Java虚拟机中实现的。

    2.1.1 Dalvik

    android在最开始时,内置了一个Dalvik虚拟机,其实也就是Google自己编写的一个Java虚拟器,然后使用边解释边执行的方式来运行Java代码,这种模式==运行效率极其低下==,因此很快Google就引入了JIT模式来改善这种情况。

    2.1.2 JIT

    JIT(Just In Time)是即时编译的意思,当用户在使用App时,会将经常使用的功能编译成机器码,这样当再次使用这个功能时就可以直接运行机器码,而不用每次都一行行翻译了。

    虽然JIT挺聪明的,且总体思路清晰理想丰满,但现实是仍然卡的要死。==打开APP的时候会变慢==;==每次打开APP都要重复劳动,不能一劳永逸==;==如果用户打开了JIT没有编译的代码,就只能等DVM中的解释器去边执行边解释了==。

    2.1.3 ART

    然而JIT的机制仍然不够完美,在Android 5.0系统的时候Google进行了一次大变更,废弃了Dalvik虚拟机,引入了全新开发的ART虚拟机,并使用 AOT(Ahead Of Time) 的方式来提升运行效率。AOT就是在应用安装的时候预先将代码都编译成机器码,这样在应用运行的时候就不用再做解释工作了,直接就可以运行。

    然而最终用户实际的反馈却并不怎么好,==AOT机制使得安装应用变得太慢了,而且预先编译机器码还会占用额外的手机空间==。

    2.1.4 混合编译

    于是在Android 7.0系统中,Google又进行了调整,这次重新引入了JIT模式。应用安装的时候不会进行编译操作,以保证安装速度。App运行时,dex文件先通过解析器被直接执行并记录在profile文件,通过profile来判断是否为热点函数,热点函数会被识别并被JIT编译后存储在 jit code cache 中。手机进入 IDLE(空闲) 或者 Charging(充电) 状态的时候,系统会扫描 App 目录下的 profile 文件并执行 AOT 过程进行编译。如果执行到还没来得及编译的代码,那么就使用JIT+解释执行的方式来顶住。

    image

    2.1.5 Android 8.0 改进解释器

    在Android8.0时期,谷歌又盯上了解释器,其实纵观上面的问题,根源就是这个解释器解释的太慢了!(什么JIT,AOT,老夫解释只有一个字,快)那我们何不让这个解释器解释的快一点呢?于是谷歌改进了解释器,解释模式执行效率大大提升

    2.1.6 Android 9.0 改进编译模板

    在Android9.0上提供了预先放置热点代码的方式,应用在安装的时候就能知道常用代码会被提前编译

    谷歌允许你在开发阶段添加一个配置文件,这个配置文件内可指定“热点代码”,当应用安装完后,ART在后台悄悄编译APP时,会优先编译配置文件中指定的“热点代码”。

    2.1.7 文件格式odex与oat

    • dex:可以简单的理解为:Dex 文件是很多 .class 文件处理后的产物,最终可以在 Android 运行时环境执行。是字节码
    • odexdalvik虚拟机中,安装时从apk文件中提取出classes.dex文件,并通过dexopt生成一个可运行的文件单独存放在data/dalvik-cache目录下,在DexClassLoader动态加载Dex文件时,也会进行Dex的优化。odex 文件也属于dex文件
    • oatart虚拟机中,APK中的.dex文件(字节码)会被dex2oat解释为ELF格式的.oat文件(机器码)同样保存在手机的data/dalvik-cache目录下。

    APK运行时,上述生成的oat文件会被加载到内存中,并且ART虚拟机可以通过里面的oatdata和oatexec段找到任意一个类的方法对应的本地机器指令来执行。 oat文件中的oatdata包含用来生成本地机器指令的dex文件,内容oat文件中的oatexec包含有生成的本地机器指令。

    2.1.8 编译模式jit与aot

    在android5.0以下和7.0以上,jit和art的表现都不一样。

    2.1.8.1 android5.0以下的jit

    在运行时对dex的指令进行intercept,解释成机器码;虚拟机根据函数调用的次数,来决定热点代码;以函数为维度将热点代码的机器码进行缓存,而jit就是将热点代码优化编译缓存,存在下一次调用时直接调用该机器码。

    • 优点:安装速度超快;存储空间小
    • 缺点:Multidex加载的时候会非常慢,因为在dex加载时会进行dexopt;JIT中需要解释器,解释器解释的字节码会带来CPU和时间的消耗;由于热点代码的Monitor一直在运行,也会带来电量的损耗;每次启动应用都需要重新编译(没有缓存)

    2.1.8.2 5.0-7.0的aot

    art虚拟机运行的文件格式也从odex转换成了oat格式。oat文件包含oatdata和oatexec,前者包含dex文件内容,后者包含生成的本地机器指令。

    在APK安装的时候,PackageManagerService会调用dex2oat通过aot静态编译的方式,来将所有的dex文件(包括Multidex)编译oat文件;DexClassLoader动态加载也会生成oat文件。

    • 优点:运行时会超级快;在运行时省电,也节省各种资源
    • 缺点:应用安装和系统升级之后的应用优化比较耗时(重新编译,把程序代码转换成机器语言),所有app都需要进行dex2oat的操作;由于oat文件中包含dex文件与编译后的Native Code,导致占用空间也越来越大

    2.1.8.3 7.0至今的aot与jit

    7.0之后的采用了Hybrid Mode的ART虚拟机:aot,jit,解释器三种混合编译,来从运行时的性能、存储、安装、加载时间进行平衡。

    安装时以Intercept的方式来运行App;在系统空闲的时候会在后台对App进行AOT静态编译,并且会根据解释器运行时所收集的运行时函数调用的信息生成的Profile文件来进行参考,Profile文件中的热点函数会被jit编译器编译并缓存下次使用。

    [图片上传失败...(image-be1557-1576665837767)]
    由上图可知,解释器解释函数会记录在Profile文件中,如果Profile文件判断方法是热点代码,会jit编译器进行编译并缓存,下次调用则直接使用缓存机械码

    在BackgroundDexOptService中,会根据所生成的Profile以及Dex文件在后台进行AOT,根据运行时的Profile文件会选择性的将常用的函数编译成NativeCode

    9.0谷歌推出了热点代码配置功能,当应用安装完后,ART在后台悄悄编译APP时,会优先编译配置文件中指定的“热点代码”。

    2.1.8.4 存放目录

    大家都知道 apk其实就是zip包 apk安装过程其实就是解压过程

    用户应用安装涉及以下几个目录

    data/app 安装目录 安装时会把apk文件copy到这里

    data/dalvik-cache 如上述描述中的存放.dex(.odex 无论davilk的dex 还是art的oat格式)

    data/data/pkg/ 存放应用程序的数据

    2.1.9 方舟编译器

    翻译器和编译器是不同的;编译器是把源程序的每一条语句都编译成机器语言并保存成二进制文件,这样运行时计算机可以直接以机器语言来运行此程序,因而速度很快;而解释器则是只在执行程序时,才一条一条地解释成机器语言来让计算机执行,因此运行速度不如编译后的程序运行得快。

    例如上面的混合编译,jit是即时编译,AOT静态编译,jit配合解释器使用。无论是编译器还是解释器,只是在虚拟机上打补丁,手机上的虚拟机+编译器+解释器本身不仅占用硬件资源,还无法最大发挥软件运行性能。正因如此,所以绝大部分手机厂商只能无奈的通过简单粗暴提升Android手机的内存和存储空间,来弥补虚拟机的弊端。

    方舟编译器与其说是一个编译器,不如说是一个编译运行系统;这个系统的运行需要开发环境和终端(也就是智能手机)的配合,其目的是绕过Android操作系统中App的运行所必须依赖的虚拟机,将Java/C/C++等混合代码一次编译成机器码直接在手机上运行,彻底告别Java的JNI额外开销,也彻底告别了虚拟机的GC内存回收带来的应用进程掉线——从而最终实现Android操作系统的流畅度。

    它将编译过程放在了开发者那里,直接运行机械码也比Java/C/C++等混合代码快,还自制了更好的垃圾回收。

    2.2 ClassLoader简介

    在java中,它是类加载器,顾名思义,根据一个指定的类的全限定名,找到对应的Class字节码文件,然后加载它转化成一个java.lang.Class类的一个实例,将class类载入到JVM中。

    而android中,也同样需要有ClassLoader机制将class类加载到Android 的 Dalvik(5.0之前版本)/ART(5.0增加的)中,是将class打包成一个或者多个 dex文件,再由BaseDexClassLoader来进行处理。

    image

    在Android中,ClassLoader是一个抽象类,实际开发过程中,我们一般是使用其具体的子类DexClassLoaderPathClassLoader这些类加载器来加载类的,BootClassLoader加载系统类,它们的不同之处是:

    • BootClassLoader:主要用于加载系统的类,包括java和android系统的类库,和JVM中不同,BootClassLoader是ClassLoader内部类,是由Java实现的,它也是所有系统ClassLoader的父ClassLoader
    • DexClassLoader:可以用于加载任意路径的zip、dex、jar或者apk文件,也是进行安卓动态加载的基础
    • PathClassLoader:用于加载Android系统类和开发编写应用的类,只能加载已经安装应用的 dex 或 apk 文件

    简单来说,DexClassLoader是动态加载sd卡中的dex,PathClassLoader加载安装时解压在私有路径data/dalvik-cache下的dex。

    在Android系统启动的时候会创建一个Boot类型的ClassLoader实例,用于加载一些系统Framework层级需要的类,我们的Android应用里也需要用到一些系统的类,所以APP启动的时候也会把这个Boot类型的ClassLoader传进来。

    一个运行的Android应用至少有2个ClassLoader。一个是BootClassLoader(系统启动的时候创建的),另一个是PathClassLoader

    BootClassLoader
    class BootClassLoader extends ClassLoader {
        private static BootClassLoader instance;
        public static synchronized BootClassLoader getInstance() {
            if (instance == null) {
                instance = new BootClassLoader();
            }
    
            return instance;
        }
        public BootClassLoader() {
            super(null);
        }
        
    }
    

    BootClassLoader也是ClassLoader的内部类,为单例,parent为空。,

    DexClassLoader
    public class DexClassLoader extends BaseDexClassLoader {
        public DexClassLoader(String dexPath, String optimizedDirectory,
                String librarySearchPath, ClassLoader parent) {
            super(dexPath, new File(optimizedDirectory), librarySearchPath, parent);
        }
    }
    
    PathClassLoader
    public class PathClassLoader extends BaseDexClassLoader {
    
        public PathClassLoader(String dexPath, ClassLoader parent) {
            super(dexPath, null, null, parent);
        }
    
        public PathClassLoader(String dexPath, String librarySearchPath, ClassLoader parent) {
            super(dexPath, null, librarySearchPath, parent);
        }
    }
    

    PathClassLoader在app启动时,在ActivityThread,performLaunchActivity方法中,LoadApk.getClassLoader->LoadApk.createOrUpdateClassLoaderLocked->ApplicationLoaders.getDefault().getClassLoader->ClassLoaderFactory.createClassLoader->new PathClassLoader(dexPath, librarySearchPath, parent);

    这两者只是简单的对BaseDexClassLoader做了一下封装,具体的实现还是在父类里。不过这里也可以看出,PathClassLoader的optimizedDirectory只能是null,进去BaseDexClassLoader看看这个参数是干什么的

    BaseDexClassLoader
    public BaseDexClassLoader(String dexPath, File optimizedDirectory,
            String libraryPath, ClassLoader parent) {
        super(parent);
        this.originalPath = dexPath;
        this.pathList = new DexPathList(this, dexPath, libraryPath, optimizedDirectory);
    }
    

    主要是创建了一个DexPathList对象

    DexPathList
    public DexPathList(ClassLoader definingContext, String dexPath,
            String libraryPath, File optimizedDirectory) {
        ……
        this.dexElements = makeDexElements(splitDexPath(dexPath), optimizedDirectory);
    }
    private static Element[] makeDexElements(ArrayList<File> files,
            File optimizedDirectory) {
        ArrayList<Element> elements = new ArrayList<Element>();
        for (File file : files) {
            ZipFile zip = null;
            DexFile dex = null;
            String name = file.getName();
            if (name.endsWith(DEX_SUFFIX)) {
                dex = loadDexFile(file, optimizedDirectory);
            } else if (name.endsWith(APK_SUFFIX) || name.endsWith(JAR_SUFFIX)
                    || name.endsWith(ZIP_SUFFIX)) {
                zip = new ZipFile(file);
            }
            ……
            if ((zip != null) || (dex != null)) {
                elements.add(new Element(file, zip, dex));
            }
        }
        return elements.toArray(new Element[elements.size()]);
    }
    
    private static DexFile loadDexFile(File file, File optimizedDirectory)
            throws IOException {
        if (optimizedDirectory == null) {
            return new DexFile(file);
        } else {
            String optimizedPath = optimizedPathFor(file, optimizedDirectory);
            return DexFile.loadDex(file.getPath(), optimizedPath, 0);
        }
    }
    
    /**
     * Converts a dex/jar file path and an output directory to an
     * output file path for an associated optimized dex file.
     */
    private static String optimizedPathFor(File path,
            File optimizedDirectory) {
        String fileName = path.getName();
        if (!fileName.endsWith(DEX_SUFFIX)) {
            int lastDot = fileName.lastIndexOf(".");
            if (lastDot < 0) {
                fileName += DEX_SUFFIX;
            } else {
                StringBuilder sb = new StringBuilder(lastDot + 4);
                sb.append(fileName, 0, lastDot);
                sb.append(DEX_SUFFIX);
                fileName = sb.toString();
            }
        }
        File result = new File(optimizedDirectory, fileName);
        return result.getPath();
    }
    

    前面介绍虚拟机的时候说过,odex和oat都是存在/data/dalvik-cache 目录,这也是optimizedDirectory为null时,默认的内部存储路径,也证明PathClassLoader是用来加载已经安装应用的 dex

    从创建代码就可以窥测出一些实现热修复的关键,BaseDexClassLoader中会创建一个对象DexPathList,DexPathList中会使用dexElements 来存储已加载的dex信息,DexFile

    2.4 双亲代理模型

    既然知道dex信息存储对象,那来看看如何加载dex到。

    ClassLoader.loadClass
    protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
        // 1 通过调用c层findLoadedClass检查该类是否被加载过,若加载过则返回class对象(缓存机制),是通过BootClassLoader加载的缓存
        Class c = findLoadedClass(name);
        if (c == null) {
            long t0 = System.nanoTime();
            try {
                if (parent != null) {
                    //2 各种类型的类加载器在构造时都会传入一个parent类加载器
                    //2 若parent类不为空,则调用parent类的loadClass方法
                    c = parent.loadClass(name, false);
                } else {
                    //3 查阅了PathClassLoader、DexClassLoader并没有重写该方法,默认是返回null
                    c = findBootstrapClassOrNull(name);
                }
            } catch (ClassNotFoundException e) {
            }
            if (c == null) {
                //4  如果父ClassLoader不能加载该类才由自己去加载,这个方法从本ClassLoader的搜索路径中查找该类
                long t1 = System.nanoTime();
                c = findClass(name);
            }
        }
        return c;
    }
    protected final Class<?> findLoadedClass(String name) {
        ClassLoader loader;
        //BootClassLoader加载的缓存
        if (this == BootClassLoader.getInstance())
            loader = null;
        else
            loader = this;
        return VMClassLoader.findLoadedClass(loader, name);
    }
    
    1. 系统类会优先通过BootClassLoader进行加载,反正串改系统类。

    2. parent.loadClass(name, false)这步就是双亲委派机制的妙处了。优先从父类读取dex,父类找不到再由自己用findClass去加载。

    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;
    }
    

    可以看出,它是通过pathList来加载类的。

    DexPathList#findClass
    public Class findClass(String name, List<Throwable> suppressed) {
        for (Element element : dexElements) {
            DexFile dex = element.dexFile;
    
            if (dex != null) {
                 //调用到c层defineClassNative方法进行查找
                Class clazz = dex.loadClassBinaryName(name, definingContext, suppressed);
                if (clazz != null) {
                    return clazz;
                }
            }
        }
        if (dexElementsSuppressedExceptions != null) {
            suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions));
        }
        return null;
    }
    

    前面创建对象的时候,BaseDexClassLoader构造函数中创建了pathList对象,DexPathList对象中makeElements方法通过dex路径创建了dexElements对象。而加载类时也是通过遍历dexElements,通过其DexFileloadClassBinaryName来加载类。

    所以这里是==一个热修复的点==,你可以将需要热修复的dex文件插入到dexElements数组前面,这样遍历的时候查到你最新插入的则返回,从而实现动态替换有问题类

    DexFile#loadClassBinaryName
    public Class loadClassBinaryName(String name, ClassLoader loader) {
        return defineClass(name, loader, mCookie);
    }
    private native static Class defineClass(String name, ClassLoader loader, int cookie);
    

    最终还是调用了Native方法defineClass加载类。有趣的是,标准JVM中,ClassLoader是用defineClass加载类的,而Android中defineClass被弃用了,改用了loadClass方法,而且加载类的过程也挪到了DexFile中,在DexFile中加载类的具体方法也叫defineClass,不知道是Google故意写成这样的还是巧合。

    2.5 热修复方案

    2.5.1 向PathClassLoader的dexElements进行插入新的dex(目前最常见的方式)

    从上面的ClassLoader#loadClass方法你就会知道,初始化的时候会进入BaseDexClassLoader#findClass方法中通过遍历dexElements进行查找dex文件,因为dexElements是一个数组,所以我们可以通过反射的形式,将需要热修复的dex文件插入到数组首部,这样遍历数组的时候就会优先读取你插入的dex,从而实现热修复。

    image

    我们可以通过创建一个DexClassLoader对象,来获取要加载dex的dexElements对象,然后插入PathClassLoader的dexElements中。 这也是公司框架的热修复实现方式。

    实现细节,创建热修复中的对象
    //1.创建DexClassLoader对象,这里optimizedDirectory为libs
    DexClassLoader loader = new DexClassLoader(dexPath, context.getDir("libs", 0).getAbsolutePath(), libraryPath, context.getClassLoader());
    //2.获取PathClassLoader对象
    PathClassLoader pathLoader = context.getClassLoader();
    //3.通过反射获取响应的PathList对象
    Object pathLoaderPathList = getField(classLoader, Class.forName("dalvik.system.BaseDexClassLoader"), "pathList");
    Object dexLoaderPathList = getField(classLoader, Class.forName("dalvik.system.BaseDexClassLoader"), "pathList");
    //4.通过反射获取相应的DexElements对象
    Object pathDexElements = getField(pathLoaderPathList, pathLoaderPathList.getClass(), "dexElements");
    Object dexDexElements = getField(dexLoaderPathList, dexLoaderPathList.getClass(), "dexElements");
    //5.将要加载的dex插入PathClassLoader的dexElements中,比较dexFile的mFileName,如果有相同的,先置空以去除重复的dexElements,再将两个dexElements合并
    Object dexElements = combine(pathDexElements, dexDexElements);
    //6.将合并后的dexElements放入pathLoaderPathList中
    setFieldValue(pathLoaderPathList, pathLoaderPathList.getClass(), "dexElements", dexElements);
    //7.获取要创建的类Class,创建DexClassLoader对象,因为双亲委派机制,会由PathClassLoader加载类(直接使用PathClassLoader加载也行)
    loader = new DexClassLoader(dexPath, context.getDir("libs", 0).getAbsolutePath(), libraryPath, context.getClassLoader());
    Class clazz = loader.loadClass(classPath);
    //8.通过反射创建对象
    Constructor<?> constructor = clazz.getConstructor(Context.class);
    Object ret = constructor.newInstance(c);
    
    //通过反射获取变量
    private Object getField(Object obj, Class<?> classObject, String fieldName) {
        Field localField = null;
    
        try {
            if (obj != null && classObject != null && fieldName != null) {
                localField = classObject.getDeclaredField(fieldName);
                if (localField != null) {
                    localField.setAccessible(true);
                    Object var7 = localField.get(obj);
                    return var7;
                }
            }
        } catch (Exception var10) {
        } finally {
        }
    
        return null;
    }
    
    //合并两个dexElements,并去除重复的dexElement
    private Object combine(Object local, Object tar) {
        Class<?> localClass = null;
        int len = 0;
        int len_i = 0;
        int len_local = false;
        int len_tar = false;
        Object locObj = null;
        Object tarObj = null;
        Object loc = null;
        Object result = null;
        Object obj_loc = null;
        Object obj_loc_name = null;
        Object obj_tar = null;
        Object obj_tar_name = null;
    
        try {
            if (local != null && tar != null) {
                int len_local = Array.getLength(local);
                int len_tar = Array.getLength(tar);
                int i;
                //1.去除重复的dexElement
                if (len_local > 0 && len_tar > 0) {
                    for(i = 0; i < len_local; ++i) {
                        locObj = Array.get(local, i);
                        if (locObj != null) {
                            obj_loc = this.a(locObj, locObj.getClass(), "dexFile");
                            if (obj_loc != null) {
                                obj_loc_name = this.a(obj_loc, obj_loc.getClass(), "mFileName");
                                if (obj_loc_name != null) {
                                    for(int j = 0; j < len_tar; ++j) {
                                        tarObj = Array.get(tar, j);
                                        if (tarObj != null) {
                                            obj_tar = this.a(tarObj, tarObj.getClass(), "dexFile");
                                            if (obj_tar != null) {
                                                obj_tar_name = this.a(obj_tar, obj_tar.getClass(), "mFileName");
                                                if (obj_tar_name != null && obj_tar_name.equals(obj_loc_name)) {
                                                    Array.set(local, i, (Object)null);
                                                    break;
                                                }
                                            }
                                        }
                                    }
                                }
                            }
                        }
                    }
                }
    
                for(i = 0; i < len_local; ++i) {
                    locObj = Array.get(local, i);
                    if (locObj != null) {
                        ++len;
                    }
                }
    
                localClass = local.getClass().getComponentType();
                if (localClass != null) {
                    if (len > 0) {
                        loc = Array.newInstance(localClass, len);
                        if (loc != null) {
                            for(i = 0; i < len_local; ++i) {
                                if (Array.get(local, i) != null) {
                                    Array.set(loc, len_i, Array.get(local, i));
                                    ++len_i;
                                }
                            }
                        }
                    }
    
                    result = Array.newInstance(localClass, len + len_tar);
                    if (result != null) {
                        for(i = 0; i < len + len_tar; ++i) {
                            if (i < len) {
                                Array.set(result, i, Array.get(loc, i));
                            } else {
                                Array.set(result, i, Array.get(tar, i - len));
                            }
                        }
    
                        Object var19 = result;
                        return var19;
                    }
                }
            }
        } catch (Exception var22) {
        }
    
        return null;
    }
    
    //通过反射将值存入field中
    private void setFieldValue(Object obj, Class<?> classObject, String field, Object value) {
        Field localField = null;
    
        try {
            if (obj != null && classObject != null && field != null && value != null) {
                localField = classObject.getDeclaredField(field);
                if (localField != null) {
                    localField.setAccessible(true);
                    localField.set(obj, value);
                }
            }
        } catch (Exception var10) {
        } 
    }
    

    2.5.2 通过自定义ClassLoader实现class拦截替换

    我们知道PathClassLoader是加载已安装的apk的dex,那我们可以
    在 PathClassLoader 和 BootClassLoader 之间插入一个 自定义的MyClassLoader,而我们通过ClassLoader#loadClass方法中的第2步知道,若parent不为空,会调用parent.loadClass方法,固我们可以在MyClassLoader中重写loadClass方法,在这个里面做一个判断去拦截替换掉我们需要修复的class。

    也就是说,利用双亲委派模式,在PathClassLoader加载类时,先调用MyClassLoader的loadClass,在自定义的loadClass方法中,加载热修复的类。


    image
    具体实现
    1. 创建MyClassLoader,parent要设置为pathClassLoader的Parent
    ClassLoader pathClassLoader = MyApplication.getContext().getClassLoader();
    MyClassLoader myClassLoader = new MyClassLoader(pathClassLoader.getParent());
    
    1. 将pathClassLoader的Parent设为MyClassLoader,将MyClassLoader插入PathClassLoader 和 BootClassLoader 之间
    Field parentField = ClassLoader.class.getDeclaredField("parent");
    parentField.setAccessible(true);
    parentField.set(classLoader, newParent);
    
    1. 利用DexClassLoader加载热修复的类。
    DexClassLoader dexClassLoader = new DexClassLoader(dexPath, context.getDir("dex",context.MODE_PRIVATE).getAbsolutePath(),null, context.getClassLoader());
    Class<?> herosClass = dexClassLoader.getClass().getSuperclass();
    Method m1 = herosClass.getDeclaredMethod("findClass", String.class);
    m1.setAccessible(true);
    Class newClass = (Class) m1.invoke(dexClassLoader, className);
    
    1. 存入自定义的MyClassLoader中,在其loadClass加载相应的热加载类
    myClassLoader.registerClass(className, newClass);
    

    2.6 CLASS_ISPREVERIFIED标记

    android5.0之前,在 Dalvik虚拟机下,执行 dexopt 时,会对类进行扫描,如果类里面所有直接依赖的类都在同一个 dex 文件中,那么这个类就会被打上 CLASS_ISPREVERIFIED 标记,表示这个类已经预先验证过了。如果一个类有 CLASS_ISPREVERIFIED标记,那么在热修复时,它加载了其他 dex 文件中的类,会报经典的Class ref in pre-verified class resolved to unexpected implementation异常

    为了解决这个问题,QQ空间给出的解决方案就是,准备一个 AntilazyLoad 类,这个类会单独打包成一个 hack.dex,然后在所有的类的构造方法中增加这样的代码:

    if (ClassVerifier.PREVENT_VERIFY) {
       System.out.println(AntilazyLoad.class);
    }
    

    复制代码这样在 odex 过程中,每个类都会出现 AntilazyLoad 在另一个dex文件中的问题,所以odex的验证过程也就不会继续下去,这样做牺牲了dvm对dex的优化效果了。

    总结

    本文先介绍了

    • Dalvik与art虚拟机,
    • jit即时编译与aot静态编译,
    • dex与odex与oat格式文件。
    • PathClassLoader,DexClassLoader,BaseDexClassLoader的关系;
    • 双亲委派机制加载类;
    • BaseDexClassLoader构造函数中创建了pathList对象,DexPathList对象中makeElements方法通过dex路径创建了dexElements对象。而加载类时也是通过遍历dexElements,通过其DexFile的loadClassBinaryName来加载类。
    • 利用加载机制实现热修复的两种方式:1.插入PathClassLoader的dexElements,利用其记载类的顺序;2.利用双亲委派机制,自定义ClassLoader插入PathClassLoader 和 BootClassLoader 之间,通过自定义的loadClass来加载类。

    参考

    Android虚拟机的JIT编译器

    Dalvik和Art,JIT ,AOT, oat, dex, odex

    说一说Android的Dalvik,ART与JIT,AOT

    解读:华为方舟编译器的革命性到底体现在哪里?

    9102年了,还不知道Android为什么卡?

    Android动态加载基础 ClassLoader工作机制

    剖析ClassLoader深入热修复原理

    2018 深入解析Android热修复技术

    相关文章

      网友评论

        本文标题:android ClassLoader实现热修复

        本文链接:https://www.haomeiwen.com/subject/hvgsnctx.html