Android动态加载之ClassLoader —热修复、插件化

Android平台上虚拟机运行的是Dex字节码,一种对class文件优化的产物,传统Class文件是一个Java源码文件会生成一个.class文件,而Android是把所有Class文件进行合并,优化,然后生成一个最终的class.dex,目的是把不同class文件重复的东西只需保留一份,如果我们的Android应用不进行分dex处理,最后一个应用的apk只会有一个dex文件。

Android平台的ClassLoader

Android动态加载之ClassLoader —热修复、插件化_第1张图片
image.png

我们主要用到了两种类加载器 PathClassLoader 和 DexClassLoader 。

他们的区别是:
PathClassLoader:是 Android 应用中默认的类加载器,只能加载已经安装到 Android 系统中的apk文件(/data/app目录下,解压为 dex 后优化为 odex)。

DexClassLoader:可以加载路径下的 dex/jar/apk/zip 文件,比 PathClassLoader 更灵活,是实现热修复的关键。

首先说下双亲委托模式

双亲委托模式的特点
类加载器查找Class所采用的是双亲委托模式,所谓双亲委托模式就是首先判断该Class是否已经加载,如果没有则不是自身去查找而是委托给父加载器进行查找,这样依次的进行递归,直到委托到最顶层的Bootstrap ClassLoader,如果Bootstrap ClassLoader找到了该Class,就会直接返回,如果没找到,则继续依次向下查找,如果还没找到则最后会交由自身去查找。
这样讲可能会有些抽象,来看下面的图。


Android动态加载之ClassLoader —热修复、插件化_第2张图片
image.png

再说dex文件的加载和类的查找过程
Java层通过我们会通过创建一个DexClassLoader来加载我们的dex,下面就以此为切入点进行

创建ClassLoader
dexClassLoader = new DexClassLoader(apkPath, getFilesDir().getAbsolutePath(), null, getClassLoader());

查看DexClassLoader的构造方法。

public class DexClassLoader extends BaseDexClassLoader {
    // dexPath:是加载apk/dex/jar的路径
    // optimizedDirectory:是优化dex后得到的.odex文件的输出路径
    // libraryPath:是加载的时候需要用到的so库
    // parent:给DexClassLoader指定父加载器
    public DexClassLoader(String dexPath, String optimizedDirectory,
            String libraryPath, ClassLoader parent) {
        super(dexPath, new File(optimizedDirectory), libraryPath, parent);
    }
}

可以看到它调用的是父类的构造函数,所以直接来看BaseDexClassLoader的构造函数。

 private final DexPathList pathList;

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

创建了一个DexPathList实例,下面来看看DexPathList的构造函数。

private final Element[] dexElements;

public DexPathList(ClassLoader definingContext, String dexPath,
        String libraryPath, File optimizedDirectory) {
    ArrayList suppressedExceptions = new ArrayList();
    this.dexElements = makeDexElements(splitDexPath(dexPath), optimizedDirectory,
                                       suppressedExceptions);
}

//它调用的是makeDexElements方法来创建一个Element数组来存放Element对象,

//每个Element对象包含一个DexFile对象。
private static Element[] makeDexElements(ArrayList files, File optimizedDirectory,
                                         ArrayList suppressedExceptions) {
    ArrayList elements = new ArrayList();
    /*
     * Open all files and load the (direct or contained) dex files
     * up front.
     */
    for (File file : files) {
        File zip = null;
        DexFile dex = null;
        String name = file.getName();

        // 如果是一个dex文件
        if (name.endsWith(DEX_SUFFIX)) {
            // Raw dex file (not inside a zip/jar).
            try {
                dex = loadDexFile(file, optimizedDirectory);
            } catch (IOException ex) {
                System.logE("Unable to load dex file: " + file, ex);
            }
        // 如果是一个apk或者jar或者zip文件
        } else if (name.endsWith(APK_SUFFIX) || name.endsWith(JAR_SUFFIX)
                || name.endsWith(ZIP_SUFFIX)) {
            zip = file;

            try {
                // 1、调用loadDexFile加载dex文件,得到一个DexFile对象
                 //   loadDexFile通过c++层native方法去加载dex文件
                dex = loadDexFile(file, optimizedDirectory);
            } catch (IOException suppressed) {
               
                suppressedExceptions.add(suppressed);
            }
        } else if (file.isDirectory()) {
            elements.add(new Element(file, true, null, null));
        } else {
            System.logW("Unknown file type for: " + file);
        }
        
        // 2、把DexFile对象封装到Element对象中,然后将Element对象加入Element数组
        if ((zip != null) || (dex != null)) {
            elements.add(new Element(file, false, zip, dex));
        }
    }
    return elements.toArray(new Element[elements.size()]);
}

