本文基于Android5.0.0_r2分析
Java和Android中的classloader的區(qū)別
Java系統(tǒng)的ClassLoader:
- Bootstrap ClassLoader 最頂層加載類,主要加載核心類庫 rt.jar, resources.jar, charset.jar等
- Extention ClassLoader 擴(kuò)展的類加載器
- AppClassLoader 加載當(dāng)前應(yīng)用的classpath的所有類
普通類的類加載器是AppClassLoader,AppClassLoader的parent是ExtClassLoader,ExtClassLoader沒有parent,BootstrapClassLoader是C++寫的,無法在java代碼中獲取它的引用。JVM初始化sun.misc.Launcher并創(chuàng)建ExtClassLoader和AppClassLoader并將ExtClassLoader設(shè)置為AppClassLader的父加載器。
Java中的ClassLoader使用了雙親委托機(jī)制,一個(gè)類加載器尋找class和resource時(shí),先判斷這個(gè)class是否加載成功,沒有的話不是自己加載而是交給父加載器,然后遞歸下去直到BootstrapClassLoader,如果 BootstrapClassLoader找到了就直接返回,如果沒有找到就一級(jí)一級(jí)返回,最后到達(dá)自身去加載,這就叫做雙親委托。每次加載都是先查找緩存中是否存在,也就是有沒有加載過,沒有的話就到各自加載器負(fù)責(zé)加載的路徑下查找。比如BootstrapClassLoader負(fù)責(zé)的rt.jar classes.jar等。
Android的ClassLoader
不同于Java的ClassLoader,應(yīng)該說不同于JVM,Android不加載單獨(dú)的class文件,而是去加載dex文件。我們知道dex其實(shí)就是很多個(gè)class集合在一起。因?yàn)楫吘筩lass是有結(jié)構(gòu)上的冗余的,而dex文件則消除了這些冗余。

