Android類載入機制 Dalvik虛擬機如同其他Java虛擬機一樣,在運行程式時首先需要將對應的類載入到記憶體中。而在Java標準的虛擬機中,類載入可以從class文件中讀取,也可以是其他形式的二進位流。因此,我們常常利用這一點,在程式運行時手動載入Class,從而達到代碼動態載入執行的目的。只不 ...
Android類載入機制
Dalvik虛擬機如同其他Java虛擬機一樣,在運行程式時首先需要將對應的類載入到記憶體中。而在Java標準的虛擬機中,類載入可以從class文件中讀取,也可以是其他形式的二進位流。因此,我們常常利用這一點,在程式運行時手動載入Class,從而達到代碼動態載入執行的目的。
只不過Android平臺上虛擬機運行的是Dex位元組碼,一種對class文件優化的產物,傳統Class文件是一個Java源碼文件會生成一個.class文件,而Android是把所有Class文件進行合併,優化,然後生成一個最終的class.dex,目的是把不同class文件重覆的東西只需保留一份,如果我們的Android應用不進行分dex處理,最後一個應用的apk只會有一個dex文件。
首先看一下Android平臺中幾個常用的載入相關的類,以及他們的繼承關係。
- PathClassLoader是用來載入Android系統類和應用的類。
- DexClassLoader支持載入APK、DEX和JAR,也可以從SD卡進行載入。
ClassLoader類的主要職責:根據一個指定的類的名稱,找到或者生成其對應的位元組代碼,然後從這些位元組代碼中定義出一個 Java 類,即 java.lang.Class
類的一個實例。 (還可以載入圖片等,這裡不討論)
ClassLoader中的主要方法及含義
下麵這段代碼時loadClass的實現,可以看出首先會查找載入類是否已經被載入了,如果是直接返回。否則,通過findClass()查找
1 protected Class<?> loadClass(String name, boolean resolve) 2 throws ClassNotFoundException 3 { 4 // First, check if the class has already been loaded 5 Class c = findLoadedClass(name); 6 if (c == null) { 7 // If still not found, then invoke findClass in order 8 // to find the class. 9 c = findClass(name); 10 // this is the defining class loader; record the stats 11 } 12 return c; 13 }
在BaseClassLoader中的findClass函數:實際是在一個pathList中去查找這個類。
1 protected Class<?> findClass(String name) throws ClassNotFoundException { 2 List<Throwable> suppressedExceptions = new ArrayList<Throwable>(); 3 Class c =pathList.findClass
(name, suppressedExceptions); 4 if (c == null) { 5 ClassNotFoundException cnfe = new ClassNotFoundException("Didn't find class \"" + name + "\" on path: " + pathList); 6 for (Throwable t : suppressedExceptions) { 7 cnfe.addSuppressed(t); 8 } 9 throw cnfe; 10 } 11 return c; 12 }
DexPathList的源碼可知,成員變數dexElements用來保存dex數組,而每個dex文件其實就是DexFile對象。遍歷dexElements,然後通過DexFile去載入class文件,載入成功就返回,否則返回null,看到這裡應該基本知道我們想幹啥了,我打算在dexElements上面做手腳,可以通過反射來載入。
1 /*package*/ final class DexPathList { 2 private static final String DEX_SUFFIX = ".dex"; 3 private static final String JAR_SUFFIX = ".jar"; 4 private static final String ZIP_SUFFIX = ".zip"; 5 private static final String APK_SUFFIX = ".apk"; 6 7 /** class definition context */ 8 private final ClassLoader definingContext; 9 10 /** 11 * List of dex/resource (class path) elements. 12 * Should be called pathElements, but the Facebook app uses reflection 13 * to modify 'dexElements' (http://b/7726934). 14 */ 15 private final Element[] dexElements; 16 17 /** 18 * Finds the named class in one of the dex files pointed at by 19 * this instance. This will find the one in the earliest listed 20 * path element. If the class is found but has not yet been 21 * defined, then this method will define it in the defining 22 * context that this instance was constructed with. 23 * 24 * @param name of class to find 25 * @param suppressed exceptions encountered whilst finding the class 26 * @return the named class or {@code null} if the class is not 27 * found in any of the dex files 28 */ 29 public Class findClass(String name, List<Throwable> suppressed) { 30 for (Element element : dexElements) { 31 DexFile dex = element.dexFile; 32 33 if (dex != null) { 34 Class clazz = dex.loadClassBinaryName(name, definingContext, suppressed); 35 if (clazz != null) { 36 return clazz; 37 } 38 } 39 } 40 if (dexElementsSuppressedExceptions != null) { 41 suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions)); 42 } 43 return null; 44 } 45 }View Code
實際程式運行時我們只需要將本地載入的dex文件插入到DexPathList中的dexElements中,後續程式運行時就會自動到該變數中去查找新的類,這就是該篇講的熱修複的原理。
補丁包dex文件生成
如果某個APP遠程出現bug,那麼開發者如何生成一個新的dex文件,然後通過網路下發到客戶端呢?
1.到該android sdk的該目錄下:(不一定是23.0.1也可以是其它版本號)
2. 將要生成的dex的class文件(全路徑)放到該目錄下(對應上圖中的com文件夾)。class文件可以從Android Studio的該目錄拷貝:
3. 運行如下命令即可根據MyTestClass.class文件本地生成補丁包: patch.dex
.\dx.bat --dex --output=patch.dex .\com\xxx\xxx\hotfix_qqzone\MyTestClass.class
通過反射將本地dex註入
前面已經介紹了ClassLoader的一些介面和DexPathList。下麵將介紹如何一步一步將本地(可能是通過網路從服務端獲取的)dex文件動態載入到記憶體中。
1. 通過反射獲取dexElements
1 private static Object getDexElementByClassLoader(ClassLoader classLoader) throws Exception { 2 Class<?> classLoaderClass = Class.forName("dalvik.system.BaseDexClassLoader"); 3 Field pathListField = classLoaderClass.getDeclaredField("pathList"); 4 pathListField.setAccessible(true); 5 Object pathList = pathListField.get(classLoader); 6 7 Class<?> pathListClass = pathList.getClass(); 8 Field dexElementsField = pathListClass.getDeclaredField("dexElements"); 9 dexElementsField.setAccessible(true); 10 Object dexElements = dexElementsField.get(pathList); 11 12 return dexElements; 13 }
2. 獲取本地dex補丁包,生成dexElement,併合併到已有的dexElements中(通過combineArray函數)
1 public static void loadFixedDex(Context context,String path){ 2 3 String optimizeDir = context.getDir("odex",Context.MODE_PRIVATE)+File.separator+"opt_dex"; 4 File fopt = new File(optimizeDir); 5 if(!fopt.exists()){ 6 fopt.mkdirs(); 7 } 8 //1.載入應用程式的dex 9 try { 10 PathClassLoader pathLoader = (PathClassLoader) context.getClassLoader(); 11 //2.載入指定的修複的dex文件。 12 DexClassLoader classLoader = new DexClassLoader( 13 path,//String dexPath, 14 fopt.getAbsolutePath(),//String optimizedDirectory, 15 null,//String libraryPath, 16 pathLoader//ClassLoader parent 17 ); 18 //3.合併 19 Object path_pathList = getPathList(pathLoader); 20 Object dex_pathList = getPathList(classLoader); 21 Object path_DexElements = getDexElements(path_pathList); 22 Object dex_DexElements = getDexElements(dex_pathList); 23 //合併完成 24 Object dexElements = combineArray(dex_DexElements,path_DexElements); 25 //重寫給PathList裡面的lement[] dexElements;賦值 26 Object pathList = getPathList(pathLoader); 27 setField(pathList,pathList.getClass(),"dexElements",dexElements); 28 29 } catch (Exception e) { 30 e.printStackTrace(); 31 } 32 } 33
private static void setField(Object obj,Class<?> cl, String field, Object value) throws Exception {
Field localField = cl.getDeclaredField(field);
localField.setAccessible(true);
localField.set(obj,value);
}
問題1:
假設class.dex中有一個bug.class出現了bug,現在希望獲通過一個fixbug.class生成了一個補丁包patch.dex。 如果我們APP初始化的時候就載入pathc.dex 不會出現問題,下次調用bug.class會優先使用patch.dex中的bug.class而不是.
總結: 當代碼中已經調用了有問題的類,而沒有載入patch.dex,後續載入將不會再起作用。
問題2: CLASS_ISPREVERIFIED標記問題
如果待修複的類bug.class中沒有引用到class.dex中其它類,則bug.class不會被打上該標記,熱修複不會出現問題。
如果bug.class中引用了class.dex 情況。 // 後續有時間再補充。
實際業務使用場景:
項目本地有一套代碼邏輯,當本地代碼執行失敗後,會從服務端獲取補丁包(.dex文件)。因為提前知道補丁包的類名和介面,所以通過DexClassLoader 將補丁信息載入成一個類,然後再通過反射構造一個該類的對象。同時可以通過反射調用類裡面相關的介面了,這樣就相當於對本地的代碼進行了替換也是一種修複過程。
Class<?> clazz; DexClassLoader dexClassLoader = new DexClassLoader(patchSaveDir + patchName, dataDir, "", getClass().getClassLoader()); clazz = dexClassLoader.loadClass(className); Constructor constructor = clazz.getDeclaredConstructor(); Object object = constructor.newInstance();
自此,簡單的類載入機制和熱修複就介紹完畢了!
參考:
https://www.ibm.com/developerworks/cn/java/j-lo-classloader/
https://www.jianshu.com/p/a620e368389a