Android ClassLoader原始碼解析
提起熱修復以及外掛化,相信大家肯定不陌生,而無論是熱修復還是外掛化,其理論依據就是Android 類載入機制。今天我們從原始碼的角度一起學習下。
簡單來講,Android中的ClassLoader主要分為BootClassLoader、PathClassLoader和DexClassLoader這三種類型。BootClassLoader:主要負責載入Android FrameWork層中的位元組碼檔案; PathClassLoader:負責載入已經安裝到系統APK檔案中的位元組碼檔案;DexClassLoader:負責載入指定目錄中的位元組碼檔案;我們先來看下其原始碼實現:
#BootClassLoader class BootClassLoader extends ClassLoader { private static BootClassLoader instance; @FindBugsSuppressWarnings("DP_CREATE_CLASSLOADER_INSIDE_DO_PRIVILEGED") public static synchronized BootClassLoader getInstance() { if (instance == null) { instance = new BootClassLoader(); } return instance; } public BootClassLoader() { super(null); } ... }
由上述程式碼可以看出,BootClassLoader 繼承自ClassLoader抽象類,實現方式為單例模式,需要注意的是BootClassLoader的訪問修飾符是預設的,只有在同一個包中才可以訪問,所以我們在應用程式中是無法直接呼叫到的。
#PathClassLoader public class PathClassLoader extends BaseDexClassLoader { public PathClassLoader(String dexPath, ClassLoader parent) { super((String)null, (File)null, (String)null, (ClassLoader)null); } public PathClassLoader(String dexPath, String librarySearchPath, ClassLoader parent) { super((String)null, (File)null, (String)null, (ClassLoader)null); } }
PathClassLoader繼承自BaseDexClassLoader ,由上述程式碼,很顯然,PathClassLoader中的方法實現都在其父類BaseDexClassLoader 中,在這裡我們分析下PathClassLoader構造方法中各個引數的含義:
dexPath:dex檔案以及包含dex的apk檔案或jar檔案的路徑集合,多個路徑用檔案分隔符分隔,預設檔案分隔符為‘:’。
librarySearchPath:所使用到的C/C++庫存放的路徑
parent:該ClassLoader所對應的父ClassLoader
#DexClassLoader public class DexClassLoader extends BaseDexClassLoader { public DexClassLoader(String dexPath, String optimizedDirectory, String librarySearchPath, ClassLoader parent) { super((String)null, (File)null, (String)null, (ClassLoader)null); } }
同樣,DexClassLoader 也是繼承自BaseDexClassLoader ,相比較PathClassLoader而言,DexClassLoader的構造方法中多了一個引數optimizedDirectory,我們看下這個引數的含義:
optimizedDirectory:Android系統將dex檔案進行優化後所生成的ODEX檔案的存放路徑,該路徑必須是一個內部儲存路徑。PathClassLoader中使用預設路徑“/data/dalvik-cache”,而DexClassLoader則需要我們指定ODEX優化檔案的存放路徑。
和Java中的ClassLoader類似,Android中的ClassLoader同樣遵循雙親委託機制。上述三種ClassLoader中,PathClassLoader的parent為BootClassLoader,DexClassLoader的parent同樣為BootClassLoader,下面我們來驗證下:
public class MainActivity extends Activity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); ClassLoader classLoader = getClassLoader(); if (classLoader != null){ Log.e("MainActivity", classLoader.toString()); while (classLoader.getParent() != null){ classLoader = classLoader.getParent(); Log.e("MainActivity", classLoader.toString()); } } Log.e("MainActivity", "------------------"); TextView mText = findViewById(R.id.tv_text); Log.e("MainActivity-TextView", mText.getClass().getClassLoader().toString()); } }
輸出日誌為:
11-24 22:34:44.205 20274-20274/com.example.administrator.mdtest E/MainActivity: dalvik.system.PathClassLoader[DexPathList[[zip file "/data/app/com.example.administrator.mdtest-2/base.apk", zip file "/data/app/com.example.administrator.mdtest-2/split_lib_dependencies_apk.apk", zip file "/data/app/com.example.administrator.mdtest-2/split_lib_slice_0_apk.apk", zip file "/data/app/com.example.administrator.mdtest-2/split_lib_slice_1_apk.apk", zip file "/data/app/com.example.administrator.mdtest-2/split_lib_slice_2_apk.apk", zip file "/data/app/com.example.administrator.mdtest-2/split_lib_slice_3_apk.apk", zip file "/data/app/com.example.administrator.mdtest-2/split_lib_slice_4_apk.apk", zip file "/data/app/com.example.administrator.mdtest-2/split_lib_slice_5_apk.apk", zip file "/data/app/com.example.administrator.mdtest-2/split_lib_slice_6_apk.apk", zip file "/data/app/com.example.administrator.mdtest-2/split_lib_slice_7_apk.apk", zip file "/data/app/com.example.administrator.mdtest-2/split_lib_slice_8_apk.apk", zip file "/data/app/com.example.administrator.mdtest-2/split_lib_slice_9_apk.apk"],nativeLibraryDirectories=[/vendor/lib, /system/lib]]] 11-24 22:34:44.205 20274-20274/com.example.administrator.mdtest E/MainActivity: java.lang.BootClassLoader@245c18f6 11-24 22:34:44.205 20274-20274/com.example.administrator.mdtest E/MainActivity: ------------------ 11-24 22:34:44.205 20274-20274/com.example.administrator.mdtest E/MainActivity-TextView: java.lang.BootClassLoader@245c18f6
由上述輸出日誌,我們不僅可以驗證,PathClassLoader的parent為BootClassLoader,同時還驗證了我們文章開始所說的應用程式的ClassLoader為PathClassLoader,FrameWork層的ClassLoader為BootClassLoader。
照例我們開啟原始碼,看下BootClassLoader是在哪裡作為parent參與構建PathClassLoader物件的:
#ClassLoader private static ClassLoader createSystemClassLoader() { String classPath = System.getProperty("java.class.path", "."); String librarySearchPath = System.getProperty("java.library.path", ""); // TODO Make this a java.net.URLClassLoader once we have those? return new PathClassLoader(classPath, librarySearchPath, BootClassLoader.getInstance()); }
沒錯,正是在ClassLoader類中的createSystemClassLoader方法中。
好了,我們既然知道Android的ClassLoader遵循雙親委託機制,那麼肯定要看下ClassLoader類中的loadClass方法了:
#ClassLoader 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; }
關於雙親委託機制,相信大家都瞭解,在這裡我就不詳細介紹了,需要注意的是,在Java中,類載入是通過defineClass方法,而在Android中,類載入則是通過findClass方法,我們跟進去findClass方法看下類載入的過程(由於BaseDexClassLoader對findClass方法進行了重寫,所以我們需要跟進到BaseDexClassLoader類中,而Android Studio中無法檢視到BaseDexClassLoader的具體原始碼,所以筆者在這裡通過原始碼線上檢視網站:ofollow,noindex">https://www.androidos.net.cn/sourcecode ):
#BaseDexClassLoader @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的findClass方法中直接呼叫到pathList的findClass方法進行類載入操作,pathList是個什麼東東呢?我們看下它的定義:
private final DexPathList pathList; public BaseDexClassLoader(String dexPath, File optimizedDirectory, String librarySearchPath, ClassLoader parent) { super(parent); this.pathList = new DexPathList(this, dexPath, librarySearchPath, null); if (reporter != null) { reporter.report(this.pathList.getDexPaths()); } }
由上述程式碼可以看到,pathList被定義為final型別,其物件是在BaseDexClassLoader的構造方法中建立的,也就是說在PathClassLoader物件建立的時候就建立了DexPathList物件,並將相應引數傳入。我們跟進去看下DexPathList的構造方法:
//定義所要載入檔案字尾 private static final String DEX_SUFFIX = ".dex"; //構造方法中傳入的ClassLoader private final ClassLoader definingContext; //Element為 DexPathList 中的內部類,其主要的成員變數為 dexFile //DexFile:dex檔案在安卓虛擬機器中的具體實現 private Element[] dexElements; public DexPathList(ClassLoader definingContext, String dexPath, String librarySearchPath, File optimizedDirectory) { 異常判斷操作... //接收classloader物件 this.definingContext = definingContext; ArrayList<IOException> suppressedExceptions = new ArrayList<IOException>(); // save dexPath for BaseDexClassLoader //重點,通過 makeDexElements 方法初始化 dexElements陣列 this.dexElements = makeDexElements(splitDexPath(dexPath), optimizedDirectory, suppressedExceptions, definingContext); ... }
我們跟進去看下makeDexElements方法的實現:
private static Element[] makeDexElements(List<File> files, File optimizedDirectory, List<IOException> suppressedExceptions, ClassLoader loader) { Element[] elements = new Element[files.size()]; int elementsPos = 0; /* * 遍歷所有的dex檔案 */ for (File file : files) { if (file.isDirectory()) {//判斷file是否為資料夾 elements[elementsPos++] = new Element(file); } else if (file.isFile()) {//判斷file是否為檔案 //獲取檔名稱 String name = file.getName(); //判斷檔名稱是否以“.dex”結尾 if (name.endsWith(DEX_SUFFIX)) { // Raw dex file (not inside a zip/jar). try { //將dex檔案轉換為DexFile物件 DexFile dex = loadDexFile(file, optimizedDirectory, loader, elements); if (dex != null) { //建立Element物件,將DexFile物件作為引數傳入, //並將該Element物件新增到elements陣列中 elements[elementsPos++] = new Element(dex, null); } } catch (IOException suppressed) { System.logE("Unable to load dex file: " + file, suppressed); suppressedExceptions.add(suppressed); } } else { DexFile dex = null; try { dex = loadDexFile(file, optimizedDirectory, loader, elements); } catch (IOException suppressed) { /* * IOException might get thrown "legitimately" by the DexFile constructor if * the zip file turns out to be resource-only (that is, no classes.dex file * in it). * Let dex == null and hang on to the exception to add to the tea-leaves for * when findClass returns null. */ 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); } } if (elementsPos != elements.length) { elements = Arrays.copyOf(elements, elementsPos); } return elements; }
由上述程式碼,我們可以知道makeDexElements方法的主要作用為:遍歷指定路徑下的所有檔案,將其中的.dex檔案轉換成DexFile物件,最終儲存到elements陣列中。
由上述分析,我們知道類載入操作最終是由pathList的findClass方法來實現的,我們繼續跟進去pathList的findClass方法看下:
#DexPathList 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; }
可以看到, DexPathList 的 findClass方法中簡單粗暴,對dexElements陣列進行遍歷,呼叫element的findClass方法來尋找當前需要的class位元組碼,簡單來講就是Android在進行類載入的時候,會遍歷我們的每一個dex檔案,來尋找所需的Class。
我們接著跟進去element的findClass方法去看下:
#Element public Class<?> findClass(String name, ClassLoader definingContext, List<Throwable> suppressed) { return dexFile != null ? dexFile.loadClassBinaryName(name, definingContext, suppressed) : null; }
可以看到,最終是呼叫到 DexFile 的loadClassBinaryName方法,我們接著跟:
#DexFile public Class loadClassBinaryName(String name, ClassLoader loader, List<Throwable> suppressed) { return defineClass(name, loader, mCookie, this, suppressed); } ... private static Class defineClass(String name, ClassLoader loader, Object cookie, DexFile dexFile, List<Throwable> suppressed) { Class result = null; try { result = defineClassNative(name, loader, cookie, dexFile); } catch (NoClassDefFoundError e) { if (suppressed != null) { suppressed.add(e); } } catch (ClassNotFoundException e) { if (suppressed != null) { suppressed.add(e); } } return result; } ... private static native Class defineClassNative(String name, ClassLoader loader, Object cookie, DexFile dexFile)
可以看到最終是通過DexFile類中的defineClassNative方法來完成所需Class的查詢。
好了,Android ClassLoader原始碼解析到這裡就結束了,歡迎大家一起交流。