Skip to content

Android(4): ClassLoader与MultiDex分包

clarkehe edited this page Aug 15, 2016 · 29 revisions

1. 什么是ClassLoader

ClassLoader是虚拟机用来加载要执行的类。虚拟机在运行jar或dex中的代码时,可能会遇到未加载的类(一个类的相关代码只有从dex文件加载到虚拟机才能执行),这时就需要ClassLoader来加载。
在我们平时开发过程似乎是没有感受到ClassLoader的存在,其实它是无处不在的。每一个类都会有一个加载它的ClassLoader,看下面的代码:

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        final TextView view = new TextView(this);
        setContentView(view);

        String classLoaderStr = "Current Activity ClassLoader:";
        ClassLoader classLoader = getClassLoader();

        int i = 1;
        if (classLoader != null){
            Log.i(TAG, "[onCreate] classLoader " + i + " : " + classLoader.toString());

            classLoaderStr += "\r\n\r\n";
            classLoaderStr += (i + ". ");
            classLoaderStr += classLoader.toString();

            while (classLoader.getParent()!=null){
                classLoader = classLoader.getParent();

                i += 1;
                Log.i(TAG,"[onCreate] classLoader " + i + " : " + classLoader.toString());

                classLoaderStr += "\r\n\r\n";
                classLoaderStr += (i + ". ");
                classLoaderStr += classLoader.toString();
            }
        }
        view.setText(classLoaderStr);
    }
}

上面代码获取了当前Activity的ClassLoader及其父ClassLoader,运行结果如下:

classLoader 1 : dalvik.system.PathClassLoader[DexPathList[[zip file "/data/app/com.sample-2.apk"],nativeLibraryDirectories=[/data/app-lib/com.sample-2, /vendor/lib, /system/lib]]]       
classLoader 2 : java.lang.BootClassLoader@8090c88         

从例子我们可以知道,每一个类都有一个PathClassLoader和一个BootClassLoader,这两个类都是系统在运行APP时,自动帮助我们创建的。PathClassLoader有两个关键的属性:DexPathList和nativeLibraryDirectories。DexPathList保存了class文件的加载路径,一般都是从安装的APK文件中加载;nativeLibraryDirectories保存了so文件的加载路径。

2. ClassLoader的实现

Android实现了一整套的ClassLoader机制,除了上面提到的PathClassLoader,还有其他的ClassLoader实现。不同ClassLoader又有什么区别?

class loader

除了系统的BootClassLoader,我们用到的主是要DexClassLoader和PathClassLoader。他们都是从BaseCassLoder继承,在功能与使用上会有一点区别:

我们安装到手机中apk包中的dex文件,都是通过PathClassLoader来加载的。安装与未安装在实现上有什么区别呢?

// 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参数,这个参数有什么用。继续看源吗。

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

optimizedDirectory会用于创建一个DexPathList。

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

源码,当optimizedDirectory为空时,则直接创建一个DexFile;不为空,则加载DexFile,并在optimizedDirectory目录生成DEX文件的缓存。

原来在安装apk或手动加载dex文件时,系统会对dex文件进行处理(dexopt),生成一个相应的缓存文件。加载class时,是从缓存文件中加载的。生成缓存的过程相对耗时。生成的缓存文件也比原dex文件要大。

安装apk时,apk文件会原样copy到/data/app/包名目录;apk包中的dex文件,会在data/dalvik-cache目录生成缓存:

root@hammerhead:/data/app/ # ls -al | grep "xxx"  
-rw-r--r-- system   system   16427213 2016-06-08 20:20 com.xxx.xxx-1.apk
root@hammerhead:/data/dalvik-cache/# ls -al | grep "xxx"                 
-rw-r--r-- system   all_a74  9822872 2016-06-08 20:20 data@[email protected]@classes.dex

归纳成一句话: 加载已安装apk中的dex文件,用PathClassLoader, 其他它用DexClassLoder。

3. 分包问题及解决

分包本质问题是dex文件中的方法数据不能超过65535,这是dex文件在设计上的缺陷。当在编译生成dex文件时,如果方法数超过了65535会编译失败。

Error Code:2
Output:
UNEXPECTED TOP-LEVEL EXCEPTION:
com.android.dex.DexIndexOverflowException: Cannot merge new index 65620 into a non-jumbo instruction!

为了编译通过,要在gradle编译脚本中,打开“multiDexEnabled true”开关。最终会生成classes.dex、classes1.dex等多个dex文件,会一起打包到apk中。有多个dex时,类会包含在那个dex文件是随机的(默认的情况,在manifest中注册的组件类会放在主dex),据了解是有方法可以控制的。

这样分包的问题似乎就已经解决了。实际问题只解决了一半。前面说过,在安装apk时,会对apk包中dex文件生成缓存。如果apk中有多个dex文件呢?在5.0以下(5.0及以上系统使用的是ART虚拟机,在安装时对APK的处理有所不同)的系统中,只会对classes.dex文件生成缓存,其他的classesN.dex等则不会生成缓存。如果这种场景不处理,就会导致在classesN.dex等dex文件中的类加载不到。

为了解决这个问题,google给出support包:android.support.multidex.MultiDex。 在app启动初始化回调中通过调用 MultiDex.install(this)来安装除classes.dex外的dex文件,就是对这些dex文件在程序运行时,再生成缓存。这也在一定程度上影响了app的启动速度。

