最近在搞android插件化贮配,其中最核心的一點(diǎn)就是如何讓插件apk跑起來(lái)谍倦,其實(shí)就利用動(dòng)態(tài)加載,將插件中的類(lèi)加載到host中泪勒。之前也看過(guò)android類(lèi)的加載機(jī)制昼蛀,感覺(jué)對(duì)于整個(gè)過(guò)程大致看懂了,然后就開(kāi)始整插件化圆存,結(jié)果碰到問(wèn)題了又感覺(jué)一知半解的叼旋,很是不爽,索性放一放插件化沦辙,先把a(bǔ)ndroid的類(lèi)加載機(jī)制看個(gè)明白夫植。這里給自己總結(jié)下,加深印象的同時(shí)保留筆記油讯。
首先需要知道的是android中兩個(gè)主要的classloader详民,PathClassLoader和DexClassLoader,它們都繼承自BaseDexClassLoader陌兑,這兩個(gè)類(lèi)有什么區(qū)別呢沈跨?其實(shí)看一下它們的源碼注釋就一目了然了。
因?yàn)榇a很少兔综,約等于沒(méi)有饿凛,這里直接貼出它們的源碼,其實(shí)主要是注釋?zhuān)?/p>
PathClassLoader
package dalvik.system;
/**
* 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 {
/**
* Creates a {@code PathClassLoader} that operates on a given list of files
* and directories. This method is equivalent to calling
* {@link #PathClassLoader(String, String, ClassLoader)} with a
* {@code null} value for the second argument (see description there).
*
* @param dexPath the list of jar/apk files containing classes and
* resources, delimited by {@code File.pathSeparator}, which
* defaults to {@code ":"} on Android
* @param parent the parent class loader
*/
public PathClassLoader(String dexPath, ClassLoader parent) {
super(dexPath, null, null, parent);
}
/**
* Creates a {@code PathClassLoader} that operates on two given
* lists of files and directories. The entries of the first list
* should be one of the following:
*
* <ul>
* <li>JAR/ZIP/APK files, possibly containing a "classes.dex" file as
* well as arbitrary resources.
* <li>Raw ".dex" files (not inside a zip file).
* </ul>
*
* The entries of the second list should be directories containing
* native library files.
*
* @param dexPath the list of jar/apk files containing classes and
* resources, delimited by {@code File.pathSeparator}, which
* defaults to {@code ":"} on Android
* @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 PathClassLoader(String dexPath, String libraryPath,
ClassLoader parent) {
super(dexPath, null, libraryPath, parent);
}
}
由注釋看可以發(fā)現(xiàn)PathClassLoader被用來(lái)加載本地文件系統(tǒng)上的文件或目錄软驰,但不能從網(wǎng)絡(luò)上加載涧窒,關(guān)鍵是它被用來(lái)加載系統(tǒng)類(lèi)和我們的應(yīng)用程序,這也是為什么它的兩個(gè)構(gòu)造函數(shù)中調(diào)用父類(lèi)構(gòu)造器的時(shí)候第二個(gè)參數(shù)傳null锭亏,具體的參數(shù)意義請(qǐng)看接下來(lái)DexClassLoader的注釋纠吴。
DexClassLoader
package dalvik.system;
import java.io.File;
/**
* 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);
}
}
DexClassLoader用來(lái)加載jar、apk慧瘤,其實(shí)還包括zip文件或者直接加載dex文件呜象,它可以被用來(lái)執(zhí)行未安裝的代碼或者未被應(yīng)用加載過(guò)的代碼膳凝。這里也寫(xiě)出了它需要的四個(gè)參數(shù)的意思
- dexPath:需要被加載的文件地址,可以多個(gè)恭陡,用File.pathSeparator分割
- optimizedDirectory:dex文件被加載后會(huì)被編譯器優(yōu)化蹬音,優(yōu)化之后的dex存放路徑,不可以為null休玩。注意著淆,注釋中也提到需要一個(gè)應(yīng)用私有的可寫(xiě)的一個(gè)路徑,以防止應(yīng)用被注入攻擊拴疤,并且給出了例子 File dexOutputDir = context.getDir("dex", 0);
- libraryPath:包含libraries的目錄列表永部,plugin中有so文件,需要將so拷貝到sd卡上呐矾,然后把so所在的目錄當(dāng)參數(shù)傳入苔埋,同樣用File.pathSeparator分割,如果沒(méi)有則傳null就行了,會(huì)自動(dòng)加上系統(tǒng)so庫(kù)的存放目錄
- parent:父類(lèi)構(gòu)造器
這里著重看一下第二個(gè)參數(shù)蜒犯,之前說(shuō)過(guò)PathClassLoader中調(diào)用父類(lèi)構(gòu)造器的時(shí)候這個(gè)參數(shù)穿了null组橄,因?yàn)榧虞dapp應(yīng)用的時(shí)候我們的apk已經(jīng)被安裝到本地文件系統(tǒng)上了,其內(nèi)部的dex已經(jīng)被提取并且執(zhí)行過(guò)優(yōu)化了罚随,優(yōu)化之后放在系統(tǒng)目錄/data/dalvik-cache下玉工。
接下來(lái)我們看一下類(lèi)的具體加載過(guò)程
首先看一段如何使用類(lèi)加載器加載的調(diào)用代碼:
try {
File file = view.getActivity().getDir("dex",0);
String optimizedDirectory = file.getAbsolutePath();
DexClassLoader loader = new DexClassLoader("需要被加載的dex文件所在的路徑",optimizedDirectory,null,context.getClassLoader());
loader.loadClass("需要加載的類(lèi)的完全限定名");
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
這里我們就用了自定義了一個(gè)DexClassLoaderLoader,并且調(diào)用了它的loadClass方法淘菩,這樣一個(gè)需要被使用的類(lèi)就被我們加載進(jìn)來(lái)了遵班,接下去就可以正常使用這個(gè)類(lèi)了,具體怎么使用我就不多說(shuō)了潮改,我們還是來(lái)研究研究這個(gè)類(lèi)是怎么被加載進(jìn)來(lái)的吧~
可以看到new DexClassLoader的時(shí)候我們用了4個(gè)參數(shù),參數(shù)意義上面已經(jīng)講過(guò)了翰萨,從上面的源碼中可以看到DexClassLoader的構(gòu)造器中直接調(diào)用了父類(lèi)的構(gòu)造器趾疚,只是將optimizedDirectory路徑封裝成一個(gè)File以蕴,具體這些參數(shù)是如何被使用的呢,我們往下看丛肮。
BaseDexClassLoader類(lèi)的構(gòu)造器
/**
* Constructs an instance.
*
* @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.originalPath = dexPath;
this.pathList =
new DexPathList(this, dexPath, libraryPath, optimizedDirectory);
}
首先也是調(diào)用了父類(lèi)的構(gòu)造器,但這里只將parent傳給父類(lèi)宝与,即ClassLoader焚廊,ClassLoader中做的也很很簡(jiǎn)單冶匹,它內(nèi)部有個(gè)parent屬性,正好保存?zhèn)鬟M(jìn)來(lái)的參數(shù)parent,這里可以稍微看一下第二個(gè)參數(shù)的注釋?zhuān)詈笠痪湔f(shuō)到可以為null咆瘟,而是否為null又剛好是PathClassLoader和DexClassLoader的區(qū)別嚼隘,那是否為null最終又意味著什么呢?賣(mài)個(gè)關(guān)子先~
protected ClassLoader(ClassLoader parentLoader) {
this(parentLoader, false);
}
ClassLoader(ClassLoader parentLoader, boolean nullAllowed) {
if (parentLoader == null && !nullAllowed) {
throw new NullPointerException("parentLoader == null && !nullAllowed");
}
parent = parentLoader;
}
接下去BaseDexClassLoader給originalPath 和 pathList賦了值袒餐,originalPath就是我們傳進(jìn)入的dex文件路徑飞蛹,pathList 是一個(gè)new 出來(lái)的DexPathList對(duì)象,這個(gè)DexPathList又是何方神圣灸眼?且聽(tīng)慢慢道來(lái)卧檐。
/**
* Constructs an instance.
*
* @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) {
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;
this.dexElements =
makeDexElements(splitDexPath(dexPath), optimizedDirectory);
this.nativeLibraryDirectories = splitLibraryPath(libraryPath);
}
別的先不說(shuō),先看注釋焰宣。第四個(gè)參數(shù)中說(shuō)到如果optimizedDirectory 為null則使用系統(tǒng)默認(rèn)路徑代替霉囚,這個(gè)默認(rèn)路徑也就是/data/dalvik-cache/目錄,這個(gè)一般情況下是沒(méi)有權(quán)限去訪問(wèn)的匕积,所以這也就解釋了為什么我們只能用DexClassLoader去加載類(lèi)而不用PathClassLoader盈罐。
然后接著看代碼,顯然闸天,前面三個(gè)if判斷都是用來(lái)驗(yàn)證參數(shù)的合法性的暖呕,之后同樣只是做了三個(gè)賦值操作,第一個(gè)就不說(shuō)了苞氮,保存了實(shí)例化DexPathList的classloader湾揽,第二個(gè)參數(shù)的聲明是一個(gè)Element數(shù)組,第三個(gè)參數(shù)是lib庫(kù)的目錄文件數(shù)組,
private final Element[] dexElements;
private final File[] nativeLibraryDirectories;
看它們之前先看看幾個(gè)split小函數(shù):
private static ArrayList<File> splitDexPath(String path) {
return splitPaths(path, null, false);
}
private static File[] splitLibraryPath(String path) {
ArrayList<File> result = splitPaths(
path, System.getProperty("java.library.path", "."), true);
return result.toArray(new File[result.size()]);
}
這兩個(gè)顧名思義就是拿來(lái)分割dexPath和libPath笼吟,它們內(nèi)部都調(diào)用了splitPaths方法库物,只是三個(gè)參數(shù)不一樣,其中splitLibraryPath方法中調(diào)用splitPaths時(shí)的第二個(gè)參數(shù)仿佛又透露了什么信息贷帮,沒(méi)錯(cuò),之前介紹DexClassLoader參數(shù)中的libraryPath的時(shí)候說(shuō)過(guò)民晒,會(huì)加上系統(tǒng)so庫(kù)的存放目錄潜必,就是在這個(gè)時(shí)候添加上去的磁滚。
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;
}
什么啊维雇,原來(lái)這個(gè)方法也沒(méi)做什么事啊吱型,只是把參數(shù)path1和參數(shù)path2又分別調(diào)用了下splitAndAdd方法唁影,但是這里創(chuàng)建了一個(gè)ArrayList,而且調(diào)用splitAndAdd方法的時(shí)候都當(dāng)參數(shù)傳入了掂名,并且最終返回了這個(gè)list饺蔑,所以我們大膽猜測(cè)下猾警,path1和path2最后被分割后的值都存放在了list中返回,看下是不是這么一回事吧:
private static void splitAndAdd(String path, boolean wantDirectories,
ArrayList<File> resultList) {
if (path == null) {
return;
}
String[] strings = path.split(Pattern.quote(File.pathSeparator));
for (String s : strings) {
File file = new File(s);
if (! (file.exists() && file.canRead())) {
continue;
}
/*
* Note: There are other entities in filesystems than
* regular files and directories.
*/
if (wantDirectories) {
if (!file.isDirectory()) {
continue;
}
} else {
if (!file.isFile()) {
continue;
}
}
resultList.add(file);
}
}
果然崔慧,跟我們猜的一樣穴墅,只是又加上了文件是否存在以及是否可讀的驗(yàn)證玄货,然后根據(jù)參數(shù)wantDirectories判斷是否文件類(lèi)型是被需要的類(lèi)型松捉,最終加入list“溃現(xiàn)在我們回過(guò)頭去看看splitDexPath方法和splitLibraryPath方法铃剔,是不是一目了然了玛荞。
再往上看DexPathList的構(gòu)造器,nativeLibraryDirectories的最終值也已經(jīng)知道了滥沫,就差dexElements了,makeDexElements方法的兩個(gè)參數(shù)我們也已經(jīng)知道了世分,那我們就看看makeDexElements都干了些什么吧臭埋。
private static Element[] makeDexElements(ArrayList<File> files,
File optimizedDirectory) {
ArrayList<Element> elements = new ArrayList<Element>();
/*
* Open all files and load the (direct or contained) dex files
* up front.
*/
for (File file : files) {
ZipFile 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)) {
try {
zip = new ZipFile(file);
} catch (IOException ex) {
/*
* Note: ZipException (a subclass of IOException)
* might get thrown by the ZipFile constructor
* (e.g. if the file isn't actually a zip/jar
* file).
*/
System.logE("Unable to open zip file: " + file, ex);
}
try {
dex = loadDexFile(file, optimizedDirectory);
} catch (IOException ignored) {
/*
* 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). Safe to just ignore
* the exception here, and let dex == null.
*/
}
} else {
System.logW("Unknown file type for: " + file);
}
if ((zip != null) || (dex != null)) {
elements.add(new Element(file, zip, dex));
}
}
return elements.toArray(new Element[elements.size()]);
}
方法也不長(zhǎng),我們一段段看下去荣恐。首先創(chuàng)建了一個(gè)elememt 列表叠穆,然后遍歷由dexpath分割得來(lái)的文件列表硼被,其實(shí)一般使用場(chǎng)景下也就一個(gè)文件嚷硫。循環(huán)里面針對(duì)每個(gè)file 聲明一個(gè)zipfile和一個(gè)dexfile论巍,判斷file的文件后綴名嘉汰,如果是".dex"則使用loadDexFile方法給dex賦值鞋怀,如果是“.apk”,“.jar”,“.zip”文件的則把file包裝成zipfile賦值給zip密似,然后同樣是用loadDexFile方法給dex賦值残腌,如果是其他情況則不做處理抛猫,打印日志說(shuō)明文件類(lèi)型不支持闺金,而且下一個(gè)if判斷中由于zip和dex都未曾賦值败匹,所以也不會(huì)添加到elements列表中去掀亩。注意下:這里所謂的文件類(lèi)型僅僅是指文件的后綴名而已槽棍,并不是文件的實(shí)際類(lèi)型刹泄,比如我們將.zip文件后綴改成.txt特石,那么就不支持這個(gè)文件了姆蘸。而且我們可以看到對(duì)于dexpath目前只支持“.dex”逞敷、“.jar”推捐、“.apk”牛柒、“.zip”這四種類(lèi)型皮壁。
現(xiàn)在還剩下兩個(gè)東西可能還不太明確蛾魄,一個(gè)是什么是DexFile以及這里的loadDexFile方法是如何創(chuàng)建dexfile實(shí)例的湿滓,另一個(gè)是什么是Elememt叽奥,看了下Element源碼,哈哈恋日,so easy岂膳,就是一個(gè)簡(jiǎn)單的數(shù)據(jù)結(jié)構(gòu)體加一個(gè)方法磅网,所以我們就先簡(jiǎn)單把它當(dāng)做一個(gè)存儲(chǔ)了file簸喂,zip喻鳄,dex三個(gè)字段的一個(gè)實(shí)體類(lèi)除呵。那么就剩下DexFile了颜曾。
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);
}
}
很簡(jiǎn)潔泛豪,如果optimizedDirectory == null則直接new 一個(gè)DexFile候址,否則就使用DexFile.loadDex來(lái)創(chuàng)建一個(gè)DexFile實(shí)例岗仑。
/**
* 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) {
/*
* 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();
}
}
File result = new File(optimizedDirectory, fileName);
return result.getPath();
}
這個(gè)方法獲取被加載的dexpath的文件名稳其,如果不是“.dex”結(jié)尾的就改成“.dex”結(jié)尾既鞠,然后用optimizedDirectory和新的文件名構(gòu)造一個(gè)File并返回該File的路徑嘱蛋,所以DexFile.loadDex方法的第二個(gè)參數(shù)其實(shí)是dexpath文件對(duì)應(yīng)的優(yōu)化文件的輸出路徑洒敏。比如我要加載一個(gè)dexpath為“sdcard/coder_yu/plugin.apk”凶伙,optimizedDirectory 為使用范例中的目錄的話函荣,那么最終優(yōu)化后的輸出路徑為/data/user/0/com.coder_yu.test/app_dex/plugin.dex,具體的目錄在不同機(jī)型不同rom下有可能會(huì)不一樣傻挂。
是時(shí)候看看DexFile了踊谋。在上面的loadDexFile方法中我們看到optimizedDirectory參數(shù)為null的時(shí)候直接返回new DexFile(file)了殖蚕,否則返回 DexFile.loadDex(file.getPath(), optimizedPath, 0)睦疫,但其實(shí)他們最終都是使用了相同方法去加載dexpath文件蛤育,因?yàn)镈exFile.loadDex方法內(nèi)部也是直接調(diào)用的了DexFile的構(gòu)造器瓦糕,以下:
static public DexFile loadDex(String sourcePathName, String outputPathName,
int flags) throws IOException {
return new DexFile(sourcePathName, outputPathName, flags);
}
然后看看DexFile的構(gòu)造器吧
public DexFile(File file) throws IOException {
this(file.getPath());
}
/**
* Opens a DEX file from a given filename. This will usually be a ZIP/JAR
* file with a "classes.dex" inside.
*
* The VM will generate the name of the corresponding file in
* /data/dalvik-cache and open it, possibly creating or updating
* it first if system permissions allow. Don't pass in the name of
* a file in /data/dalvik-cache, as the named file is expected to be
* in its original (pre-dexopt) state.
*
* @param fileName
* the filename of the DEX file
*
* @throws IOException
* if an I/O error occurs, such as the file not being found or
* access rights missing for opening it
*/
public DexFile(String fileName) throws IOException {
mCookie = openDexFile(fileName, null, 0);
mFileName = fileName;
guard.open("close");
//System.out.println("DEX FILE cookie is " + mCookie);
}
private DexFile(String sourceName, String outputName, int flags) throws IOException {
mCookie = openDexFile(sourceName, outputName, flags);
mFileName = sourceName;
guard.open("close");
//System.out.println("DEX FILE cookie is " + mCookie);
}
可以看到直接new DexFile(file)和DexFile.loadDex(file.getPath(), optimizedPath, 0)最終都是調(diào)用了openDexFile(sourceName咕娄, outputName, flags)方法费变,只是直接new的方式optimizedPath參數(shù)為null圣贸,這樣openDexFile方法會(huì)默認(rèn)使用 /data/dalvik-cache目錄作為優(yōu)化后的輸出目錄吁峻,第二個(gè)構(gòu)造器的注釋中寫(xiě)的很明白了矮慕。mCookie是一個(gè)int值耕餐,保存了openDexFile方法的返回值肠缔,openDexFile方法是一個(gè)native方法明未,我們就不深入了趟妥,我自己也就看了個(gè)大概披摄,有興趣的同學(xué)可以看下這篇文章:
http://blog.csdn.net/zhoushishang/article/details/38703623
這樣總算是創(chuàng)建了一個(gè)DexClassLoader疚膊。
我們回顧一下:從new DexClassLoader(dexPath,optimizedDirectory,libraryPath,parentLoader)開(kāi)始,調(diào)用父類(lèi)BaseDexClassLoader構(gòu)造器寓盗,用originalPath 保存了 dexPath傀蚌,pathList保存了一個(gè)由dexPath善炫、optimizedDirectory销部、libraryPath、loader四個(gè)參數(shù)構(gòu)建的DexPathList酱虎,DexPathList中的definingContext 保存了parentLoader读串,optimizedDirectory和libraryPath會(huì)被分割成數(shù)組恢暖,其中nativeLibraryDirectories保存了libraryPath被分割后的數(shù)組杰捂,并且加上了系統(tǒng)so庫(kù)的目錄嫁佳,dexElements保存了由dexPath被分割后的對(duì)應(yīng)的file而創(chuàng)建的Elememt蒿往,它只是一個(gè)簡(jiǎn)單實(shí)體類(lèi)瓤漏,由一個(gè)File蔬充,一個(gè)ZipFile饥漫,一個(gè)DexFile組成趾浅,ZipFile是由jar皿哨、zip证膨、apk形式的file包裝成而來(lái)不见,DexFile使用native方法openDexFile打開(kāi)了具體的file并輸出到優(yōu)化路徑稳吮。
DexClassLoader的創(chuàng)建過(guò)程我們已經(jīng)看完了灶似,看看它是如何去找到我們需要的類(lèi)的吧酪惭!
先把使用范例再貼一遍
try {
File file = view.getActivity().getDir("dex",0);
String optimizedDirectory = file.getAbsolutePath();
DexClassLoader loader = new DexClassLoader("需要被加載的dex文件所在的路徑",optimizedDirectory,null,context.getClassLoader());
loader.loadClass("需要加載的類(lèi)的完全限定名");
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
我們已經(jīng)拿到loader了春感,然后就是loader.loadClass("需要加載的類(lèi)的完全限定名"),我們假設(shè)需要被加載的類(lèi)為 'com.coder_yu.plugin.Test',也即是loader.loadClass("com.coder_yu.plugin.Test"),所以我們先去DexClassLoader中看看loaderClass方法刀疙,不過(guò)好像沒(méi)有這個(gè)方法扫倡,那就去它的父類(lèi)BaseDexClassLoader中看看撵溃,還是 沒(méi)有,那就再往上走锥累,去ClassLoader類(lèi)里面找缘挑,總算找到了:
public Class<?> loadClass(String className) throws ClassNotFoundException {
return loadClass(className, false);
}
直接調(diào)用另一個(gè)loadclass方法,看一下:
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;
}
直接調(diào)用findLoadedClass方法桶略,這個(gè)方法會(huì)去虛擬機(jī)中找當(dāng)前classloader是否已經(jīng)加載過(guò)該class了语淘,如果加載過(guò)了則直接返回這個(gè)class,否則返回null际歼,并去調(diào)用parent的loadClass方法惶翻,依次類(lèi)推,會(huì)一直找到根classloader的findLoadedClass方法吕粗,因?yàn)槲覀冞@里是第一次去加載sd卡上的自定義插件,所以肯定沒(méi)被加載過(guò),那么直到根classloader.findLoadedClass將一直返回null靶剑,然后會(huì)去調(diào) findClass(className)方法收夸,我們先看一下findLoadedClass方法吧:
protected final Class<?> findLoadedClass(String className) {
ClassLoader loader;
if (this == BootClassLoader.getInstance())
loader = null;
else
loader = this;
return VMClassLoader.findLoadedClass(loader, className);
}
BootClassLoader是ClassLoader的默認(rèn)parentloader,所以它是加載器鏈中的頂端设凹,也就是classloader的根節(jié)點(diǎn),因此它會(huì)最終代理一些方法到虛擬機(jī)去執(zhí)行,感興趣的同學(xué)可以去看下ClassLoader的源碼。這里說(shuō)一下,其實(shí)根節(jié)點(diǎn)我們剛才說(shuō)了是BootClassLoader,而且它重寫(xiě)了loadClass方法困鸥,因?yàn)樗鼪](méi)有parentloader鸟废,所以它沒(méi)有再去調(diào)用parent.loadClass(className, false),而是直接調(diào)用findClass方法,而findClass方法中也是直接返回Class.classForName(name, false, null)得到的值,顯然結(jié)果是null。
我們構(gòu)造DexClassLoader的時(shí)候給的parentloader參數(shù)是context.getClassLoader()该押,它其實(shí)就是加載我們應(yīng)用的類(lèi)加載器闻牡,也就是PathClassLoader翼馆,它的parentloader就是BootClassLoader猜极,這個(gè)具體代碼我沒(méi)去找,可以簡(jiǎn)單的打印下日志就能發(fā)現(xiàn),不過(guò)這個(gè)情況在instant run 的情況下會(huì)有所不同蕊蝗,大家可以去研究研究~有點(diǎn)扯遠(yuǎn)了,這里我想說(shuō)的是我們傳入的應(yīng)用類(lèi)加載器中通過(guò)findClass也是加載不到我們需要的類(lèi)的,所以我們還是得去看我們自定義的DexClassLoader中的findClass方法,DexClassLoader中還是沒(méi)有重寫(xiě)這個(gè)方法,那么還是去BaseDexClassLoader中看吧:
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
Class clazz = pathList.findClass(name);
if (clazz == null) {
throw new ClassNotFoundException(name);
}
return clazz;
}
還記得我們的pathList嗎忽匈?不記得回頭看看上面的圖:
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;
}
Element是不是也很熟悉郭厌?就是那個(gè)簡(jiǎn)單的實(shí)體類(lèi),所以繼續(xù)到DexFile中去看dex.loadClassBinaryName方法:
public Class loadClassBinaryName(String name, ClassLoader loader) {
return defineClass(name, loader, mCookie);
}
private native static Class defineClass(String name, ClassLoader loader, int cookie);
最終使用了native方法defineClass去加載我們需要的類(lèi)巷懈,注意參數(shù)里有個(gè)mCookie欧引,它就是openDexFile方法返回的dex文件返回的int值因痛,這樣就將我們需要加載的類(lèi)和我們的dex文件對(duì)應(yīng)起來(lái)了,去從我們給定的dex文件中去加載我們需要的類(lèi)。
至此歹垫,我們的加載過(guò)程就很清楚了欲低。本文所提及的內(nèi)容并沒(méi)有什么技術(shù)含量,只是順著一條主線梳理了下整個(gè)過(guò)程财松,對(duì)于搞明白一些細(xì)節(jié)可能還是有點(diǎn)幫助喷户。順帶說(shuō)一下龟虎,其實(shí)熱修復(fù)也是用到了這個(gè)加載機(jī)制,動(dòng)態(tài)的替換掉有bug的類(lèi)衷模,因?yàn)橐呀?jīng)被加載過(guò)的類(lèi)不會(huì)再被加載镊叁,所以只要我們把修復(fù)bug后的類(lèi)放到有bug那個(gè)類(lèi)之前加載痰催,就能解決問(wèn)題了缝裁。