dex文件的加载流程:我们会使用DexClassLoader去加载dex文件,DexClassLoader会将这个任务委派给DexPathList中的makeDexElements方法,在makeDexElements中调用了native层的 c++方法去真正的加载dex文件,然后返回DexFile的对象,通过这个对象构建一个Element的对象,然后将这个Element添加到dexElements的数组中。

类的加载过程

当类加载器收到加载类或资源的请求时,通常都是先委托给父类加载器加载,也就是说只有当父类加载器找不到指定类或资源时,自身才会执行实际的类加载过程,源码如下

protected Class loadClass(String className, boolean resolve) throws ClassNotFoundException {
    // 首先从已经加载的类中查找
    Class clazz = findLoadedClass(className);
    if (clazz == null) {
        ClassNotFoundException suppressed = null;
        try {
            // 如果没有加载过,先调用父加载器的 loadClass
            clazz = parent.loadClass(className, false);
        } catch (ClassNotFoundException e) {
            suppressed = e;
        }
        if (clazz == null) {
            try {
                // 父加载器都没有加载,则尝试自己去加载
                clazz = findClass(className);
            } catch (ClassNotFoundException e) {
                e.addSuppressed(suppressed);
                throw e;
            }
        }
    }
    return clazz;
}

一句话概括:ClassLoader 加载类时,先查看自身是否已经加载过该类,如果没有加载过会首先让父加载器去加载,如果父加载器无法加载该类时才会调用自身的 findClass 方法加载,该逻辑避免了类的重复加载。所以我们所要实现的就是把要替换的类可见性提前,这样类加载器就会优先找到修复过的类。

类的查找过程

//DexClassLoader间接调用父类findClass方法
//,findClass方法中调用DexPathList中的DexPathList方法
public BaseDexClassLoader(String dexPath, File optimizedDirectory,
            String libraryPath, ClassLoader parent) {
        super(parent);
        this.originalPath = dexPath;
        this.pathList =
            new DexPathList(this, dexPath, libraryPath, optimizedDirectory);
    }
    @Override
    protected Class findClass(String name) throws ClassNotFoundException {
        Class clazz = pathList.findClass(name);
        if (clazz == null) {
            throw new ClassNotFoundException(name);
        }
        return clazz;
    }

//看DexPathList中的findClass方法,可以看到它是遍历dexElements数组,
//到每个dex文件去寻找当前需要的类,找到之后直接返回不往下找了
    public Class findClass(String name) {
        for (Element element : dexElements) {
            DexFile dex = element.dexFile;
            if (dex != null) {
                Class clazz = dex.loadClassBinaryName(name, definingContext);
                if (clazz != null) {
                    return clazz;
                }
            }
        }
        return null;
    }

类的查找过程:DexClassLoader通过findClass去查找一个类,同样它也是委派给DexPathList的findClass去查找,在DexPathList的findClass中会去遍历我们上面创建的dexElements数组,然后在每个dex中去查找相应的类,找到之后就返回,不再向后查找。

热修复过程

1、PathClassLoader 作为默认的类加载器,也就是第一个 DEX 文件是 PathClassLoader 自动加载的。
2、通过前面的分析来看,我们知道 PathClassLoader 里面的 DEX 文件是存放在一个 Element 数组中,可以包含多个 DEX 文件,所以我们只需要通过反射获取 PathClassLoader 中的 DexPathList 中的Element数组(已加载了第一个dex包,由系统加载),将要替换的 DEX 文件放置到这个数组中去。
3、将两个Element数组合并之后,再将其赋值给 PathClassLoader 的 Element 数组
Bugly热更新是基于Tinker的 Bugly 热更新原理图如下:


Android动态加载之ClassLoader —热修复、插件化_第3张图片
image.png

Android动态加载之ClassLoader —热修复、插件化_第4张图片
image.png

如版本 1.0.0 上线后有bug 启动1.0.0 版本app的时候 会把当前对应的TinkerId上报到平台,
修改相关代码 执行第二步骤 打补丁包后 补丁包里面会有一个YAPATH.md文件,from 表示我发布的版本, to 表示我现在打补丁包,意思就是 这个1.0.0-patch这个补丁包 是针对于1.0.0-base 来修复的,
上传补丁包到配置平台后,平台会自动解析YAPATH.md文件, 然后针对性的下发补丁。

这篇文章写得很好

你可能感兴趣的:(Android动态加载之ClassLoader —热修复、插件化)