4. 分包与ClassLoader

上面说过google为分包处理提供了support包。但具体实现原理是怎样的呢?还是和classLoader有关:通过反射修改PathClassLoader中DexPathList信息,将classes1.dex等的路径加到DexPathList中。我们打印有分包app中一个Activity的ClassLoader的信息,就可以直观看到。

classLoader 1 : dalvik.system.PathClassLoader[DexPathList[[zip file "/data/app/com.xxx.xxx-1.apk", zip file "/data/data/com.xxx.xxx/code_cache/secondary-dexes/com.xxx.xxx-1.apk.classes2.zip"],nativeLibraryDirectories=[/data/app-lib/com.xx-1, /vendor/lib, /system/lib]]]
classLoader 2 : java.lang.BootClassLoader@4159efd0

对比之前的的日志,可以发现zip file多了一个,除apk的安装路径外,多了一个"/data/data/com.xxx.xxx/code_cache/secondary-dexes/com.xxx.xxx-1.apk.classes2.zip"。

通过查看MultiDex的源码,分析大概处理过程如下:

  • 在app的data目录下,建立文件夹./code_cache/secondary-dexes。
  • 遍历apk包中的classes1.dex、classes2.dex把它们导出来,并打包成一个zip文件,文件名格式如下:com.xxx.xxx-1.apk.classes2.zip。
  • 将打包生成的zip文件的全路径,通过反射的方式添加到当前PathClassLoader@DexPathList@dexElements中。
  • 在添加DexPath时,zip文件会被加载,经历dexopt处理生成缓存,缓存目录也是./code_cache/secondary-dexes。
  • 这样classLoader在加载类时,通过遍历DexPathList,就能找到所有的dex文件。
    private static final class V14 {
        private V14() {
        }

        private static void install(ClassLoader loader, List<File> additionalClassPathEntries, File optimizedDirectory) throws IllegalArgumentException, IllegalAccessException, NoSuchFieldException, InvocationTargetException, NoSuchMethodException {
            Field pathListField = MultiDex.findField(loader, "pathList");
            Object dexPathList = pathListField.get(loader);
            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}));
        }
    }
root@msm8226:/data/data/com.xxx.xxx/code_cache/secondary-dexes # ls -al                                                                        
-rw-r--r-- u0_a83   u0_a83    1462880 2014-01-05 03:12 com.xxx.xxx-1.apk.classes2.dex
-rw------- u0_a83   u0_a83     573047 2014-01-05 03:12 com.xxx.xxx-1.apk.classes2.zip

5. ClassLoader的机制

ClassLoader的核心工作机制体现在其公开方法上:loadClass。先看下loadClass的源码

    /**
     * Loads the class with the specified name, optionally linking it after
     * loading. The following steps are performed:
     * <ol>
     * <li> Call {@link #findLoadedClass(String)} to determine if the requested
     * class has already been loaded.</li>
     * <li>If the class has not yet been loaded: Invoke this method on the
     * parent class loader.</li>
     * <li>If the class has still not been loaded: Call
     * {@link #findClass(String)} to find the class.</li>
     * </ol>
     * <p>
     * <strong>Note:</strong> In the Android reference implementation, the
     * {@code resolve} parameter is ignored; classes are never linked.
     * </p>
     *
     * @return the {@code Class} object.
     * @param className
     *            the name of the class to look for.
     * @param resolve
     *            Indicates if the class should be resolved after loading. This
     *            parameter is ignored on the Android reference implementation;
     *            classes are not resolved.
     * @throws ClassNotFoundException
     *             if the class can not be found.
     */
    protected Class<?> loadClass(String className, boolean resolve) throws ClassNotFoundException {
        Class<?> clazz = findLoadedClass(className);

        if (clazz == null) {
            try {
                clazz = parent.loadClass(className, false);
            } catch (ClassNotFoundException e) {
                // Don't want to see this.
            }

            if (clazz == null) {
                clazz = findClass(className);
            }
        }

        return clazz;
    }

从源码中我们也可以看出,loadClass方法在加载一个类的实例的时候:

  • 会先查询当前ClassLoader实例是否加载过此类,有就返回;
  • 如果没有。查询Parent是否已经加载过此类,如果已经加载过,就直接返回Parent加载的类;
  • 如果继承路线上的ClassLoader都没有加载,才由Child执行类的加载工作;

这样做有个明显的特点,如果一个类被位于树根的ClassLoader加载过,那么在以后整个系统的生命周期内,这个类永远不会被重新加载。

如果类没有被加载过,父classLoader也没有,则要从当前classLoader中加载,就会调用findClass方法:

@Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        Class clazz = pathList.findClass(name);
        if (clazz == null) {
            throw new ClassNotFoundException(name);
        }
        return clazz;
    }

会调用DexPathList中的findClass方法:

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

遍历所有的DexFile,直到找到类为止。最终解析Dex文件,加载类,都是VM在Native层完成的。

曾经想过一个问题,类加载的越多,是不是占用的内存也会多,能不能把加载的类动态卸载掉。现在看,没有卸载类的接口,或许VM内部已经做了相关优化。

6. 自定义ClassLoader

参考资料:
Android动态加载基础 ClassLoader工作机制
android 安装目录介绍

Clone this wiki locally