Android类加载机制及热修复原理

604 阅读4分钟

类加载机制-概念

主要是应用了和Java相似的双亲委派模型,可以参考这篇文章Java虚拟机-类加载机制

其中验证要加载的多个类都是同一个类的成立条件为,

  1. 相同的className
  2. 相同的packageName
  3. 被相同的classLoader加载

Android中的类加载器

类型

Android主要的类加载器有四个,

  1. BootClassLoader,加载Android Framework层中的class字节码文件(与Java的Bootstrap ClassLoader类似)
  2. PathClassLoader,加载已经安装到系统中apk的class字节码文件(与Java的Application ClassLoader类似)
  3. DexClassLoader,加载制定目录的class字节码文件(与ava中的Custom ClassLoader类似)
  4. BaseDexClassLoaderPathClassLoaderDexClassLoader的父类

他们之间的继承关系,

源码

loadClass()

public Class<?> loadClass(String name) throws ClassNotFoundException {
    return loadClass(name, false);
}

protected Class<?> loadClass(String name, boolean resolve)
    throws ClassNotFoundException {
    // 步骤一
    Class<?> c = findLoadedClass(name);
    if (c == null) {
        try {
            if (parent != null) {
                //步骤二
                c = parent.loadClass(name, false);
            } else {
                c = findBootstrapClassOrNull(name);
            }
        } catch (ClassNotFoundException e) {
            // ClassNotFoundException thrown if class not found
            // from the non-null parent class loader
        }

        if (c == null) {
            // 步骤三
            c = findClass(name);
        }
    }
    return c;
}

通过源码可以看出,

  1. 调用loadClass方法,先在当前类加载器中查找是否已经加载了该类,有则返回该类,没有跳到步骤2
  2. 调用父类加载器对应的loadClass方法,重复步骤一,直到顶层类加载器
  3. 如果所有的类加载器都没有加载过该类,则调用findClass去dex文件中加载这个class

PathClassLoader与DexClassLoader

  1. PathClassLoader:只能加载已经安装到Android系统中的apk文件(/data/app目录),是Android默认使用的类加载器。
// libcore/dalvik/src/main/java/dalvik/system/PathClassLoader.java

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);
    }
}
  1. DexClassLoader:可以加载任意目录下的dex/jar/apk/zip文件,比PathClassLoader更灵活,是实现热修复的重点
public class DexClassLoader extends BaseDexClassLoader {
    
    public DexClassLoader(String dexPath, String optimizedDirectory,
            String librarySearchPath, ClassLoader parent) {
        super(dexPath, null, librarySearchPath, parent);
    }
}

类加载过程

通过上述分析,DexClassLoaderPathClassLoader其具体实现都是通过他们的父类来完成的,我们先看看BaseDexClassLoader的构造函数,

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

参数 optimizedDirectory 需要重点提一下,

应用程序在第一次被加载的时候,为了提高以后的启动速度和执行效率,Android 系统会对 dex 相关文件做一定程度的优化,并生成一个 odex 文件,此后再运行这个应用程序的时候,只要加载优化过的 odex 文件就行了,省去了每次都要优化的时间,而参数 optimizedDirectory 就是代表存储 odex 文件的路径,这个路径必须是一个内部存储路径

PathClassLoader 没有参数 optimizedDirectory,这是因为 PathClassLoader 已经默认了参数 optimizedDirectory 的路径为:/data/dalvik-cache

findClass

private final DexPathList pathList;

@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
    List<Throwable> suppressedExceptions = new ArrayList<Throwable>();
    // 实际调用的是DexPathList.findClass()方法来获取class
    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;
}

DexPathList中,他的构造函数需要重点分析,

private final Element[] dexElements;

//简化过的代码
public DexPathList(ClassLoader definingContext, String dexPath,
        String libraryPath, File optimizedDirectory) {
    ...
    //构造了一个Element对象的数组
    this.dexElements = makeDexElements(splitDexPath(dexPath), optimizedDirectory,suppressedExceptions);
    ...
}

makeDexElements方法中,以遍历dex文件的方式,将(dex、apk、jar、zip等等文件)封装成Element对象,最后作为Elements数组返回,

//简化过的代码
private static Element[] makeDexElements(ArrayList<File> files, 
File optimizedDirectory, ArrayList<IOException> suppressedExceptions) {
    // 1.创建Element集合
    ArrayList<Element> elements = new ArrayList<Element>();
    // 2.遍历所有dex文件(也可能是jar、apk或zip文件)
    for (File file : files) {
        ZipFile zip = null;
        DexFile dex = null;
        String name = file.getName();
        ...
        // 如果是dex文件
        if (name.endsWith(DEX_SUFFIX)) {
            dex = loadDexFile(file, optimizedDirectory);

        // 如果是apk、jar、zip文件,不同的Android版本处理方式不同
        } else {
            zip = file;
            dex = loadDexFile(file, optimizedDirectory);
        }
        ...
        // 3.将dex文件或压缩文件包装成Element对象,并添加到Element集合中
        if ((zip != null) || (dex != null)) {
            elements.add(new Element(file, false, zip, dex));
        }
    }
    // 4.将Element集合转成Element数组返回
    return elements.toArray(new Element[elements.size()]);
}

DexPathList构造好了Element数组后,我们来看看它的findClass方法,

public Class findClass(String name, List<Throwable> suppressed) {
    for (Element element : dexElements) {
        // 遍历得到一个dex文件
        DexFile dex = element.dexFile;

        if (dex != null) {
            // 在dex文件中查找类名是参数name的类
            Class clazz = dex.loadClassBinaryName(name, definingContext, suppressed);
            if (clazz != null) {
                return clazz;
            }
        }
    }
    if (dexElementsSuppressedExceptions != null) {
        suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions));
    }
    return null;
}

最终通过DexFile.loadClassBinaryName在dex文件中查找到对应的class类,如果找不到则返回null。

热修复原理

综上可以看出,热修复核心逻辑是在DexPathList.findClass()过程中,一个Classloader可以包含多个dex文件,每个dex文件被封装到一个Element对象,这些Element对象排列成有序的数组dexElements。当查找某个类时,会遍历所有的dex文件,如果找到则直接返回,不再继续遍历dexElements。

也就是说当两个类不同的dex中出现,会优先处理排在前面的dex文件,这便是热修复的核心精髓,将需要修复的类所打包的dex文件插入到dexElements前面。当然有bug的class仍然存在,但因为类加载机制没有办法被重新加载而已。