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 files,
        File optimizedDirectory) {
    ArrayList elements = new ArrayList();
    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 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 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原理研究一波。

你可能感兴趣的:(Android 动态加载原理)