一. 類加載器
Android中的類加載器中主要包括三類BootClassLoader(繼承ClassLoader),PathClassLoader和DexClassLoader。后兩個繼承于BaseDexClassLoader。
1.BootClassLoader
:主要用于加載系統的類,包括java和android系統的類庫。(比如TextView,Context,只要是系統的類都是由BootClassLoader加載完成)。
通過打印TextView.class.getClassLoader()即可驗證
2.PathClassLoader
:主要用于加載我們應用程序內的類。路徑是固定的,只能加載
/data/app中的apk,無法指定解壓釋放dex的路徑,無法動態加載。對于我們的應用默認為PathClassLoader
通過打印getClassLoader()以及ClassLoader.getSystemClassLoader()即可驗證
3.DexClassLoader
:可以用來加載任意路徑的zip,jar或者apk文件。可以實現動態加載。
簡單看一下這兩個類的源碼:
DexClassLoader類的源碼如下:
package dalvik.system;
public class DexClassLoader extends BaseDexClassLoader {
public DexClassLoader(String dexPath, String optimizedDirectory,
String libraryPath, ClassLoader parent) {
super(dexPath, new File(optimizedDirectory), libraryPath, parent);
}
//但是api26以上 這個函數源碼如下 也就是第二個參數已經沒有影響
// * @param optimizedDirectory this parameter is deprecated and has no effect since API level 26.
public DexClassLoader(String dexPath, String optimizedDirectory,
String librarySearchPath, ClassLoader parent) {
super(dexPath, null, librarySearchPath, parent);
}
}
PathClassLoader類的源碼如下:
package dalvik.system;
public class PathClassLoader extends BaseDexClassLoader {
public PathClassLoader(String dexPath, ClassLoader parent) {
super(dexPath, null, null, parent);
}
public PathClassLoader(String dexPath, String librarySearchPath, ClassLoader parent) {
super(dexPath, null, librarySearchPath, parent);
}
}
參數意義
dexPath :需要被加載的jar/apk/dex 文件地址,可以多個,用File.pathSeparator分割。
optimizedDirectory:因為加載apk/jar的時候會被編譯器優化解壓出dex文件,這個路徑就是保存dex文件的。但在api26以上這個參數默認也為null。
libraryPath:庫lib文件的路徑
parent:給DexClassLoader指定父加載器
可以發現PathClassLoader和DexClassLoader源碼很簡單,只包含了一個構造函數,去調用父類BaseDexClassLoader。(所有的工作都應該是在BaseDexClassLoader里完成的了。)而這兩個加載器不同的是PathClassLoader的構造中少了optimizedDirectory這個參數,原因是PathClassLoader是加載/data/app中的apk,也就是系統中的apk,而這部分的apk都會解壓釋放dex到指定的目錄:/data/dalvik-cache中,這個操作由系統完成,不需要單獨傳入路徑,而DexClassLoader傳入,用來緩存需要加載的dex文件,并創建一個DexFile對象,如果為null,會直接使用dex文件原有路徑創建DexFile;這個參數已經棄用,自API26起無效;
二、 DexPathList
接下來具體看一下BaseDexClassLoader
public class BaseDexClassLoader extends ClassLoader {
private final DexPathList pathList;
public BaseDexClassLoader(String dexPath, File optimizedDirectory,
String librarySearchPath, ClassLoader parent) {
super(parent);
this.pathList = new DexPathList(this, dexPath, librarySearchPath, 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里我們可以看到根據傳入的地址參數構造了一個DexPathList對象。從findClass方法可以看出來加載的類都是從pathList中查找。【findclass方法】是BaseDexClassLoader這個類的核心。那接下來看一下DexPathList類
private final Element[] dexElements;
public DexPathList(ClassLoader definingContext, String dexPath,
String libraryPath, File optimizedDirectory) {
...
this.definingContext = definingContext;
this.dexElements = makeDexElements(splitDexPath(dexPath), optimizedDirectory,suppressedExceptions);
...
}
這里的重點是通過makeDexElements方法得到dexElements集合。而splitDexPath方法是將傳入的文件集合轉化為一個文件File合集,因為我們上面提到了dexPath可以是多個,用文件分隔符連接即可。
private static Element[] makeDexElements(ArrayList<File> files, File optimizedDirectory, ArrayList<IOException> suppressedExceptions) {
// 1.創建Element集合
ArrayList<Element> elements = new ArrayList<Element>();
// 2.遍歷所有dex文件(也可能是jar、apk或zip文件)
for (File file : files) {
ZipFile zip = null;
DexFile dex = null;
String name = file.getName();
...
// 如果是dex文件
if (name.endsWith(DEX_SUFFIX)) {
dex = loadDexFile(file, optimizedDirectory);
// 如果是apk、jar、zip文件(這部分在不同的Android版本中,處理方式有細微差別)
} else {
zip = file;
dex = loadDexFile(file, optimizedDirectory);
}
...
// 3.將dex文件或壓縮文件包裝成Element對象,并添加到Element集合中
if ((zip != null) || (dex != null)) {
elements.add(new Element(file, false, zip, dex));
}
}
// 4.將Element集合轉成Element數組返回
return elements.toArray(new Element[elements.size()]);
}
總體來說,DexPathList的構造函數是將一個個的程序文件(可能是dex、apk、jar、zip)先通過loadDexFile轉變成dex,然后封裝成一個個Element對象,最后添加到Element集合中。BaseDexClassLoader的findclass方法也就是進一步,我們可以繼續看DexPathList的findClass()方法了:
public Class findClass(String name, List<Throwable> suppressed) {
for (Element element : dexElements) {
// 遍歷出一個dex文件
DexFile dex = element.dexFile;
if (dex != null) {
// 在dex文件中查找類名與name相同的類
Class clazz = dex.loadClassBinaryName(name, definingContext, suppressed);
if (clazz != null) {
return clazz;
}
}
}
if (dexElementsSuppressedExceptions != null) {
suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions));
}
return null;
}
對Element數組進行遍歷(也就是對每一個dex文件遍歷),一個dex文件有很多類,通過調用DexFile的loadClassBinaryName找到與name相同的類返回,否則為null。正是這個特性!!我們可以把布丁dex作為Element數組的首個元素。這個就可以動態修復bug了!!【MultiDex方案以及由此衍生出的QQ空間熱更新方案都是通過改變dexElements數組的元素位置來實現的】
結合圖示
image.png
三、 雙親委派機制
如何理解Android ClassLoader的雙親代理/委派機制呢?ClassLoader的loadClass方法保證了雙親委派機制,那我們先看一下這個方法:
public Class<?> loadClass(String className) throws ClassNotFoundException {
return loadClass(className, false);
}
protected Class<?> loadClass(String className, boolean resolve) throws ClassNotFoundException {
Class<?> clazz = findLoadedClass(className);//1
if (clazz == null) {
ClassNotFoundException suppressed = null;
try {
clazz = parent.loadClass(className, false);//2
} catch (ClassNotFoundException e) {
suppressed = e;
}
if (clazz == null) {
try {
clazz = findClass(className);//3
} catch (ClassNotFoundException e) {
e.addSuppressed(suppressed);
throw e;
}
}
}
return clazz;
1. 首先調用findLoadedClass看自身是否加載過該name的類文件。
2. 如果沒有,調用父ClassLoader的loadClass看是否加載過類文件。
3. 如果父classLoader也沒有加載過,表明我們這個類從來沒有沒加載過,則調用自身的findClass方法去dex文件中查找這個類。(聯系我們上一節BaseDexClassLoader的findClass方法)
雙親委派模型的工作過程為:如果一個類加載器收到了類加載的請求,它首先不會自己去嘗試加載這個類,而是把這個請求委派給父類加載器去完成,每一個層次的加載器都是如此,因此所有的類加載請求都會傳給頂層的啟動類加載器(BootClassLoader根加載器 加載器的頂端),只有當父加載器反饋自己無法完成該加載請求(該加載器的搜索范圍中沒有找到對應的類)時,子加載器才會嘗試自己去加載。
總結說:
1. 什么是雙親委派機制:
ClassLoader在加載一個字節碼時,首先會詢問 當前的
ClassLoader是否已經加載過此類,如果已經加載過就直接返回,不在重復的去
加載,如果沒有的話,會查詢它的parent是否已經加載過此類,如果加載過那
么就直接返回parent加載過的字節碼文件,如果整個繼承線路上都沒有加載過
此類,最后由子ClassLoader執行真正的加載。
2. 這樣做的好處:
如果一個類被位于樹中的任意ClassLoader節點加載過,就會緩存在內存里,那么在以后的整個系統的生命周期中這個類都不會在被重新加載,大大提高了加載類的效率。同樣還能類隔離,防止其他類冒充系統類。
3. 什么樣的類可以說是同一個類?
包名類名相同以及要被同一個類加載加載過。三個條件都滿足,才能說是同一個類。