Android 类加载机制以及基于类加载机制的热修复

android 与 java 的类加载器

类别 加载文件 类加载器分类
java .class 文件 {{java类加载机制}}
android .dex 文件 {{android类加载机制}}

java 类加载机制

  1. BootStrapClassLoader:
    启动类加载器
    由 C/C++代码实现
    加载 .../jre/lib 下类库 / -Xbootclasspath 参数指定的路径中的类库
    顶级类加载器

  2. ExtClassLoader
    扩展类加载器
    sun.misc.Launcher 中的内部类(即 sun.misc.Launcher$ExtClassLoader)
    加载 .../jre/lib/ext 下的类库 / -Djava.ext.dirs 参数指定的路径中的类库
    parent: 无,此时可以看做 parent 就是 BootStrapClassLoader

  3. AppClassLoader
    应用程序类加载器
    sun.misc.Launcher 中的内部类 (即 sun.misc.Launcher$AppClassLoader)
    加载 classpath 所指定的类库
    parent: ExtClassLoader

  4. 自定义 classLoader
    继承 ClassLoader
    加载自行指定位置的类库
    parent: AppClassLoader

  • java 类加载机制采用双亲委派机制:
protected Class<?> loadClass(String name, boolean resolve)throws ClassNotFoundException{
    synchronized (getClassLoadingLock(name)) {
        // First, check if the class has already been loaded
        //1.检查请求加载类是否已经加载
        Class c = findLoadedClass(name);
        if (c == null) {
            long t0 = System.nanoTime();
            try {
                //2.请求加载类还未加载,parent!=null,通过parent加载
                if (parent != null) {
                    c = parent.loadClass(name, false);
                } else {
                    //3.parent == null,尝试采用BootStrapClassLoader启动类加载器加载
                    c = findBootstrapClassOrNull(name);
                }
            } catch (ClassNotFoundException e) {
                // ClassNotFoundException thrown if class not found
                // from the non-null parent class loader
            }

            //4.以上三步均未成功加载,通过本身加载
            if (c == null) {
                // If still not found, then invoke findClass in order
                // to find the class.
                long t1 = System.nanoTime();
                c = findClass(name);

                // this is the defining class loader; record the stats
                sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                sun.misc.PerfCounter.getFindClasses().increment();
            }
        }
        if (resolve) {
            resolveClass(c);
        }
        return c;
    }
}
  • 自定义 classLoader
//继承ClassLoader
public class MyClassLoader extends ClassLoader {
    private String classPath;

    public MyClassLoader(String classPath) {
        this.classPath = classPath;
    }

    //重写findClass(String name)方法
    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        try {
            byte[] data = getByte(name);
            //调用defineClass(name, data, 0, data.length)返回class
            return defineClass(name, data, 0, data.length);
        } catch (Exception e) {
            e.printStackTrace();
            throw new ClassNotFoundException();
        }
    }

    //获取流
    private byte[] getByte(String name) throws Exception {
        name = name.replaceAll("\\.", "/");
        FileInputStream fis = new FileInputStream(name);
        int len = fis.available();
        byte[] data = new byte[len];
        fis.read(data);
        fis.close();
        return data;
    }
}

Android 类加载器机制

注: 除了 classLoader 与 BootClassLoader,可以通过 as 直接查看,其他均无法直接查看,源码取自互联网

  1. BootClassLoader
    java.lang.ClassLoader 中的内部类(即 java.lang.ClassLoader$BootClassLoader)
    预加载常用类

  2. PathClassLoader
    dalvik.system.PathClassLoader
    加载已经安装的 Apk(/data/app/package)的 Apk 文件
    只有构造方法

package dalvik.system;

public class PathClassLoader extends BaseDexClassLoader {

    public PathClassLoader(String dexPath, ClassLoader parent) {
        super(dexPath, null, null, parent);
    }

    public PathClassLoader(String dexPath, String libraryPath,
            ClassLoader parent) {
        super(dexPath, null, libraryPath, parent);
    }
}
  1. DexClassLoader
    dalvik.system.DexClassLoader
    加载任意位置的 dex/zip/apk/jar
    只有构造方法
package dalvik.system;
import java.io.File;
public class DexClassLoader extends BaseDexClassLoader {
    public DexClassLoader(String dexPath, String optimizedDirectory,
            String libraryPath, ClassLoader parent) {
        super(dexPath, new File(optimizedDirectory), libraryPath, parent);
    }
}
  1. BaseDexClassLoader
    PathClassLoader/DexClassLoader 均继承于 BaseDexClassLoader
  • PathClassLoader 与 DexClassLoader 的区别
    只有构造函数所传参数不同
    DexClassLoader 比 PathClassLoader 多传 optimizedDirectory: dex 优化缓存路径
    optimizedDirectory : 用于存放:加载 jar/apk/zip 等压缩格式的程序文件时解压出其中的 dex 文件,如果本身只是 dex 文件,也会进行复制到此文件中
    必须是需要加载的程序目录:即 data/data/packname/xxx
  • android 中的 java.lang.ClassLoader 与 java 中的 java.lang.ClassLoader 并不相同 *
//可以看到android是无法通过defineClass来获取class类的
protected final Class<?> defineClass(String name, byte[] b, int off, int len)throws ClassFormatError{
        throw new UnsupportedOperationException("can't load this type of class file");
}
  • DexClassLoader 类加载流程
  • BaseDexClassLoader
