Android 动态加载原理

热修复、mutidex等都是基于安卓动态加载实现的动态插装dex文件的应用实例,那么究竟他们都是如何实现的,让我们花些时间了解一下原理。

ClassLoader

ClassLoader 和 BootClassLoader

看安卓中ClassLoader的源码实现可以看到,它是一个抽象类,构造器中需要传入一个parent ClassLoader并且不能为空,默认的parent ClassLoader为PathClassLoader且它的parent的ClassLoader为BootClassLoader。

private static ClassLoader createSystemClassLoader() {
    String classPath = System.getProperty("java.class.path", ".");
    return new PathClassLoader(classPath, BootClassLoader.getInstance());
}

BootClassLoader是Android平台上所有ClassLoader的最终parent,这个内部类是包内可见,所以我们没法使用和修改。

双亲代理模型加载

刚才提到的parent ClassLoader如何理解?这里的ClassLoader是抽象类,所有的ClassLoader实现类都一定继承自这个抽象类,并且通过代理模式的方式传入了一个parent ClassLoader实例,就算不传,也会默认给你生成一个PathClassLoader作为parent ClassLoader,可以理解为多继承,这就是双亲代理模型。

在ClassLoader中最核心的方法是loadClass方法,在java1.2之后不建议子类重写该方法,而是建议子类修改findClass方法。

protected Class<?> loadClass(String className, boolean resolve) throws ClassNotFoundException {
        Class<?> clazz = findLoadedClass(className);

        if (clazz == null) {
            ClassNotFoundException suppressed = null;
            try {
                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;
    }

可以看到loadClass方法在加载一个类的实例的时候,会先查询当前ClassLoader实例是否加载过此类,有就返回,如果没有就查询parent ClassLoader是否已经加载过此类,如果已经加载过,就直接返回parent加载的类,如果继承路线上的ClassLoader都没有加载,才由Child执行类的加载工作。也就是说如果一个类被位于树根的ClassLoader加载过,那么在以后整个系统的生命周期内,这个类永远不会被重新加载。

通过这种方式可以更好的实现共享和隔离,同一个类不需要被重复的加载,同时一些核心类也不会被用户的同名类替换,更加安全。

在这里也可以抛出一个问题,当我们使用热修复修复代码时,两个同样的类只要保证新的类被优先加载,旧的类就不会生效,这便是像nuwa这种热修复框架的原理,那么等下我们再看如何保证新类被优先加载这个问题。

DexClassLoader 和 PathClassLoader

在Android中,我们一般是使用DexClassLoader、PathClassLoader这些类加载器来加载类,它们的不同之处是:

  • DexClassLoader可以加载jar/apk/dex,可以从SD卡中加载未安装的apk;
  • PathClassLoader只能加载系统中已经安装过的apk;
// DexClassLoader.java
public class DexClassLoader extends BaseDexClassLoader {
    public DexClassLoader(String dexPath, String optimizedDirectory,
            String libraryPath, ClassLoader parent) {
        super(dexPath, new File(optimizedDirectory), libraryPath, parent);
    }
}

// PathClassLoader.java
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);
    }
}

从代码中可以看到DexClassLoader和PathClassLoader主要不同是在于传入的optimizedDirectory一个为null,一个不为null。

optimizedDirectory是一个内部存储路径,DexClassLoader可以指定自己的optimizedDirectory,所以它可以加载外部的dex,我们可以把外部的dex复制到optimizedDirectory路径下;而PathClassLoader没有optimizedDirectory,会有个默认的路径,只能加载系统的类和已经安装的应用apk的类。这个ClassLoader不建议开发者使用。

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

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

在主要的实现类BaseDexClassLoader中,有一个DexPathList对象pathList,在这个对象中有个Element数组对象dexElements,通过调用一个private方法makeDexElements生成的一个dex列表。

Load Class

在app启动的时候会创建一个PathClassLoader,用来加载apk文件中第一个dex,也就是说严格意义上讲,我们一个app的所有class都是通过这个PathClassLoader加载进入davlik虚拟机的,也就是我们Context内的getClassLoader返回的对象。

class文件是通过ClassLoader的loadClass方法被load进虚拟机, 在第一次时会通过findClass方法来加载class,实际上调用的是BaseClassLoader的findClass方法:

// BaseDexClassLoader
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        Class clazz = pathList.findClass(name);
        if (clazz == null) {
            throw new ClassNotFoundException(name);
        }
        return clazz;
    }
// DexPathList    
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;
    }