那這時(shí)候JVM的classloader自然就排不上用場了,所以Android有自己的classloader去加載dex。
- PathClassLoader只能加載系統(tǒng)中已經(jīng)安裝過的apk
- DexClassLoader可以加載jar/apk/dex,可以指定加載目錄
至于為什么,我們?nèi)ゴa里面看:
源碼分析
查看源碼可以在http://androidxref.com/5.0.0_r2/網(wǎng)站上看,如果自己down下源碼來的,也可以借助eclipse或者source insight查看。
我們定兩個(gè)我們渴望知道的兩個(gè)問題:
- DexClassLoader跟PathClassLoader的區(qū)別,是怎么體現(xiàn)的。
- findClass是怎么實(shí)現(xiàn)的。
我們帶著問題來看源碼:
DexClassLoader和PathClassLoader
//DexClassLoader
public class DexClassLoader extends BaseDexClassLoader {
public DexClassLoader(String dexPath, String optimizedDirectory,
String libraryPath, ClassLoader parent) {
super(dexPath, new File(optimizedDirectory), libraryPath, parent);
}
}
//PathClassLoader
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);
}
}
上面就是出掉注釋后兩個(gè)類的所有代碼。什么都沒干,而且沒有實(shí)際loadClass方法,而且構(gòu)造方法也只是調(diào)用了super,很明顯邏輯代碼都是在父類BaseDexClassLoader里面實(shí)現(xiàn)的。
其實(shí)這兩個(gè)類的區(qū)別就是構(gòu)造方法參數(shù)數(shù)量的區(qū)別:
- DexClassLoader調(diào)用的是四個(gè)參數(shù)的構(gòu)造方法。
- PathClassLoader分別調(diào)用了兩個(gè)參數(shù)的構(gòu)造方法和三個(gè)參數(shù)的構(gòu)造方法,這倆構(gòu)造方法的區(qū)別就是傳不傳so庫的地址。
謎團(tuán)都在BaseDexClassloader里:
class BaseDexClassLoader extends ClassLoader {
private final DexPathList pathList;
/**
* Constructs an instance. 故意沒有刪這個(gè)注釋,可以看一下這四個(gè)參數(shù)的意思:
*
* @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; may 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 BaseDexClassLoader(String dexPath, File optimizedDirectory, String libraryPath, ClassLoader parent) {
super(parent);
this.pathList = new DexPathList(this, dexPath, libraryPath, optimizedDirectory);
}
@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;
}
...
}
BaseDexClassLoader還是沒干什么具體的邏輯,我們看得出來其實(shí)真正做事情的其實(shí)是這個(gè)叫做DexPathList的類,整個(gè)BaseDexClassLoader其實(shí)是對(duì)DexPathList的功能做了一個(gè)包裝,功能都是通過DexPathList來實(shí)現(xiàn)的。
我們看一下構(gòu)造函數(shù)這四個(gè)參數(shù)的意思:
- dexPath:dex或者包含dex的jar/apk文件路徑,多個(gè)需要用File.pathSeparator分隔開
- optimizedDirectory:odex的被存放的路徑,可以為null,這里就能看出來區(qū)別,PathClassLoader設(shè)置的是null,DexClassLoader設(shè)置的是非null。
- libraryPath:本地庫的文件路徑,多個(gè)需要用File.pathSeparator分隔開
- parent:父classloader
我們的兩個(gè)問題:
- 區(qū)別是有沒有傳optimizedDirectory,但是具體區(qū)別還是在DexPathList的構(gòu)造方法里面,這里看不出來。
- findClass還是調(diào)用了DexPathList的findClass,這里找不到的時(shí)候拋了個(gè)異常。
DexPathList
class DexPathList {
private static final String DEX_SUFFIX = ".dex";
private final ClassLoader definingContext;
/**
* List of dex/resource (class path) elements.
* Should be called pathElements, but the Facebook app uses reflection
* to modify 'dexElements' (http://b/7726934). 看到這段注釋的時(shí)候,真的是直白,直接寫到源碼里。。。
*/
private final Element[] dexElements;
private final File[] nativeLibraryDirectories;
/**
* @param definingContext the context in which any as-yet unresolved classes should be defined
* @param dexPath list of dex/resource path elements, separated by {@code File.pathSeparator}
* @param libraryPath list of native library directory path elements, separated by {@code File.pathSeparator}
* @param optimizedDirectory directory where optimized {@code .dex} files should be found and written to, or {@code null} to use the default system directory for same
*/
public DexPathList(ClassLoader definingContext, String dexPath,
String libraryPath, File optimizedDirectory) {
//definingContext和dexPath不能為空
if (definingContext == null) {
throw new NullPointerException("definingContext == null");
}
if (dexPath == null) {
throw new NullPointerException("dexPath == null");
}
//如果設(shè)置了optimizedDirectory,持續(xù)判空
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>();
//":"分隔開的path
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);
}
...
}
最重要的一句
this.dexElements = makeDexElements(splitDexPath(dexPath), optimizedDirectory, suppressedExceptions);
看splitDexPath這一串調(diào)用
//DexPathList.java
private static ArrayList<File> splitDexPath(String path) {
return splitPaths(path, null, false);
}
//DexPathList.java
private static ArrayList<File> splitPaths(String path1, String path2,
boolean wantDirectories) {
ArrayList<File> result = new ArrayList<File>();
splitAndAdd(path1, wantDirectories, result);
splitAndAdd(path2, wantDirectories, result);
return result;
}
//DexPathList.java
private static void splitAndAdd(String searchPath, boolean directoriesOnly,
ArrayList<File> resultList) {
if (searchPath == null) {
return;
}
for (String path : searchPath.split(":")) {
try {
StructStat sb = Libcore.os.stat(path);
if (!directoriesOnly || S_ISDIR(sb.st_mode)) {
resultList.add(new File(path));
}
} catch (ErrnoException ignored) {
}
}
}
其實(shí)就是把冒號(hào):分隔的dex路徑add到一個(gè)ArrayList返回回來。我們看makeDexElements做了什么,這里也就是PathClassLoader跟DexClassLoader區(qū)分的地方:
private static Element[] makeDexElements(ArrayList<File> files, File optimizedDirectory,
ArrayList<IOException> suppressedExceptions) {
ArrayList<Element> elements = new ArrayList<Element>();
for (File file : files) {
File zip = null;
DexFile dex = null;
String name = file.getName();
if (file.isDirectory()) {
// We support directories for looking up resources.
// This is only useful for running libcore tests. 支持目錄,但只是做test的時(shí)候
elements.add(new Element(file, true, null, null));
} else if (file.isFile()) {
if (name.endsWith(DEX_SUFFIX)) { //路徑下是一個(gè)dex文件
// 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 {
zip = file;
try {
dex = loadDexFile(file, optimizedDirectory);
} catch (IOException suppressed) {
suppressedExceptions.add(suppressed);
}
}
} else {
System.logW("ClassLoader referenced unknown path: " + file);
}
if ((zip != null) || (dex != null)) {
elements.add(new Element(file, false, zip, dex));
}
}
return elements.toArray(new Element[elements.size()]);
}
//PathDexList.java
private static DexFile loadDexFile(File file, File optimizedDirectory) throws IOException {
if (optimizedDirectory == null) {//PathClassLoader的分支
return new DexFile(file);//構(gòu)造一個(gè)DexFile來表示Dex
} else {//DexClassLoader的分支,創(chuàng)建一個(gè)path來緩存dex
String optimizedPath = optimizedPathFor(file, optimizedDirectory);
//這里file.getPath():需要load的dex路徑;optimizedPath:load進(jìn)來的dex要存在哪兒。
return DexFile.loadDex(file.getPath(), optimizedPath, 0);
}
}
//PathDexList.java
private static String optimizedPathFor(File path,
File optimizedDirectory) {
/*
* Get the filename component of the path, and replace the
* suffix with ".dex" if that's not already the suffix.
*
* We don't want to use ".odex", because the build system uses
* that for files that are paired with resource-only jar
* files. If the VM can assume that there's no classes.dex in
* the matching jar, it doesn't need to open the jar to check
* for updated dependencies, providing a slight performance
* boost at startup. The use of ".dex" here matches the use on
* files in /data/dalvik-cache.
*/
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();
}
}
//在optimizedDirectory目錄下創(chuàng)建一個(gè)叫fileName的文件
File result = new File(optimizedDirectory, fileName);
return result.getPath();
}
上面的代碼中我們看到了PathClassLoader跟DexClassLoader的區(qū)分,因?yàn)镻athClassLoader加載的是系統(tǒng)已經(jīng)安裝好了的,所以直接用就好了。
而DexClassLoader加載的是用戶指定的目錄下的dex或者包含dex的jar和apk,所以需要重新load。這里創(chuàng)建了一個(gè)dex,并且用DexFile.loadDex(file.getPath(), optimizedPath, 0);來把dex加載進(jìn)來。
這里我們發(fā)現(xiàn)又多了個(gè)類叫做DexFile,這是存儲(chǔ)了Dex文件一些屬性的類,而loadDex又做了什么我們DexFile.java里面怎么做的。
final class DexFile {
private long mCookie;
private final String mFileName;
private final CloseGuard guard = CloseGuard.get();
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");
}
//指定了name的dex,也就是DexClassLoader需要調(diào)用的地方
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'll 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 + " sourceName=" + sourceName + " outputName=" + outputName);
}
/**
* Open a DEX file, specifying the file in which the optimized DEX
* data should be written. If the optimized form exists and appears
* to be current, it will be used; if not, the VM will attempt to
* regenerate it.
* This is intended for use by applications that wish to download
* and execute DEX files outside the usual application installation
* mechanism. This function should not be called directly by an
* application; instead, use a class loader such as
* dalvik.system.DexClassLoader. //注釋沒刪,這里能看到說這個(gè)方法應(yīng)該只會(huì)被DexClassLoader調(diào)用到。
*
* @param sourcePathName Jar or APK file with "classes.dex". (May expand this to include
* "raw DEX" in the future.) dex文件或者包含dex的jar或者apk
* @param outputPathName File that will hold the optimized form of the DEX data. dex需要存放的路徑
* @param flags Enable optional features. (Currently none defined.)
* @return A new or previously-opened DexFile.
* @throws IOException If unable to open the source or output file.
*/
static public DexFile loadDex(String sourcePathName, String outputPathName,
int flags) throws IOException {
//調(diào)用了上面三個(gè)參數(shù)的構(gòu)造方法
return new DexFile(sourcePathName, outputPathName, flags);
}
...
private static long openDexFile(String sourceName, String outputName, int flags) throws IOException {
// Use absolute paths to enable the use of relative paths when testing on host.
return openDexFileNative(new File(sourceName).getAbsolutePath(),
(outputName == null) ? null : new File(outputName).getAbsolutePath(),
flags);
}
...
private static native long openDexFileNative(String sourceName, String outputName, int flags);
}
我列出了構(gòu)造方法用到的一些方法,我們看到其實(shí)最后是調(diào)用到了一個(gè)native方法得到了一個(gè)long值并存到了一個(gè)叫做mCookie的變量里,這個(gè)操作很眼熟,我之前寫過一篇文章JNI中用long傳遞指針到j(luò)ava,JNI編程里面很多時(shí)候我們會(huì)把在C++中開辟的地址的指針傳到j(luò)ava中存成long,后續(xù)的調(diào)用中我們?cè)倌弥@個(gè)指針去C++中就能定位到我們之前操作的那塊地址。這里是不是也是這樣子的呢?
我們?cè)?a target="_blank">dalvik_system_DexFile.cc找到了這個(gè)方法
怎么找到的?