private final DexPathList pathList;

/**
 * 首先执行ClassLoader构造方法
 * 之后执行PathClassLoader/DexClassLoader的构造方法
 * 创建DexPathList对象
 */
public class BaseDexClassLoader extends ClassLoader {
    ...
    public BaseDexClassLoader(String dexPath, File optimizedDirectory, String libraryPath, ClassLoader parent){
        super(parent);
        this.pathList = new DexPathList(this, dexPath, libraryPath, optimizedDirectory);
    }
    ...
}

//findclass是交由构造方法中所创建的DexPathList对象来执行的
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
    List<Throwable> suppressedExceptions = new ArrayList<Throwable>();
    // 实质是通过pathList的对象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;

/**
 * 创建Element[]数组
 * dexPath是要加载的dex/zip/jar/apk的原始路径,支持多个以:分割
 * 遍历存入优化缓存路径当中
 * 包装成Element对象,以Element[]数组的形式返回
 */
public DexPathList(ClassLoader definingContext, String dexPath,
        String libraryPath, File optimizedDirectory) {
    ...
    this.dexElements = makeDexElements(splitDexPath(dexPath), optimizedDirectory,suppressedExceptions);
    ...
}

/**
 * 遍历Element[]数组
 * 找到name所对应的DexFile
 * 由DexFile加载dex文件,值得注意的是,加载成功一个就会直接返回,后面有相同name的也不会加载了
 * 通过类加载机制实现的热修复就是采用这个机制,将修复类的dex放在Element[]数组的最前方,则只会加载修复类的dex
 * 而不会去加载本身所具有的dex即具有bug的dex
 */
public Class findClass(String name, List<Throwable> suppressed) {
    for (Element element : dexElements) {
        DexFile dex = element.dexFile;
        if (dex != null) {
            Class clazz = dex.loadClassBinaryName(name, definingContext, suppressed);
            if (clazz != null) {
                return clazz;
            }
        }
    }
    if (dexElementsSuppressedExceptions != null) {
        suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions));
    }
    return null;
}

基于 android 类加载机制实现的热修复

从以上的分析可以得知:

  • DexClassLoader 创建
    构造方法传入希望加载的任意路径的 dex/zip/jar/apk 路径,以:分割
    构造方法传入优化缓存路径,需 data/data/packageName/xxx
  • BaseDexClassLoader
    构造方法创建 DexPathList 对象
    DexPathList 对象构造方法获取 Element[]数组
    获取 Element[]数组的过程是将 dex 文件包装成 Element 对象,并将任意位置的 dex/zip/jar/apk 存储到指定的优化缓存路径当中

  • DexClassLoader 继承于 BaseDexClassLoader
    DexClassLoader 并没有重写 BaseDexClassLoader 的 findClass()方法
    DexClassLoader 调用 findClass()
    BaseDexClassLoader 调用 findClass()
    DexPathList 调用 findClass()
    遍历 Element[]数组,根据所传入 name 来加载对应 dex 文件
    找到第一个之后,则直接加载并返回,后续的不再继续进行查找

  • 基于 android 类加载机制实现的热修复
    任意位置存入修复后的 dex/zip/jar/apk,设置优化缓存路径
    创建 DexClassLoader
    构造方法将任意位置存入的修复后的 dex/zip/jar/apk 存入设置的优化缓存路径当中,并包装成 Element[]数组
    此时,我们未进行 findClass(), 修复后的 dex 文件并没有被类加载器加载
    最理想的情况是,在第一次调用出现 bug 之前,findClass()进行修复后的 dex 文件加载替换掉有 bug 的 dex
    我们也不可能去判断哪个时候是第一次加载,然后在其之前调用 DexClassLoader 中的 findClass()来加载,因为当我们上一次发布,即具有 bug 的程序时,是根本不知道这个 bug 的存在的
    那么,换一个角度,当通过类加载器加载我们普通的 apk 中的类的时候,是通过 PathClassLoader 来完成的,那么,只需要修改 PathClassLoader 当中的 Element[]数组即可
    将通过 DexClassLoader 生成的 Element[]数组,放置于 PathClassLoader 的 Element[]之前
    那么,第一次加载 bug 的类的时候,系统通过 PathClassLoader 进行 findClass(),而此时在最前面的是我们在 DexClassLoader 中生成的 Element[]数组,这样,就不会再加载原有的具有 bug 的类了,也就是完成了修复

示例:
github示例

如何生成dex文件

  • 注意:

当已经通过类加载器加载了具有 bug 的类之后,修复类则不会加载了,所以我们将修复放在尽可能前一些的位置,这也是为什么我们常说的热修复需要重启的原因,可能在我们的修复包发送之前,客户已经启动过我们的应用了,也已经加载过 bug 类,他一直没有退过,则永远不会加载修复类

就像示例中所展示的,如果先点击了 add,则已经加载过 test 类,再点击就没有作用了
这时候,只需要把程序退出,然后重新启动,先点击修复,再点击 add,则可以正确执行

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,163评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,301评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,089评论 0 352
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,093评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,110评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,079评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,005评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,840评论 0 273
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,278评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,497评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,667评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,394评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,980评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,628评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,796评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,649评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,548评论 2 352