可以看到最终是通过循环遍历pathList中的dexElements列表,通过每个dexFile二分查找class文件,命中即返回。刚才提到过dexElements,里面存放的是个dex列表,这个列表是在classLoader生成过程中就被写入到内存的。

这样整个loadClass的过程就走通了,接下来我们看看mutidex和nuwa都干了些什么,顺便回答我们上面提出的一个问题——如何保证新类被优先加载。

MutiDex

MutiDex的原理我就不展开了,感觉这两篇还不错,一篇讲源码一篇讲使用

http://www.jianshu.com/p/79a14d340cb0
http://souly.cn/%E6%8A%80%E6%9C%AF%E5%8D%9A%E6%96%87/2016/02/25/android%E5%88%86%E5%8C%85%E5%8E%9F%E7%90%86/

看mutidex核心插入dex的代码:

 private static void install(ClassLoader loader, List<File> additionalClassPathEntries, File optimizedDirectory) throws IllegalArgumentException, IllegalAccessException, NoSuchFieldException, InvocationTargetException, NoSuchMethodException {
            // 通过反射获取 ClassLoader中的pathList
            Field pathListField = MultiDex.findField(loader, "pathList");
            Object dexPathList = pathListField.get(loader);
            // 先调用pathList的makeDexElements,然后将生成的Element[]传入expandFieldArray中
            MultiDex.expandFieldArray(dexPathList, "dexElements", makeDexElements(dexPathList, new ArrayList(additionalClassPathEntries), optimizedDirectory));
        }

        private static Object[] makeDexElements(Object dexPathList, ArrayList<File> files, File optimizedDirectory) throws IllegalAccessException, InvocationTargetException, NoSuchMethodException {

            Method makeDexElements = MultiDex.findMethod(dexPathList, "makeDexElements", new Class[]{ArrayList.class, File.class});
            return (Object[])((Object[])makeDexElements.invoke(dexPathList, new Object[]{files, optimizedDirectory}));
}

MultiDex.expandFieldArray方法的实现如下:

private static void expandFieldArray(Object instance, String fieldName, Object[] extraElements) throws NoSuchFieldException, IllegalArgumentException, IllegalAccessException {
    Field jlrField = findField(instance, fieldName);
    Object[] original = (Object[])((Object[])jlrField.get(instance));
    Object[] combined = (Object[])((Object[])Array.newInstance(original.getClass().getComponentType(), original.length + extraElements.length));
    System.arraycopy(original, 0, combined, 0, original.length);
    System.arraycopy(extraElements, 0, combined, original.length, extraElements.length);
    jlrField.set(instance, combined);
}

将dex2...3...4插到了dexElements主dex后面,从而实现了动态插装。

这个事情发生在attachBaseContext的回调,看源码可以知道,它发生的时间比Application onCreate的回调靠前,所以就意味着在onCreate的时候所有的dex代码都已经插装完成了。

一般我们使用nuwa插入代码的调用是发生在onCreate的回调中,看nuwa的核心源码:

DexClassLoader dexClassLoader = new DexClassLoader(dexPath, defaultDexOptPath, dexPath, getPathClassLoader());
Object baseDexElements = getDexElements(getPathList(getPathClassLoader()));
Object newDexElements = getDexElements(getPathList(dexClassLoader));
Object allDexElements = combineArray(newDexElements, baseDexElements);
Object pathList = getPathList(getPathClassLoader());
ReflectionUtils.setField(pathList, pathList.getClass(), "dexElements", allDexElements);

private static Object combineArray(Object firstArray, Object secondArray) {
        Class<?> loadClass = firstArray.getClass().getComponentType();
        int firstArrayLength = Array.getLength(firstArray);
        int allLength = firstArrayLength + Array.getLength(secondArray);
        Object result = Array.newInstance(loadClass, allLength);
        for (int k = 0; k < allLength; ++k) {
            if (k < firstArrayLength) {
                Array.set(result, k, Array.get(firstArray, k));
            } else {
                Array.set(result, k, Array.get(secondArray, k - firstArrayLength));
            }
        }
        return result;
    }

注意nuwa是把dex文件插在了列表的最前面,这就回答了我一开始的问题,只要命中了新dex中的class,就不会再向老的dex中查找class了,从而实现了类的替换,也就实现了热修复。

那么再说nuwa的生效时间,在我理解只要nuwa加载的时候,需要修复的class文件还没有被davlik虚拟机加载过,就可以实现修复的效果,否则就无法修复,不知道这样理解是否正确。

再说到最近最火的热修复Tinker,下次把Tinker原理研究一波。

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

推荐阅读更多精彩内容