直接搜就完了,記得如果不知道在哪個(gè)包下面記得右邊選擇select all
//dalvik_system_DexFile.cc
static jlong DexFile_openDexFileNative(JNIEnv* env, jclass, jstring javaSourceName, jstring javaOutputName, jint) {
ScopedUtfChars sourceName(env, javaSourceName);
if (sourceName.c_str() == NULL) {
return 0;
}
NullableScopedUtfChars outputName(env, javaOutputName);
if (env->ExceptionCheck()) {
return 0;
}
ClassLinker* linker = Runtime::Current()->GetClassLinker();
std::unique_ptr<std::vector<const DexFile*>> dex_files(new std::vector<const DexFile*>());
std::vector<std::string> error_msgs;
bool success = linker->OpenDexFilesFromOat(sourceName.c_str(), outputName.c_str(), &error_msgs,
dex_files.get());
if (success || !dex_files->empty()) {
// In the case of non-success, we have not found or could not generate the oat file.
// But we may still have found a dex file that we can use. 返回了dex_files.release()的指針
return static_cast<jlong>(reinterpret_cast<uintptr_t>(dex_files.release()));
} else {
// The vector should be empty after a failed loading attempt.
DCHECK_EQ(0U, dex_files->size());
ScopedObjectAccess soa(env);
CHECK(!error_msgs.empty());
// The most important message is at the end. So set up nesting by going forward, which will
// wrap the existing exception as a cause for the following one.
auto it = error_msgs.begin();
auto itEnd = error_msgs.end();
for ( ; it != itEnd; ++it) {
ThrowWrappedIOException("%s", it->c_str());
}
return 0;
}
}
跟我們想的一樣,就是把std::vector<const DexFile>得指針給返回回來了,也就是說這個(gè)long指向了一個(gè)std::vector<const DexFile>,后面用到mCache的時(shí)候我們確認(rèn)一下。
到這里我們看完了PathClassLoader跟DexClassLoader的構(gòu)造方法。
回到PathDexList.java繼續(xù)看findClass
//PathDexList.java
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;
}
//DexFile.java
public Class loadClassBinaryName(String name, ClassLoader loader, List<Throwable> suppressed) {
return defineClass(name, loader, mCookie, suppressed);
}
private static Class defineClass(String name, ClassLoader loader, long cookie,
List<Throwable> suppressed) {
Class result = null;
try {
result = defineClassNative(name, loader, cookie);
} catch (NoClassDefFoundError e) {
if (suppressed != null) {
suppressed.add(e);
}
} catch (ClassNotFoundException e) {
if (suppressed != null) {
suppressed.add(e);
}
}
return result;
}
private static native Class defineClassNative(String name, ClassLoader loader, long cookie)
throws ClassNotFoundException, NoClassDefFoundError;
我們其實(shí)PathDexList.findClass還是調(diào)用了DexFile方法,而DexFile最終還是調(diào)用了一個(gè)native方法去獲取Class文件,當(dāng)然去獲取的時(shí)候帶上了之前初始化時(shí)候得到的mCache的JNI的指針。我們同樣去dalvik_system_DexFile.cc確認(rèn)一下是不是跟我們想的一樣。
//dalvik_system_DexFile.cc
static jclass DexFile_defineClassNative(JNIEnv* env, jclass, jstring javaName, jobject javaLoader,
jlong cookie) {
std::vector<const DexFile*>* dex_files = toDexFiles(cookie, env);
if (dex_files == NULL) {
VLOG(class_linker) << "Failed to find dex_file";
return NULL;
}
ScopedUtfChars class_name(env, javaName);
if (class_name.c_str() == NULL) {
VLOG(class_linker) << "Failed to find class_name";
return NULL;
}
const std::string descriptor(DotToDescriptor(class_name.c_str()));
for (const DexFile* dex_file : *dex_files) {
const DexFile::ClassDef* dex_class_def = dex_file->FindClassDef(descriptor.c_str());
if (dex_class_def != nullptr) {
ScopedObjectAccess soa(env);
ClassLinker* class_linker = Runtime::Current()->GetClassLinker();
class_linker->RegisterDexFile(*dex_file);
StackHandleScope<1> hs(soa.Self());
Handle<mirror::ClassLoader> class_loader(
hs.NewHandle(soa.Decode<mirror::ClassLoader*>(javaLoader)));
mirror::Class* result = class_linker->DefineClass(descriptor.c_str(), class_loader, *dex_file,
*dex_class_def);
if (result != nullptr) {
VLOG(class_linker) << "DexFile_defineClassNative returning " << result;
return soa.AddLocalReference<jclass>(result);
}
}
}
VLOG(class_linker) << "Failed to find dex_class_def";
return nullptr;
}
static std::vector<const DexFile*>* toDexFiles(jlong dex_file_address, JNIEnv* env) {
std::vector<const DexFile*>* dex_files = reinterpret_cast<std::vector<const DexFile*>*>(
static_cast<uintptr_t>(dex_file_address));
if (UNLIKELY(dex_files == nullptr)) {
ScopedObjectAccess soa(env);
ThrowNullPointerException(NULL, "dex_file == null");
}
return dex_files;
}
跟我們想的一樣,傳進(jìn)來的mCache指針被轉(zhuǎn)成了一個(gè)std::vector<const DexFile * >,然后得到遍歷vector得到一個(gè)個(gè)的DexFile,然后用這個(gè)ClassLinker玩意兒找到class,然后包裝成jclass返回回來。C的代碼不了解也沒看過,有興趣的可以繼續(xù)探索。然后DexFile大致長這樣,可以根據(jù)數(shù)據(jù)結(jié)構(gòu)看出來Dex文件的大致結(jié)構(gòu)。

好像很長了,總結(jié)一下。
- Java中使用雙親委托加載class文件,有AppClassLoader, ExtClassLoader, BootstrapClassLoader三個(gè)系統(tǒng)的classloader,另外還可以繼承classloader實(shí)現(xiàn)自定義的ClassLoader
- Android有PathClassLoader和DexClassLoader,PathClassLoader加載系統(tǒng)中已經(jīng)安裝過的apk,DexClassLoader可以加載自定義目錄。
- DexClassLoader和PathClassLoader,其實(shí)包括BasePathClassLoader都沒什么邏輯代碼,都是依靠DexPathList實(shí)現(xiàn)加載操作
- DexClassLoader和PathClassLoader的區(qū)別是調(diào)用BasePathClassLoader構(gòu)造方法的時(shí)候傳沒傳optimizedDirectory,這是他倆的特性決定的:PathClassLoader加載的是已經(jīng)加載過的dex,DexClassLoader則是指定目錄的dex,所以DexClassLoader需要做的要更多一步,就是把dex給load進(jìn)來。
- 加載Dex和加載Class都是在native做的,其中會(huì)把native開辟的空間傳到j(luò)ava存起來,后邊用的時(shí)候再帶過去。