1.類加載器ClassLoader
Android的Dalvik/ART虛擬機如同標準JAVA的JVM虛擬機一樣,在運行程序時首先需要將對應的類加載到內(nèi)存中。因此,我們可以利用這一點,在程序運行時手動加載Class,從而達到代碼動態(tài)加載可執(zhí)行文件的目的。Android的Dalvik/ART虛擬機雖然與標準Java的JVM虛擬機不一樣,ClassLoader具體的加載細節(jié)不一樣,但是工作機制是類似的,也就是說在Android中同樣可以采用類似的動態(tài)加載插件的功能,只是在Android應用中動態(tài)加載一個插件的工作要比Eclipse加載一個插件復雜許多(這點后面在解釋說明)。
2.有幾個ClassLoader實例?
動態(tài)加載的基礎是ClassLoader,從名字也可以看出,ClassLoader就是專門用來處理類加載工作的,所以這貨也叫類加載器,而且一個運行中的APP 不僅只有一個類加載器。
其實,在Android系統(tǒng)啟動的時候會創(chuàng)建一個Boot類型的ClassLoader實例,用于加載一些系統(tǒng)Framework層級需要的類,我們的Android應用里也需要用到一些系統(tǒng)的類,所以APP啟動的時候也會把這個Boot類型的ClassLoader傳進來。
此外,APP也有自己的類,這些類保存在APK的dex文件里面,所以APP啟動的時候,也會創(chuàng)建一個自己的ClassLoader實例,用于加載自己dex文件中的類。下面我們在項目里驗證看看:
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
ClassLoader classLoader = getClassLoader();
if (classLoader != null){
Log.i(TAG, "[onCreate] classLoader " + i + " : " + classLoader.toString());
while (classLoader.getParent()!=null){
classLoader = classLoader.getParent();
Log.i(TAG,"[onCreate] classLoader " + i + " : " + classLoader.toString());
}
}
}
輸出結(jié)果為:
[onCreate] classLoader 1 : dalvik.system.PathClassLoader[DexPathList[[zip file "/data/app/me.kaede.anroidclassloadersample-1/base.apk"],nativeLibraryDirectories=[/vendor/lib, /system/lib]]]
[onCreate] classLoader 2 : java.lang.BootClassLoader@14af4e32
可以看見有2個Classloader實例,一個是BootClassLoader(系統(tǒng)啟動的時候創(chuàng)建的),另一個是PathClassLoader(應用啟動時創(chuàng)建的,用于加載apk里面的類)。由此也可以看出,一個運行的Android應用至少有2個ClassLoader。
3.創(chuàng)建自己ClassLoader實例
動態(tài)加載外部的dex文件的時候,我們也可以使用自己創(chuàng)建的ClassLoader實例來加載dex里面的Class,不過ClassLoader的創(chuàng)建方式有點特殊,我們先看看它的構(gòu)造方法:
/*
* constructor for the BootClassLoader which needs parent to be null.
*/
ClassLoader(ClassLoader parentLoader, boolean nullAllowed) {
if (parentLoader == null && !nullAllowed) {
throw new NullPointerException("parentLoader == null && !nullAllowed");
}
parent = parentLoader;
}
創(chuàng)建一個ClassLoader實例的時候,需要使用一個現(xiàn)有的ClassLoader實例作為新創(chuàng)建的實例的Parent。這樣一來,一個Android應用,甚至整個Android系統(tǒng)里所有的ClassLoader實例都會被一棵樹關聯(lián)起來,這也是ClassLoader的 雙親代理模型(Parent-Delegation Model)的特點。
4.ClassLoader雙親代理模型加載類的特點和作用
Android中ClassLoader加載class的方法為:
public Class<?> loadClass(String className) throws ClassNotFoundException {
return loadClass(className, false);
}
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;
}
從源碼中我們也可以看出,loadClass方法在加載一個類的實例的時候,
1)會先查詢當前ClassLoader實例是否加載過此類,有就返回;
2)如果沒有。查詢Parent是否已經(jīng)加載過此類,如果已經(jīng)加載過,就直接返回Parent加載的類;
3)如果繼承路線上的ClassLoader都沒有加載,才由Child執(zhí)行類的加載工作;
這樣做有個明顯的特點,如果一個類被位于樹根的ClassLoader加載過,那么在以后整個系統(tǒng)的生命周期內(nèi),這個類永遠不會被重新加載。
5.使用ClassLoader一些需要注意的問題
我們都知道,我們可以通過動態(tài)加載獲得新的類,從而升級一些代碼邏輯,這里有幾個問題要注意一下。
如果你希望通過動態(tài)加載的方式,加載一個新版本的dex文件,使用里面的新類替換原有的舊類,從而修復原有類的BUG,那么你必須保證在加載新類的時候,舊類還沒有被加載,因為如果已經(jīng)加載過舊類,那么ClassLoader會一直優(yōu)先使用舊類。
如果舊類總是優(yōu)先于新類被加載,我們也可以使用一個與加載舊類的ClassLoader沒有樹的繼承關系的另一個ClassLoader來加載新類,因為ClassLoader只會檢查其Parent有沒有加載過當前要加載的類,如果兩個ClassLoader沒有繼承關系,那么舊類和新類都能被加載。
不過這樣一來又有另一個問題了,在Java中,只有當兩個實例的類名、包名以及加載其的ClassLoader都相同,才會被認為是同一種類型。上面分別加載的新類和舊類,雖然包名和類名都完全一樣,但是由于加載的ClassLoader不同,所以并不是同一種類型,在實際使用中可能會出現(xiàn)類型不符異常。
同一個Class = 相同的 ClassName + PackageName + ClassLoader
6.Android中的ClassLoader繼承關系

