ClassLoader的工作原理(Java中和Andriod中的一些區別)
早期使用過 Eclipse 等 Java 編寫的軟體的同學可能比較熟悉,Eclipse 可以載入許多第三方的外掛(或者叫擴充套件),這就是動態載入。這些外掛大多是一些 Jar 包,而使用外掛其實就是動態載入 Jar 包裡的 Class 進行工作。這其實非常好理解,Java 程式碼都是寫在 Class 裡面的,程式執行在虛擬機器上時,虛擬機器需要把需要的 Class 載入進來才能建立例項物件並工作,而完成這一個載入工作的角色就是 ClassLoader。
基本資訊
類載入器 ClassLoader
對於 Java 程式來說,編寫程式就是編寫類,執行程式也就是執行類(編譯得到的
class檔案
),其中起到關鍵作用的就是類載入器 ClassLoader。
Android 的 Dalvik/ART 虛擬機器如同標準 JAVA 的 JVM 虛擬機器一樣,在執行程式時首先需要將對應的類載入到記憶體中。因此,我們可以利用這一點,在程式執行時手動載入 Class,從而達到程式碼動態載入可執行檔案的目的。Android 的 Dalvik/ART 虛擬機器雖然與標準 Java 的 JVM 虛擬機器不一樣,ClassLoader 具體的載入細節不一樣,但是工作機制是類似的,也就是說在 Android 中同樣可以採用類似的動態載入外掛的功能,只是在 Android 應用中動態載入一個外掛的工作要比 Eclipse 載入一個外掛複雜許多(這點後面在解釋說明)。
有幾個 ClassLoader 例項?
動態載入的基礎是 ClassLoader,從名字也可以看出,ClassLoader 就是專門用來處理類載入工作的,所以這貨也叫類載入器,而且一個執行中的 APP 不僅只有一個類載入器。
其實,在 Android 系統啟動的時候會建立一個 Boot 型別的 ClassLoader 例項,用於載入一些系統 Framework 層級需要的類,我們的 Android 應用裡也需要用到一些系統的類,所以 APP 啟動的時候也會把這個 Boot 型別的 ClassLoader 傳進來。
此外,APP 也有自己的類,這些類儲存在 APK 的 dex 檔案裡面,所以 APP 啟動的時候,也會建立一個自己的 ClassLoader 例項,用於載入自己 dex 檔案中的類。下面我們在專案裡驗證看看
123456789101112 | 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()); } }} |
輸出結果為
123 | [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 : [email protected] |
可以看見有 2 個 Classloader 例項,一個是 BootClassLoader(系統啟動的時候建立的),另一個是 PathClassLoader(應用啟動時建立的,用於載入 “/data/app/me.kaede.anroidclassloadersample-1/base.apk” 裡面的類)。由此也可以看出,一個執行的 Android 應用至少有 2 個 ClassLoader。
建立自己 ClassLoader 例項
動態載入外部的 dex 檔案的時候,我們也可以使用自己建立的 ClassLoader 例項來載入 dex 裡面的 Class,不過 ClassLoader 的建立方式有點特殊,我們先看看它的構造方法
123456789 | /* * 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;} |
建立一個 ClassLoader 例項的時候,需要使用一個現有的 ClassLoader 例項作為新建立的例項的 Parent。這樣一來,一個 Android 應用,甚至整個 Android 系統裡所有的 ClassLoader 例項都會被一棵樹關聯起來,這也是 ClassLoader 的 雙親代理模型(Parent-Delegation Model)的特點。
ClassLoader 雙親代理模型載入類的特點和作用
JVM 中 ClassLoader 通過 defineClass 方法載入 jar 裡面的 Class,而 Android 中這個方法被棄用了。
12345 | protected final Class<?> defineClass(byte[] classRep, int offset, int length) throws ClassFormatError { throw new UnsupportedOperationException("can't load this type of class file");} |
取而代之的是 loadClass 方法
123456789101112131415161718192021222324252627 | 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 方法在載入一個類的例項的時候,
- 會先查詢當前 ClassLoader 例項是否載入過此類,有就返回;
- 如果沒有。查詢 Parent 是否已經載入過此類,如果已經載入過,就直接返回 Parent 載入的類;
- 如果繼承路線上的 ClassLoader 都沒有載入,才由 Child 執行類的載入工作;
這樣做有個明顯的特點,如果一個類被位於樹根的 ClassLoader 載入過,那麼在以後整個系統的生命週期內,這個類永遠不會被重新載入。
作用
首先是共享功能,一些 Framework 層級的類一旦被頂層的 ClassLoader 載入過就快取在記憶體裡面,以後任何地方用到都不需要重新載入。
除此之外還有隔離功能,不同繼承路線上的 ClassLoader 載入的類肯定不是同一個類,這樣的限制避免了使用者自己的程式碼冒充核心類庫的類訪問核心類庫包可見成員的情況。這也好理解,一些系統層級的類會在系統初始化的時候被載入,比如 java.lang.String,如果在一個應用裡面能夠簡單地用自定義的 String 類把這個系統的 String 類給替換掉,那將會有嚴重的安全問題。
使用 ClassLoader 一些需要注意的問題
我們都知道,我們可以通過動態載入獲得新的類,從而升級一些程式碼邏輯,這裡有幾個問題要注意一下。
如果你希望通過動態載入的方式,載入一個新版本的 dex 檔案,使用裡面的新類替換原有的舊類,從而修復原有類的 BUG,那麼你必須保證在載入新類的時候,舊類還沒有被載入,因為如果已經載入過舊類,那麼 ClassLoader 會一直優先使用舊類。
如果舊類總是優先於新類被載入,我們也可以使用一個與載入舊類的 ClassLoader 沒有樹的繼承關係的另一個 ClassLoader 來載入新類,因為 ClassLoader 只會檢查其 Parent 有沒有載入過當前要載入的類,如果兩個 ClassLoader 沒有繼承關係,那麼舊類和新類都能被載入。
不過這樣一來又有另一個問題了,在 Java 中,只有當兩個例項的類名、包名以及載入其的 ClassLoader 都相同,才會被認為是同一種類型。上面分別載入的新類和舊類,雖然包名和類名都完全一樣,但是由於載入的 ClassLoader 不同,所以並不是同一種類型,在實際使用中可能會出現型別不符異常。
同一個 Class = 相同的 ClassName + PackageName + ClassLoader
以上問題在採用動態載入功能的開發中容易出現,請注意。
DexClassLoader 和 PathClassLoader
在 Android 中,ClassLoader 是一個抽象類,實際開發過程中,我們一般是使用其具體的子類 DexClassLoader、PathClassLoader 這些類載入器來載入類的,它們的不同之處是:
- DexClassLoader 可以載入 jar/apk/dex,可以從 SD 卡中載入未安裝的 apk;
- PathClassLoader 只能載入系統中已經安裝過的 apk;
類載入器的初始化
平時開發的時候,使用 DexClassLoader 就夠用了,但是我們不妨挖一下這兩者具體細節上的區別。
12345678910111213141516171819 | // DexClassLoader.javapublic class DexClassLoader extends BaseDexClassLoader { public DexClassLoader(String dexPath, String optimizedDirectory, String libraryPath, ClassLoader parent) { super(dexPath, new File(optimizedDirectory), libraryPath, parent); }}// PathClassLoader.javapublic 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 做了一下封裝,具體的實現還是在父類裡。不過這裡也可以看出,PathClassLoader 的 optimizedDirectory 只能是 null,進去 BaseDexClassLoader 看看這個引數是幹什麼的
123456 | public BaseDexClassLoader(String dexPath, File optimizedDirectory, String libraryPath, ClassLoader parent) { super(parent); this.originalPath = dexPath; this.pathList = new DexPathList(this, dexPath, libraryPath, optimizedDirectory); } |
這裡建立了一個 DexPathList 例項,進去看看
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758 | 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 檔案的,並建立一個 DexFile 物件,如果它為 null,那麼會直接使用 dex 檔案原有的路徑來建立 DexFile
物件。
optimizedDirectory 必須是一個內部儲存路徑,還記得我們之前說過的,無論哪種動態載入,載入的可執行檔案一定要存放在內部儲存。DexClassLoader 可以指定自己的 optimizedDirectory,所以它可以載入外部的 dex,因為這個 dex 會被複制到內部路徑的 optimizedDirectory;而 PathClassLoader 沒有 optimizedDirectory,所以它只能載入內部的 dex,這些大都是存在系統中已經安裝過的 apk 裡面的。
載入類的過程
上面還只是建立了類載入器的例項,其中建立了一個 DexFile 例項,用來儲存 dex 檔案,我們猜想這個例項就是用來載入類的。
Android 中,ClassLoader 用 loadClass 方法來載入我們需要的類
12345678910111213141516171819202122232425 | 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 方法呼叫了 findClass 方法,而 BaseDexClassLoader 過載了這個方法,得到 BaseDexClassLoader 看看
12345678 | protected Class<?> findClass(String name) throws ClassNotFoundException { Class clazz = pathList.findClass(name); if (clazz == null) { throw new ClassNotFoundException(name); } return clazz; } |
結果還是呼叫了 DexPathList 的 findClass
123456789101112 | 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 檔案,再呼叫 loadClassBinaryName 方法一個個嘗試能不能載入想要的類,真是簡單粗暴
1234 | public Class loadClassBinaryName(String name, ClassLoader loader) { return defineClass(name, loader, mCookie);}private native static Class defineClass(String name, ClassLoader loader, int cookie); |
看到這裡想必大家都明白了,loadClassBinaryName 中呼叫了 Native 方法 defineClass 載入類。
至此,ClassLoader 的建立和載入類的過程的完成了。有趣的是,標準 JVM 中,ClassLoader 是用 defineClass 載入類的,而 Android 中 defineClass 被棄用了,改用了 loadClass 方法,而且載入類的過程也挪到了 DexFile 中,在 DexFile 中載入類的具體方法也叫 defineClass,不知道是 Google 故意寫成這樣的還是巧合。
自定義 ClassLoader
平時進行動態載入開發的時候,使用 DexClassLoader 就夠了。但我們也可以建立自己的類去繼承 ClassLoader,需要注意的是 loadClass 方法並不是 final 型別的,所以我們可以過載 loadClass 方法並改寫類的載入邏輯。
通過前面我們分析知道,ClassLoader 雙親代理的實現很大一部分就是在 loadClass 方法裡,我們可以通過重寫 loadClass 方法避開雙親代理的框架,這樣一來就可以在重新載入已經載入過的類,也可以在載入類的時候注入一些程式碼。這是一種 Hack 的開發方式,採用這種開發方式的程式穩定性可能比較差,但是卻可以實現一些 “黑科技” 的功能。
Android 程式比起一般 Java 程式在使用動態載入時麻煩在哪裡
通過上面的分析,我們知道使用 ClassLoader 動態載入一個外部的類是非常容易的事情,所以很容易就能實現動態載入新的可執行程式碼的功能,但是比起一般的 Java 程式,在 Android 程式中使用動態載入主要有兩個麻煩的問題:
- Android 中許多元件類(如 Activity、Service 等)是需要在 Manifest 檔案裡面註冊後才能工作的(系統會檢查該元件有沒有註冊),所以即使動態載入了一個新的元件類進來,沒有註冊的話還是無法工作;
- Res 資源是 Android 開發中經常用到的,而 Android 是把這些資源用對應的 R.id 註冊好,執行時通過這些 ID 從 Resource 例項中獲取對應的資源。如果是執行時動態載入進來的新類,那類裡面用到 R.id 的地方將會丟擲找不到資源或者用錯資源的異常,因為新類的資源 ID 根本和現有的 Resource 例項中儲存的資源 ID 對不上;
說到底,拋開虛擬機器的差別不說,一個 Android 程式和標準的 Java 程式最大的區別就在於他們的上下文環境(Context)不同。Android 中,這個環境可以給程式提供元件需要用到的功能,也可以提供一些主題、Res 等資源,其實上面說到的兩個問題都可以統一說是這個環境的問題,而現在的各種 Android 動態載入框架中,核心要解決的東西也正是 “如何給外部的新類提供上下文環境” 的問題。