插件化的第一步就是要解決類加載問(wèn)題贬媒,因?yàn)椴寮遣话惭b的,要直接加載Apk中的類,apk的中的class是封裝成dex文件放在APK內(nèi)的移剪。
Dex文件
Dex即 Dalvik Executable的簡(jiǎn)寫喜喂,Dex文件是一種壓縮文件格式的封裝瓤摧。 是Android對(duì)class文件進(jìn)行翻譯、重構(gòu)玉吁、解釋照弥、壓縮等操作之后的產(chǎn)物。dex中各個(gè)類能夠共享數(shù)據(jù)进副,在一定程度上降低了冗余这揣,同時(shí)也是文件結(jié)構(gòu)更加經(jīng)湊悔常,實(shí)驗(yàn)表明,dex文件是傳統(tǒng)jar文件大小的50%左右给赞。
文件結(jié)構(gòu):
名稱 | 解釋 |
---|---|
header | dex文件頭部机打,記錄整個(gè)dex文件的相關(guān)屬性 |
string_ids | 字符串?dāng)?shù)據(jù)索引,記錄了每個(gè)字符串在數(shù)據(jù)區(qū)的偏移量 |
type_ids | 類似數(shù)據(jù)索引片迅,記錄了每個(gè)類型的字符串索引 |
proto_ids | 原型數(shù)據(jù)索引残邀,記錄了方法聲明的字符串,返回類型字符串柑蛇,參數(shù)列表 |
field_ids | 字段數(shù)據(jù)索引芥挣,記錄了所屬類,類型以及方法名 |
method_ids | 類方法索引耻台,記錄方法所屬類名空免,方法聲明以及方法名等信息 |
class_defs | 類定義數(shù)據(jù)索引,記錄指定類各類信息粘我,包括接口鼓蜒,超類,類數(shù)據(jù)偏移量 |
data | 數(shù)據(jù)區(qū)征字,保存了各個(gè)類的真實(shí)數(shù)據(jù) |
link_data | 連接數(shù)據(jù)區(qū) |
DexClassloader
PathClassLoader和DexClassLoader都繼承自BaseDexClassLoader,其中的主要邏輯都是在BaseDexClassLoader完成的
public BaseDexClassLoader(String dexPath, File optimizedDirectory,
String libraryPath, ClassLoader parent) {
super(parent);
this.originalPath = dexPath;
this.pathList =
new DexPathList(this, dexPath, libraryPath, optimizedDirectory);
}
- dexPath:目標(biāo)類所在的APK或jar文件的路徑都弹。類裝載器將從該路徑中尋找指定的目標(biāo)類,該類必須是APK或jar的全路徑.如果要包含多個(gè)路徑,路徑之間必須使用特定的分割符分隔,特定的分割符可以使用System.getProperty(“path.separtor”)獲得。支持加載APK匙姜、DEX和JAR畅厢,也可以從SD卡進(jìn)行加載。最終都是將dexPath路徑上的文件ODEX優(yōu)化到內(nèi)部位置optimizedDirectory氮昧,然后框杜,再進(jìn)行加載的。
- optimizedDirectory: 緩存需要加載的dex文件袖肥,并創(chuàng)建一個(gè)DexFile對(duì)象咪辱,如果它為null,那么會(huì)直接使用dex文件原有的路徑來(lái)創(chuàng)建DexFile對(duì)象椎组。該參數(shù)就是制定的從Apk文件中解壓出的dex 文件存放的路徑油狂。
- libPath:目標(biāo)類中所使用的C/C++庫(kù)存放的路徑
- parent: 該裝載器的父裝載器。一般為當(dāng)前執(zhí)行類的裝載器寸癌,例如在Android中以context.getClassLoader()作為父裝載器专筷。
DexClassLoader可以指定自己的optimizedDirectory,所以可以加載外部的dex蒸苇,因?yàn)檫@個(gè)dex會(huì)被復(fù)制到內(nèi)部路徑的optimizedDirectory磷蛹。而PathClassLoader沒(méi)有optimizedDirectory,所以它只能加載內(nèi)部的dex溪烤,這些大都是存在系統(tǒng)中已經(jīng)安裝過(guò)的apk里面的
所有的操作都是通過(guò)DexPathList實(shí)現(xiàn)的
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;
}
DexPathList
DexPathList中有個(gè)dexElements數(shù)組
private final Element[] dexElements;
this.dexElements =
makeDexElements(splitDexPath(dexPath), optimizedDirectory);
dexElements數(shù)組就是odex文件的集合味咳。odex文件是 dexPath指向的原始dex(.apk,.zip,.jar等)文件在optimizedDirectory文件夾中生成相應(yīng)的優(yōu)化后的文件庇勃。如果不分包一般這個(gè)數(shù)組只有一個(gè)Element元素,也就只有一個(gè)DexFile文件
makeDexElements方法生成一個(gè)Element[] dexElements 數(shù)組槽驶。
if (name.endsWith(DEX_SUFFIX)) {
try {
dex = loadDexFile(file, optimizedDirectory);
} catch (IOException ex) {
System.logE("Unable to load dex file: " + file, ex);
}
loadDexFile:
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);
}
}
DexFile最終是通過(guò)JNI調(diào)用native加載dex文件的匪凉。
加載class:
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;
}
這里就出現(xiàn)了可以實(shí)現(xiàn)熱修復(fù)和插件化的hook點(diǎn)。將 dex 文件放到 dexElements 數(shù)組前面捺檬,這樣在加載 class 時(shí),優(yōu)先找到前面的 dex 文件贸铜,加載到 class 之后就不再尋找堡纬。從而原來(lái)的 apk 文件中同名的類就不會(huì)再使用,從而達(dá)到修復(fù)的目的蒿秦。
public Class loadClassBinaryName(String name, ClassLoader loader) {
return defineClass(name, loader, mCookie);
}
private native static Class defineClass(String name, ClassLoader loader, int cookie);
標(biāo)準(zhǔn)JVM中烤镐,ClassLoader是用defineClass加載類的,而Android中defineClass被棄用了棍鳖,改用了loadClass方法炮叶,而且加載類的過(guò)程也移動(dòng)到了DexFile中,在DexFile中加載類的具體方法也叫defineClass渡处,這也是為了維護(hù)代碼可讀性
Native library添加
插件中可能包含native library镜悉,為了插件能運(yùn)行,Native library也需要加載医瘫。
DexPathList中有一個(gè)屬性:
private final File[] nativeLibraryDirectories;
nativeLibraryDirectories就是nativeLibrary所在的文件夾數(shù)組侣肄,只需要把插件的nativeLibrary所在的文件夾通過(guò)反射添加進(jìn)去就行了
整個(gè)實(shí)現(xiàn)過(guò)程
要加載插件中的類和native library, 當(dāng)然得實(shí)現(xiàn)一個(gè)DexClassloader,通過(guò)這個(gè)DexClassloader去加載插件中的dex
創(chuàng)建classLoader:
直接new一個(gè)即可醇份。初始化的參數(shù):
dexPath:插件apk的文件的絕對(duì)路徑
optimizedDirectory:dex文件夾的的絕對(duì)路徑稼锅,可通過(guò)context.getDir("dex", Context.MODE_PRIVATE)獲取
libraryPath: 存放native Library文件夾對(duì)應(yīng)的路徑,context.getDir("valibs",Context.MODE_PRIVATE)獲取
parent:默認(rèn)的ClassLoader僚纷,即context.getClassLoader()
將插件中的dex加入到dexElements中:
- 通過(guò)反射獲取到默認(rèn)的ClassLoader中的dexElements
- 反射獲取到新classLoader中的dexElements
- 合并兩個(gè)數(shù)組
- 反射將合并之后的數(shù)組賦值到默認(rèn)ClassLoader中的pathList中
native Library操作類似矩距。
這樣操作之后默認(rèn)的ClassLoader中已經(jīng)有了插件中的dex和native Library,可以加載對(duì)應(yīng)的類了怖竭。