7.DexClassLoader 和 PathClassLoader
在Android中,ClassLoader是一個抽象類,實際開發(fā)過程中,我們一般是使用其具體的子類DexClassLoader、PathClassLoader這些類加載器來加載類的,它們的不同之處是:
1)DexClassLoader可以加載jar/apk/dex,可以從SD卡中加載未安裝的apk;
2)PathClassLoader只能加載系統(tǒng)中已經(jīng)安裝過的apk;
8.類加載器的初始化
平時開發(fā)的時候,使用DexClassLoader就夠用了,但是我們不妨挖一下這兩者具體細節(jié)上的區(qū)別。
// DexClassLoader.java
public class DexClassLoader extends BaseDexClassLoader {
public DexClassLoader(String dexPath, String optimizedDirectory,
String libraryPath, ClassLoader parent) {
super(dexPath, new File(optimizedDirectory), libraryPath, parent);
}
}
// PathClassLoader.java
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);
}
}
這兩者只是簡單的對BaseDexClassLoader做了一下封裝,具體的實現(xiàn)還是在父類里。不過這里也可以看出,PathClassLoader的optimizedDirectory只能是null,進去BaseDexClassLoader看看這個參數(shù)是干什么的。
public BaseDexClassLoader(String dexPath, File optimizedDirectory,
String libraryPath, ClassLoader parent) {
super(parent);
this.originalPath = dexPath;
this.pathList = new DexPathList(this, dexPath, libraryPath, optimizedDirectory);
}
public DexPathList(ClassLoader definingContext, String dexPath,
String libraryPath, File optimizedDirectory) {
……
this.dexElements = makeDexElements(splitDexPath(dexPath), optimizedDirectory);
}
private static Element[] makeDexElements(ArrayList<File> files,
File optimizedDirectory) {
ArrayList<Element> elements = new ArrayList<Element>();
for (File file : files) {
ZipFile zip = null;
DexFile dex = null;
String name = file.getName();
if (name.endsWith(DEX_SUFFIX)) {
dex = loadDexFile(file, optimizedDirectory);
} else if (name.endsWith(APK_SUFFIX) || name.endsWith(JAR_SUFFIX)
|| name.endsWith(ZIP_SUFFIX)) {
zip = new ZipFile(file);
}
……
if ((zip != null) || (dex != null)) {
elements.add(new Element(file, zip, dex));
}
}
return elements.toArray(new Element[elements.size()]);
}
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);
}
}
/**
* 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) {
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();
}
看到這里我們明白了,optimizedDirectory是用來緩存我們需要加載的dex文件的,并創(chuàng)建一個DexFile對象,如果它為null,那么會直接使用dex文件原有的路徑來創(chuàng)建DexFile對象。
optimizedDirectory必須是一個內(nèi)部存儲路徑,還記得我們之前說過的,無論哪種動態(tài)加載,加載的可執(zhí)行文件一定要存放在內(nèi)部存儲。DexClassLoader可以指定自己的optimizedDirectory,所以它可以加載外部的dex,因為這個dex會被復制到內(nèi)部路徑的optimizedDirectory;而PathClassLoader沒有optimizedDirectory,所以它只能加載內(nèi)部的dex,這些大都是存在系統(tǒng)中已經(jīng)安裝過的apk里面的。
9.加載類的過程
上面還只是創(chuàng)建了類加載器的實例,其中創(chuàng)建了一個DexFile實例,用來保存dex文件,我們猜想這個實例就是用來加載類的。
Android中,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);
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;
}
loadClass方法調(diào)用了findClass方法,而BaseDexClassLoader重載了這個方法,得到BaseDexClassLoader看看
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
Class clazz = pathList.findClass(name);
if (clazz == null) {
throw new ClassNotFoundException(name);
}
return clazz;
}
結(jié)果還是調(diào)用了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;
}
這里遍歷了之前所有的DexFile實例,其實也就是遍歷了所有加載過的dex文件,再調(diào)用loadClassBinaryName方法一個個嘗試能不能加載想要的類,真是簡單粗暴
public Class loadClassBinaryName(String name, ClassLoader loader) {
return defineClass(name, loader, mCookie);
}
private native static Class defineClass(String name, ClassLoader loader, int cookie);
10.Android程序比起一般Java程序在使用動態(tài)加載時麻煩在哪里
通過上面的分析,我們知道使用ClassLoader動態(tài)加載一個外部的類是非常容易的事情,所以很容易就能實現(xiàn)動態(tài)加載新的可執(zhí)行代碼的功能,但是比起一般的Java程序,在Android程序中使用動態(tài)加載主要有兩個麻煩的問題:
1)Android中許多組件類(如Activity、Service等)是需要在Manifest文件里面注冊后才能工作的(系統(tǒng)會檢查該組件有沒有注冊),所以即使動態(tài)加載了一個新的組件類進來,沒有注冊的話還是無法工作;
2)Res資源是Android開發(fā)中經(jīng)常用到的,而Android是把這些資源用對應的R.id注冊好,運行時通過這些ID從Resource實例中獲取對應的資源。如果是運行時動態(tài)加載進來的新類,那類里面用到R.id的地方將會拋出找不到資源或者用錯資源的異常,因為新類的資源ID根本和現(xiàn)有的Resource實例中保存的資源ID對不上;
說到底,拋開虛擬機的差別不說,一個Android程序和標準的Java程序最大的區(qū)別就在于他們的上下文環(huán)境(Context)不同。Android中,這個環(huán)境可以給程序提供組件需要用到的功能,也可以提供一些主題、Res等資源,其實上面說到的兩個問題都可以統(tǒng)一說是這個環(huán)境的問題,而現(xiàn)在的各種Android動態(tài)加載框架中,核心要解決的東西也正是“如何給外部的新類提供上下文環(huán)境”的問題。