主目錄見:Android高級(jí)進(jìn)階知識(shí)(這是總目錄索引)
在線源碼查看:AndroidXRef
了解這一篇的知識(shí)對(duì)后面插件化中的類加載是必不可少的,我們知道罚缕,我們的應(yīng)用中的類在編譯過程中會(huì)被編譯成dex文件并鸵,所以把我們的dex加載進(jìn)我們的程序我們就可以查找到插件中的類苇倡。我們今天就會(huì)來了解這一過程。
一.類加載器
學(xué)過java的應(yīng)該知道审姓,我們的類是通過類加載器加載到JVM的珍特,Android也不例外,Android中有兩個(gè)特別重要的ClassLoader:
1.PathClassLoader:
可以看到這邊英文描述了這個(gè)類加載器不會(huì)加載網(wǎng)絡(luò)上的類魔吐,只會(huì)加載系統(tǒng)類和應(yīng)用類的扎筒,而且在dalvik虛擬機(jī)上只能加載已經(jīng)安裝的apk的dex。當(dāng)然在android 5.0之后是否可以加載未安裝的apk的dex酬姆,這個(gè)沒做過實(shí)驗(yàn)嗜桌。但是可以知道,用這個(gè)類加載器來加載插件中的dex是不可行的辞色。我們看下完整的這個(gè)類:
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òu)造函數(shù)都是調(diào)用了super的構(gòu)造函數(shù)骨宠,所以我們待會(huì)在看BaseDexClassLoader
時(shí)候會(huì)詳細(xì)來說明。但是我們看第二個(gè)參數(shù)為空相满,這個(gè)參數(shù)是optimizedDirectory
层亿,是dex文件被加載后會(huì)被編譯器優(yōu)化,優(yōu)化之后的dex存放路徑立美,因?yàn)?code>PathClassLoader只能加載系統(tǒng)類或者應(yīng)用的類棕所,所以這個(gè)為空,其實(shí)optimizedDirectory
為null時(shí)的默認(rèn)路徑就是/data/dalvik-cache 目錄悯辙。
2.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.
*/
上面英文注釋分別說明了三個(gè)方面的知識(shí):
1).DexClassLoader
支持加載APK琳省、DEX和JAR,也可以從SD卡進(jìn)行加載躲撰。
上面說dalvik不能直接識(shí)別jar,DexClassLoader
卻可以加載jar文件,這難道不矛盾嗎?其實(shí)在BaseDexClassLoader
里對(duì)".jar",".zip",".apk",".dex"后綴的文件最后都會(huì)生成一個(gè)對(duì)應(yīng)的dex文件,所以最終處理的還是dex文件针贬。
2).這個(gè)類需要提供一個(gè)optimizedDirectory
路徑用于存放優(yōu)化后的dex。
3).optimizedDirectory
路徑不允許是外部存儲(chǔ)的路徑拢蛋,為了防止應(yīng)用被注入攻擊桦他。
我們來看下DexClassLoader完整的類:
public class DexClassLoader extends BaseDexClassLoader {
public DexClassLoader(String dexPath, String optimizedDirectory,
String libraryPath, ClassLoader parent) {
super(dexPath, new File(optimizedDirectory), libraryPath, parent);
}
}
我們看到第二個(gè)參數(shù)傳了不為null的目錄,跟PathDexClassLoader不同谆棱,所以這個(gè)類加載器可以從外部存儲(chǔ)里面加載apk快压,dex或者jar文件,我們目標(biāo)就是它了垃瞧。
3.BaseDexClassLoader
PathClassLoader
和DexClassLoader
都繼承自BaseDexClassLoader
,其中的主要邏輯都是在BaseDexClassLoader
完成的蔫劣。
可以看到構(gòu)造函數(shù)會(huì)new出一個(gè)DexPathList對(duì)象,我們等會(huì)會(huì)說个从,現(xiàn)在我們先來看看參數(shù)的意思:
1).dexPath:待加載的類的apk脉幢,jar歪沃,dex的路徑,必須是全路徑嫌松。如果要包含多個(gè)路徑,路徑之間必須使用特定的分割符分隔,特定的分割符可以使用
File.pathSeparato
r獲得沪曙。上面"支持加載APK、DEX和JAR萎羔,也可以從SD卡進(jìn)行加載"指的就是這個(gè)路徑液走,最終做的是將dexPath路徑上的文件ODEX優(yōu)化到內(nèi)部位置optimizedDirectory,然后贾陷,再進(jìn)行加載的育灸。2).libraryPath:目標(biāo)類中所使用的C/C++庫存放的路徑,也就是so文件的路徑昵宇。
3).ClassLoader:是指該裝載器的父裝載器,一般為當(dāng)前執(zhí)行類的裝載器磅崭,例如在Android中以
context.getClassLoader()
作為父裝載器。因?yàn)轭惣虞d器的雙親委托機(jī)制瓦哎,需要設(shè)置一個(gè)父裝載器砸喻。
二.類加載過程
我們知道,類的加載過程最終都會(huì)通過BaseDexClassLoader
中的findClass()
開始的:
可以看到我們這里的Class對(duì)象是通過pathList中的
findClass()
方法獲取的蒋譬。這里的pathList又是什么呢割岛?這個(gè)類在上面BaseDexClassLoader
的構(gòu)造函數(shù)中初始化的。我們可以看下:
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 = makePathElements(splitDexPath(dexPath), optimizedDirectory, 121 suppressedExceptions);
this.nativeLibraryDirectories = splitPaths(libraryPath, false);
this.systemNativeLibraryDirectories = splitPaths(System.getProperty("java.library.path"), true);
List<File> allNativeLibraryDirectories = new ArrayList<>(nativeLibraryDirectories);
allNativeLibraryDirectories.addAll(systemNativeLibraryDirectories);
this.nativeLibraryPathElements = makePathElements(allNativeLibraryDirectories, null, 140 suppressedExceptions);
}
這個(gè)類很重要犯助,我們可以看到首先是dexPath癣漆,optimizedDirectory的非空判斷。然后是dexElements
的賦值剂买,這里我們說下dexElements
:
/**
* List of dex/resource (class path) elements.
* Should be called pathElements, but the Facebook app uses reflection
* to modify 'dexElements' (http://b/7726934). */
private final Element[] dexElements;
這個(gè)數(shù)組就是放我們dex的數(shù)組惠爽,我們的不同的dex作為數(shù)組存放。里面注釋很有意思有句話瞬哼,F(xiàn)aceBook使用反射來修改dexElements
婚肆,很明確告訴我們也可以通過修改這個(gè)數(shù)組來加載我們的dex。接著我們來看看makePathElements
方法坐慰,在看這個(gè)方法之前我們看到里面有個(gè)參數(shù)是調(diào)用splitDexPath
方法,這個(gè)方法是用于根據(jù)分隔符取到文件列表的:
private static Element[] makePathElements(ArrayList<File> files,
File optimizedDirectory) {
for (File file : files) {
File zip = null;
File dir = new File("");
DexFile dex = null;
String path = file.getPath();
String name = file.getName();
if (path.contains(zipSeparator)) {
String split[] = path.split(zipSeparator, 2);
zip = new File(split[0]);
dir = new File(split[1]);
} else if (file.isDirectory()) {
// We support directories for looking up resources and native libraries.
// Looking up resources in directories is useful for running libcore tests.
elements.add(new Element(file, true, null, null));
} else if (file.isFile()) {
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 {
zip = file;
try {
dex = loadDexFile(file, optimizedDirectory);
} catch (IOException suppressed) {
......
}
}
} else {
System.logW("Unknown file type for: " + file);
}
if ((zip != null) || (dex != null)) {
elements.add(new Element(dir, false, zip, dex));
}
}
return elements.toArray(new Element[elements.size()]);
}
這個(gè)方法就是遍歷之前得到的文件列表较性,然后判斷傳進(jìn)來的文件是目錄,zip文件或者dex文件结胀,如果是目錄的話赞咙,直接將文件file作為參數(shù)傳給Element然后添加進(jìn)elements中。否則其他情況都會(huì)調(diào)用loadDexFile
方法進(jìn)行加載糟港,我們看下這個(gè)方法:
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);
}
}
我們看到這個(gè)方法很簡(jiǎn)單攀操,如果optimizedDirectory == null則直接new 一個(gè)DexFile
,否則就使用DexFile#loadDex
來創(chuàng)建一個(gè)DexFile
實(shí)例着逐。
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)化文件的輸出路徑健芭。
接著 DexPathList
構(gòu)造函數(shù)中會(huì)獲取so文件庫的路徑,然后傳給makePathElements
方法秀姐,同樣地慈迈,也是添加到DexElement中,到這里我們已經(jīng)將我們的類和so文件添加進(jìn)DexElement數(shù)組中了省有。所以我們插件化框架只要將我們的插件中的類想辦法添加進(jìn)DexElement數(shù)組中就可以了痒留。
然后我們繼續(xù)分析我們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;
}
我們看到這里會(huì)遍歷我們的dexElements數(shù)組,然后取出數(shù)組中的DexFile
對(duì)象蠢沿,調(diào)用他的DexFile#loadClassBinaryName
方法:
public Class loadClassBinaryName(String name, ClassLoader loader) {
return defineClass(name, loader, mCookie);
}
private native static Class defineClass(String name, ClassLoader loader, int cookie);
我們看到最終查找類會(huì)調(diào)用到native的defineClass()方法伸头。這樣,我們的加載流程就算講完了舷蟀。
總結(jié):到這里類的加載已經(jīng)講完了恤磷,這里只是說明了一下流程,希望對(duì)大家會(huì)有點(diǎn)幫助野宜,這也是插件化中很重要的一步扫步。