Android ClassLoader淺析
前言
最近在看Tinker的原理,發現核心是通過ClassLoader
做的,由於之前也從未接觸過ClassLoader
趁著上週末看了安卓ClassLoader
相關原始碼,這裡分享一發安卓的ClassLoader
和熱更新的實現原理。
ClassLoader
首先我們要知道,程式在執行時要把對應的類載入到記憶體,在安卓上來說就是把Dex檔案中的類載入到記憶體,這個載入流程是通過ClassLoader
實現的。因此如果我們想要動態載入自己的類,就得從ClassLoader
上做文章,那麼接下我們先看看安卓的ClassLoader
。
public abstract class ClassLoader {
private ClassLoader(Void unused, ClassLoader parent) {
this.parent = parent;
}
protected ClassLoader(ClassLoader parent) {
this(checkCreateClassLoader(), parent);
}
protected ClassLoader() {
this(checkCreateClassLoader(), getSystemClassLoader( ));
}
public static ClassLoader getSystemClassLoader() {
return SystemClassLoader.loader;
}
static private class SystemClassLoader {
public static ClassLoader loader = ClassLoader.createSystemClassLoader();
}
private static ClassLoader createSystemClassLoader () {
String classPath = System.getProperty("java.class.path", ".");
String librarySearchPath = System.getProperty("java.library.path", "");
return new PathClassLoader(classPath, librarySearchPath, BootClassLoader.getInstance());
}
public Class<?> loadClass(String name) throws ClassNotFoundException {
return loadClass(name, false);
}
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
// First, check if the class has already been loaded
Class<?> c = findLoadedClass(name);
if (c == null) {
try {
if (parent != null) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
c = findClass(name);
}
}
return c;
}
protected Class<?> findClass(String name) throws ClassNotFoundException {
throw new ClassNotFoundException(name);
}
}
可以看到在構造方法的時候我們需要傳入一個父ClassLoader
,預設沒傳情況下是以PathClassLoader
作為父載入器的。
然後載入類是呼叫的loadClass()
方法,他是先判斷是本ClassLoader
是否已經載入過,如果沒載入過就讓父載入器載入,然後父載入器看自己是否載入過,如果沒載入過讓祖父載入器載入,然後祖父載入器沒找到,父載入器在找,父載入器沒找到,子載入器再找,這也就是我們常說的雙親委派模式。
採取雙親委派模式主要有兩點好處:
- 避免重複載入,如果已經載入過一次Class,就不需要再次載入,而是先從快取中直接讀取。
- 更加安全,如果不使用雙親委託模式,就可以自定義一個String類來替代系統的String類,這顯然會造成安全隱患,採用雙親委託模式會使得系統的String類在Java虛擬機器啟動時就被載入,也就無法自定義String類來替代系統的String類,除非我們修改類載入器搜尋類的預設演算法。還有一點,只有兩個類名一致並且被同一個類載入器載入的類,Java虛擬機器才會認為它們是同一個類。
那麼下面的重點是看Android的類載入器如何重寫findClass()
方法來載入類的。
Android ClassLoader繼承關係
從上圖可以看到與ClassLoader
相關的一共有7個,大致可以分為如下3類
BootClassLoader
是ClassLoader
內部類,是Android中所有ClassLoader
的parent,可見性為包內可見我們無法使用。BaseDexClassLoader
是PathClassLoader
、DexClassLoader
、InMemoryDexClassLoader
的父類,類載入的主要邏輯都是在BaseDexClassLoader
完成的。SecureClassLoader
繼承了抽象類ClassLoader
,拓展了ClassLoader
類加入了許可權方面的功能,加強了安全性,其子類URLClassLoader
是用URL路徑從jar檔案中載入類和資源,我們用不上。
我們重點關注與我們熱更新相關的PathClassLoader
和DexClassLoader
。
PathClassLoader
只能載入已經安裝到Android系統中的apk檔案(/data/app目錄),是Android預設使用的類載入器。DexClassLoader
可以載入任意目錄下的dex/jar/apk/zip檔案,比PathClassLoader更靈活,是實現熱修復的重點。
這裡我們可以驗證下PathClassLoader
是安卓預設使用的類載入器。在Activity
的onCreate()
方法中列印它的類載入器。
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Log.i("zhuliyuan", this.getClass().getClassLoader().toString());
}
原始碼檢視(基於API28)
/**
* Provides a simple {@link ClassLoader} implementation that operates on a list
* of files and directories in the local file system, but does not attempt to
* load classes from the network. Android uses this class for its system class
* loader and for its application class loader(s).
*/
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);
}
}
/**
* 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.
*/
public class DexClassLoader extends BaseDexClassLoader {
public DexClassLoader(String dexPath, String optimizedDirectory,
String librarySearchPath, ClassLoader parent) {
super(dexPath, null, librarySearchPath, parent);
}
}
從註釋上可以看出PathClassLoader
是用於系統類和應用程式類的載入,DexClassLoader
可以用來載入任意目錄的dex。具體實現還得看BaseDexClassLoader
的構造方法。
public class BaseDexClassLoader extends ClassLoader {
private final DexPathList pathList;
public BaseDexClassLoader(String dexPath, File optimizedDirectory,
String librarySearchPath, ClassLoader parent) {
this(dexPath, optimizedDirectory, librarySearchPath, parent, false);
}
public BaseDexClassLoader(String dexPath, File optimizedDirectory,
String librarySearchPath, ClassLoader parent, boolean isTrusted) {
super(parent);
this.pathList = new DexPathList(this, dexPath, librarySearchPath, null, isTrusted);
...
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
...
Class c = pathList.findClass(name, suppressedExceptions);
...
return c;
}
@Override
protected URL findResource(String name) {
return pathList.findResource(name);
}
@Override
public String findLibrary(String name) {
return pathList.findLibrary(name);
}
}
可以看出在構造方法中建立了一個DexPathList
物件賦值給了pathList
欄位,然後findxxx()
方法都是從DexPathList
中查詢。
BaseDexClassLoader
的建構函式包含四個引數:
- dexPath:包含類和資源的jar / apk檔案列表,由
File.pathSeparator
分隔,在Android上預設為:。 - optimizedDirectory:由於dex檔案被包含在APK或者Jar檔案中,因此在裝載目標類之前需要先從APK或Jar檔案中解壓出dex檔案,該引數就是制定解壓出的dex 檔案存放的路徑。這也是對apk中dex根據平臺進行ODEX優化的過程。自API26開始無效。
- librarySearchPath:指目標類中所使用的C/C++庫存放的路徑,可以為null。
- parent:父
ClassLoader
引用。
接下來我們檢視DexPathList
的構造方法和findxxx()
方法。
final class DexPathList {
private Element[] dexElements;
DexPathList(ClassLoader definingContext, String dexPath,
String librarySearchPath, File optimizedDirectory, boolean isTrusted) {
...
// 載入dexPath路徑下的dex和resource
this.dexElements = makeDexElements(splitDexPath(dexPath), optimizedDirectory,
suppressedExceptions, definingContext, isTrusted);
...
}
private static Element[] makeDexElements(List<File> files, File optimizedDirectory,
List<IOException> suppressedExceptions, ClassLoader loader, boolean isTrusted) {
Element[] elements = new Element[files.size()];
int elementsPos = 0;
for (File file : files) {
if (file.isDirectory()) {
elements[elementsPos++] = new Element(file);
} else if (file.isFile()) {
String name = file.getName();
DexFile dex = null;
if (name.endsWith(DEX_SUFFIX)) {
// Raw dex file (not inside a zip/jar).
try {
dex = loadDexFile(file, optimizedDirectory, loader, elements);
if (dex != null) {
elements[elementsPos++] = new Element(dex, null);
}
} catch (IOException suppressed) {
System.logE("Unable to load dex file: " + file, suppressed);
suppressedExceptions.add(suppressed);
}
} else {
try {
dex = loadDexFile(file, optimizedDirectory, loader, elements);
} catch (IOException suppressed) {
suppressedExceptions.add(suppressed);
}
if (dex == null) {
elements[elementsPos++] = new Element(file);
} else {
elements[elementsPos++] = new Element(dex, file);
}
}
} else {
System.logW("ClassLoader referenced unknown path: " + file);
}
}
return elements;
}
}
構造方法中呼叫makeDexElements()
方法獲取到了Element[]
陣列賦值給了dexElements
變數。
public Class<?> findClass(String name, List<Throwable> suppressed) {
for (Element element : dexElements) {
Class<?> clazz = element.findClass(name, definingContext, suppressed);
if (clazz != null) {
return clazz;
}
}
if (dexElementsSuppressedExceptions != null) {
suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions));
}
return null;
}
而findClass()
方法則是變數在構造方法初始化好的Element[]
陣列中從前往後遍歷找到我們需要的類。
這裡我們總結下類載入的過程
PathClassLoader
和DexClassLoader
呼叫了父類BaseDexClassLoader
的構造方法。BaseDexClassLoader
在構造方法中建立了DexPathList
物件並賦值給pathList
欄位,載入類的findxxx()
方法都是呼叫DexPathList
類的findxxx()
方法來實現內的載入。DexPathList
在構造方法中呼叫makeDexElements()
方法建立了Element[]
陣列賦值給dexElements
欄位,findClass()
方法就是從前往後遍歷Element[]
陣列找到我們要的class.
熱更新實現原理
上面已經分析了類的載入流程,那麼要想實現熱更新我們需要拿到PathClassLoader
中的DexPathList
物件然後在拿到他當中的Element[]
陣列將我們已經修復的Element
插入到陣列的前面,這樣在類載入時候就會先拿到我們已經修復的class了達到動態替換的效果。
載入我們自己的dex檔案需要用到DexClassLoader
,然後通過反射取出DexClassLoader
中的DexPathList
物件中的Element[]
陣列插入到PathClassLoader
中Element[]
陣列前面即可。
熱更新實際例子我會在另一篇Android 手動實現熱更新介紹。