-
Notifications
You must be signed in to change notification settings - Fork 42
Android(4): ClassLoader与MultiDex分包
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文件的加载路径。
Android实现了一整套的ClassLoader机制,除了上面提到的PathClassLoader,还有其他的ClassLoader实现。不同ClassLoader又有什么区别?
除了系统的BootClassLoader,我们用到的主是要DexClassLoader和PathClassLoader。他们都是从BaseCassLoder继承,在功能与使用上会有一点区别:
- DexClassLoader可以加载jar/apk/dex,可以从SD卡中加载未安装的apk;
- PathClassLoader只能加载系统中已经安装过的apk;
我们安装到手机中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。
分包本质问题是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的启动速度。
上面说过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
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内部已经做了相关优化。