上篇文章讲到了apk的分包,通过multidex构建出包含多个dex文件的apk,从而解决65536的方法数限制问题《Android Dex分包》。
在dalvik虚拟机上,应用启动时只会加载主dex文件,而从dex需要我们手动去加载,那么问题来了,如何手动加载一个dex文件?前面也提到了,使用DexClassLoader和PathClassLoader。
DexClassLoader和PathClassLoader
android加载dex、jar、apk主要是通过DexClassLoader或者PathClassLoader来实现
下面先看一下DexClassLoader的实现
/**
* A class loader that loads classes from {@code .jar} and {@code .apk} files
* containing a {@code classes.dex} entry. This can be used to execute code not
* installed as part of an application.
*
* <p>This class loader requires an application-private, writable directory to
* cache optimized classes. Use {@code Context.getDir(String, int)} to create
* such a directory: <pre> {@code
* File dexOutputDir = context.getDir("dex", 0);
* }</pre>
*
* <p><strong>Do not cache optimized classes on external storage.</strong>
* External storage does not provide access controls necessary to protect your
* application from code injection attacks.
*/
public class DexClassLoader extends BaseDexClassLoader {
/**
* Creates a {@code DexClassLoader} that finds interpreted and native
* code. Interpreted classes are found in a set of DEX files contained
* in Jar or APK files.
*
* <p>The path lists are separated using the character specified by the
* {@code path.separator} system property, which defaults to {@code :}.
*
* @param dexPath the list of jar/apk files containing classes and
* resources, delimited by {@code File.pathSeparator}, which
* defaults to {@code ":"} on Android
* @param optimizedDirectory directory where optimized dex files
* should be written; must not be {@code null}
* @param libraryPath the list of directories containing native
* libraries, delimited by {@code File.pathSeparator}; may be
* {@code null}
* @param parent the parent class loader
*/
public DexClassLoader(String dexPath, String optimizedDirectory,
String libraryPath, ClassLoader parent) {
super(dexPath, new File(optimizedDirectory), libraryPath, parent);
}
}
代码很简单,只有一个构造方法,调用了父类的构造方法,
参数dexPath为dex、jar、apk文件的路径,多个路径之间用:分隔
optimizedDirectory: dex文件首次加载时会进行dexopt操作,optimizedDirectory即为优化后的odex文件的存放目录,不允许为空,官方推荐使用应用私有目录来缓存优化后的dex文件,dexOutputDir = context.getDir("dex", 0);
libraryPath:动态库的路径,可以为空
parent:ClassLoader类型的参数,当前类加载器的父加载器
再来看看PathClassLoader的源码实现
/**
* Provides a simple {@link ClassLoader} implementation that operates on a list
* of files and directories in the local file system, but does not attempt to
* load classes from the network. Android uses this class for its system class
* loader and for its application class loader(s).
*/
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);
}
}
可以看到android系统采用PathClassLoader作为其系统加载器以及应用加载器.PathClassLoader 和DexClassLoader的区别就在于optimizedDirectory参数是否为空,关于optimizedDirectory的作用,接着往下看.
DexClassLoader、PathClassLoader都是继承自BaseDexClassLoader,而BaseDexClassLoader又继承自ClassLoader
/**
* Base class for common functionality between various dex-based
* {@link ClassLoader} implementations.
*/
public class BaseDexClassLoader extends ClassLoader {
private final DexPathList pathList;
public BaseDexClassLoader(String dexPath, File optimizedDirectory,
String libraryPath, ClassLoader parent) {
super(parent);
this.pathList = new DexPathList(this, dexPath, libraryPath, optimizedDirectory);
}
//省略其他代码...
}
BaseDexClassLoader继承自ClassLoader,构造方法中先调用父类ClassLoader的构造方法,然后初始化了DexPathList对象,再来看看DexPathList的构造方法
public DexPathList(ClassLoader definingContext, String dexPath,
String libraryPath, File optimizedDirectory) {
if (definingContext == null) {
throw new NullPointerException("definingContext == null");
}
if (dexPath == null) {
throw new NullPointerException("dexPath == null");
}
if (optimizedDirectory != null) {
if (!optimizedDirectory.exists()) {
throw new IllegalArgumentException(
"optimizedDirectory doesn't exist: "
+ optimizedDirectory);
}
if (!(optimizedDirectory.canRead()
&& optimizedDirectory.canWrite())) {
throw new IllegalArgumentException(
"optimizedDirectory not readable/writable: "
+ optimizedDirectory);
}
}
this.definingContext = definingContext;
ArrayList<IOException> suppressedExceptions = new ArrayList<IOException>();
this.dexElements = makeDexElements(splitDexPath(dexPath), optimizedDirectory,
suppressedExceptions);
if (suppressedExceptions.size() > 0) {
this.dexElementsSuppressedExceptions =
suppressedExceptions.toArray(new IOException[suppressedExceptions.size()]);
} else {
dexElementsSuppressedExceptions = null;
}
this.nativeLibraryDirectories = splitLibraryPath(libraryPath);
}
DexPathList的构造方法先对传入的参数进行校验,然后调用makeDexElements解析出dex相关参数,并保存到dexElements成员变量中,再来看makeDexElements方法
/**
* Makes an array of dex/resource path elements, one per element of
* the given array.
*/
private static Element[] makeDexElements(ArrayList<File> files, File optimizedDirectory,
ArrayList<IOException> suppressedExceptions) {
ArrayList<Element> elements = new ArrayList<Element>();
/*
* Open all files and load the (direct or contained) dex files
* up front.
*/
for (File file : files) {
File zip = null;
DexFile dex = null;
String name = file.getName();
if (name.endsWith(DEX_SUFFIX)) {
// Raw dex file (not inside a zip/jar).
try {
dex = loadDexFile(file, optimizedDirectory);
} catch (IOException ex) {
System.logE("Unable to load dex file: " + file, ex);
}
} else if (name.endsWith(APK_SUFFIX) || name.endsWith(JAR_SUFFIX)
|| name.endsWith(ZIP_SUFFIX)) {
zip = file;
try {
dex = loadDexFile(file, optimizedDirectory);
} catch (IOException suppressed) {
/*
* IOException might get thrown "legitimately" by the DexFile constructor if the
* zip file turns out to be resource-only (that is, no classes.dex file in it).
* Let dex == null and hang on to the exception to add to the tea-leaves for
* when findClass returns null.
*/
suppressedExceptions.add(suppressed);
}
} else if (file.isDirectory()) {
// We support directories for looking up resources.
// This is only useful for running libcore tests.
elements.add(new Element(file, true, null, null));
} else {
System.logW("Unknown file type for: " + file);
}
if ((zip != null) || (dex != null)) {
elements.add(new Element(file, false, zip, dex));
}
}
return elements.toArray(new Element[elements.size()]);
}
files为dex文件的file对象list,判断是dex文件之后调用loadDexFile方法加载dex文件,返回DexFile对象。
/**
* Constructs a {@code DexFile} instance, as appropriate depending
* on whether {@code optimizedDirectory} is {@code null}.
*/
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);
}
}
该方法对optimizedDirectory参数有区分处理,即DexClassLoader和PathClassLoader的区别就在这,若optimizedDirectory为空(采用PathClassLoader),直接返回DexdFile对象,若不为空(采用DexClassLoader),则先调用optimizedPathFor方法获取dex文件优化后存放的目录,如果不是dex文件则将后缀替换为 .dex结尾,最后又调用了DexFile.loadDex静态方法返回了DexFile对象。
/**
* 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();
}
static public DexFile loadDex(String sourcePathName, String outputPathName,
int flags) throws IOException {
/*
* TODO: we may want to cache previously-opened DexFile objects.
* The cache would be synchronized with close(). This would help
* us avoid mapping the same DEX more than once when an app
* decided to open it multiple times. In practice this may not
* be a real issue.
*/
return new DexFile(sourcePathName, outputPathName, flags);
}
可以看到DexFile.loadDex方法直接调用了DexFile的构造方法
//PathClassLoader调用
public DexFile(File file) throws IOException {
this(file.getPath());
}
public DexFile(String fileName) throws IOException {
mCookie = openDexFile(fileName, null, 0);
mFileName = fileName;
guard.open("close");
//System.out.println("DEX FILE cookie is " + mCookie);
}
//DexClassLoader调用
private DexFile(String sourceName, String outputName, int flags) throws IOException {
if (outputName != null) {
try {
String parent = new File(outputName).getParent();
if (Libcore.os.getuid() != Libcore.os.stat(parent).st_uid) {
throw new IllegalArgumentException("Optimized data directory " + parent
+ " is not owned by the current user. Shared storage cannot protect"
+ " your application from code injection attacks.");
}
} catch (ErrnoException ignored) {
// assume we will fail with a more contextual error later
}
}
mCookie = openDexFile(sourceName, outputName, flags);
mFileName = sourceName;
guard.open("close");
//System.out.println("DEX FILE cookie is " + mCookie);
}
所以BaseDexClassLoader和PathClassLoader最终都是调用了openDexFile方法,唯一的区别就是outputName是否为空,即优化后dex保存的路径,
/*
* Open a DEX file. The value returned is a magic VM cookie. On
* failure, an IOException is thrown.
*/
private static int openDexFile(String sourceName, String outputName,
int flags) throws IOException {
return openDexFileNative(new File(sourceName).getCanonicalPath(),
(outputName == null) ? null : new File(outputName).getCanonicalPath(),
flags);
}
native private static int openDexFileNative(String sourceName, String outputName,
int flags) throws IOException;
在native方法中对其进行了判断,如果outputName为空,则自动生成一个缓存目录,即/data/dalvik-cache/xxx@classes.dex。所以DexClassLoader与PathClassLoader的本质区别,就是DexClassLoader可以指定odex的路径,而PathClassLoader则采用系统默认的缓存路径。
所以一般PathDexClassLoader只能加载已安装的apk的dex,而DexClassLoader则可以加载指定路径的apk、dex和jar,也可以从sd卡中进行加载。
openDexFileNative代码中主要是对dex文件进行了优化操作,并将优将优化后得dex文件(odex文件)通过mmap映射到内存中。
其中的细节可以参考《DexClassLoader和PathClassLoader加载Dex流程》一文
类的加载
上述我们得到DexClassLoader或者PathClassLoader对象后,就可以调用其loadClass方法来动态加载某个类
DexClassLoader、PathClassLoader以及BaseDexClassLoader都没有实现这个方法,接着再去其父类ClassLoader中找,
/**
* 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) {
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;
}
首先调用了findLoadedClass查找当前虚拟机是否已经加载过该类,是则直接返回该class,如果未加载过,则调用父加载器的loadClass方法,
这里采用了java的双亲委派模型,即当一个加载器被请求加载某个类时,它首先委托自己的父加载器去加载,一直向上查找,若顶级加载器(优先)或父类加载器能加载,则返回这个类所对应的Class对象,若不能加载,则最后再由请求发起者去加载该类。这种方式的优点就是能够保证类的加载按照一定的规则次序进行,越是基础的类,越是被上层的类加载器进行加载,从而保证程序的安全性。
/**
* Returns the class with the specified name if it has already been loaded
* by the VM or {@code null} if it has not yet been loaded.
*
* @param className
* the name of the class to look for.
* @return the {@code Class} object or {@code null} if the requested class
* has not been loaded.
*/
protected final Class<?> findLoadedClass(String className) {
ClassLoader loader;
if (this == BootClassLoader.getInstance())
loader = null;
else
loader = this;
return VMClassLoader.findLoadedClass(loader, className);
}
上述由于该类还未加载,所以findLoadedClass会返回null,所以会调用parent.loadClass,而DexClassLoader在使用时一般采用默认的类加载器作为其父类加载器
DexClassLoader dexClassLoader = new DexClassLoader(dexPath, getDir("dex", 0).getAbsolutePath(), dexPath, getClassLoader());
即直接调用Context的getClassLoader方法,该方法为抽象方法,实现是在ContextImpl中
//ContextImpl.java
@Override
public ClassLoader getClassLoader() {
return mPackageInfo != null ?
mPackageInfo.getClassLoader() : ClassLoader.getSystemClassLoader();
}
//ClassLoader.java
/**
* Returns the system class loader. This is the parent for new
* {@code ClassLoader} instances and is typically the class loader used to
* start the application.
*/
public static ClassLoader getSystemClassLoader() {
return SystemClassLoader.loader;
}
//SystemClassLoader为ClassLoader的内部类
static private class SystemClassLoader {
public static ClassLoader loader = ClassLoader.createSystemClassLoader();
}
/**
* Create the system class loader. Note this is NOT the bootstrap class
* loader (which is managed by the VM). We use a null value for the parent
* to indicate that the bootstrap loader is our parent.
*/
private static ClassLoader createSystemClassLoader() {
String classPath = System.getProperty("java.class.path", ".");
//最终返回了PathClassLoader作为系统加载器SystemClassLoader,而其父类为根加载器BootClassLoader
return new PathClassLoader(classPath, BootClassLoader.getInstance());
}
所以ClassLoader的loadClass最终会调用根加载器BootClassLoader的loadClass方法,BootClassLoader也是ClassLoader的内部类,是android平台上所有ClassLoader的parent,其loadClass也是先调用findLoadedClass, 这里未加载过直接返回null,根加载器已经是顶级加载器,所以这里直接调用了findClass方法
@Override
protected Class<?> loadClass(String className, boolean resolve)
throws ClassNotFoundException {
Class<?> clazz = findLoadedClass(className);
if (clazz == null) {
clazz = findClass(className);
}
return clazz;
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
return Class.classForName(name, false, null);
}
findClass方法也是返回Class.classForName,这里第三个参数为null,采用的是根加载器,而根加载器是用来加载java核心类,无法加载用户定义的类,所以这里返回为空
所以又回到一开始ClassLoader的loadClass方法,调用findClass方法,该方法由其子类覆写,即BaseDexClassLoader中的findClass方法
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
List<Throwable> suppressedExceptions = new ArrayList<Throwable>();
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;
}
可以看到findClass直接调用pathList的findClass方法,如果为空,抛出ClassNotFoundExceptioin异常,如果不为空,则直接返回该Class
pathList即BaseDexClassLoader中的DexPathList成员变量,其中保存了dexFile的Elements集合,
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;
}
DexPathList的findClass遍历dexElements对象,并调用dexFile的loadClassBinaryName native方法来加载Class.
所以之前在dex分包的时候,我们通过PathClassLoader获取已加载的保存在pathList中的dex信息,然后利用DexClassLoadder加载我们指定的从dex文件,将dex信息合并到pathList的dexElements中,从而在app运行的时候能够将所有的dex中的类加载到内存中。
参考文章
DexClassLoader源码
《DexClassLoader和PathClassLoader加载Dex流程》
《Android动态加载——DexClassloader分析》
http://androidxref.com/4.4.2_r1/xref/libcore/dalvik/src/main/java/dalvik/system/DexClassLoader.java