android中的類加載
先看個簡單的打印日誌,直接將代碼貼到Activity#onCreate()中即可:
protected void onCreate(Bundle savedInstanceState) {
//...其他省略
Log.e(TAG, "classLoader -> " + getClassLoader());
String classPath = System.getProperty("java.class.path", ".");
String librarySearchPath = System.getProperty("java.library.path", "");
Log.e(TAG, "classPath -> " + classPath);
Log.e(TAG, "librarySearchPath -> " + librarySearchPath);
}
//打印日誌
07-11 17:42:39.299 3874-3874/com.tv189.dl E/MainActivity: classLoader -> dalvik.system.PathClassLoader[DexPathList[[zip file "/data/app/com.tv189.dl-1/base.apk"],nativeLibraryDirectories=[/data/app/com.tv189.dl-1/lib/arm64, /vendor/lib64, /system/lib64]]]
07-11 17:42:39.299 3874-3874/com.tv189.dl E/MainActivity: classPath -> .
07-11 17:42:39.299 3874-3874/com.tv189.dl E/MainActivity: librarySearchPath -> /vendor/lib64:/system/lib64
說明:
dalvik.system.PathClassLoader
爲android虛擬機類加載器
System.getProperty("java.class.path", ".")
查看當前虛擬機中加載類的路徑
System.getProperty("java.library.path", "")
查看當前虛擬機中加載的動態鏈接庫的路徑
顯然這裏android-VM中是通過PathClassLoader來加載類的。
android中類加載器的類圖如下:
涉及的類
PathClassLoader.java
PathClassLoader作用於本地文件系統中的文件列表和目錄,而不能從網絡加載classes。DexClassLoader.java
從包含classes.dex的jar/apk文件中加載class,該類可以加載未打包到apk中的代碼。所以可以用來動態加載dex等文件中的classes。
該加載器需要傳入一個應用私有的可寫入的目錄來緩存優化過的dex,如File dexOutputDir = context.getDir("dex", 0);
。BaseDexClassLoader.java
在android中使用該類加載系統classes和app中的classes。可以加載的文件類型爲包含classes的jar/apk文件,也支持多個文件同時加載,使用File.pathSeparator
把文件路徑連接即可並且android中默認爲冒號:
,如path/to/a.jar:path/to/b.apk
。dex文件實際被加在到private DexPathList pathList
,而findClass(name)
也是從pathList
中來尋找已被加載的class的。
@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;
}
- DexPathList.java
主要是類加載器把dex/resource保存在private final Element[] dexElements;
數組中的,每個Element
對應一個dex文件; 而BaseDexClassLoader#findClass
加載類時會從dexElements
中遍歷獲得,如果找到了Class就終止遍歷。
注意這一段說明
/**
* A pair of lists of entries, associated with a {@code ClassLoader}.
* One of the lists is a dex/resource path — typically referred
* to as a "class path" — list, and the other names directories
* containing native code libraries. Class path entries may be any of:
* a {@code .jar} or {@code .zip} file containing an optional
* top-level {@code classes.dex} file as well as arbitrary resources,
* or a plain {@code .dex} file (with no possibility of associated
* resources).
*
* <p>This class also contains methods to use these lists to look up
* classes and resources.</p>
*/
/*package*/ final class DexPathList {
private static final String DEX_SUFFIX = ".dex";
/** class definition context */
private final ClassLoader definingContext;
/**
* List of dex/resource (class path) elements.
* Should be called pathElements, but the Facebook app uses reflection
* to modify 'dexElements' (http://b/7726934).
*/
private final Element[] dexElements;
/** List of native library directories. */
private final File[] nativeLibraryDirectories;
解決android中方法數超出問題
由於android系統的bug,一個dex文件中方法數最大值爲65536;所以隨着應用代碼的膨脹必然會導致程序dex中的方法數超過最大值的,要解決這個問題的方案就是把dex進行切分爲多個dex,但是android5.0之前dalvik虛擬機系統只加載一個dex,從android5.0開始系統使用art虛擬機已支持加載多個dex文件;google官方也出了補丁(使用multidex加載多個dex)來修復這個bug;所以如果app要適配android5.0以下的系統程序中就要使用MultiDex方案,這個補丁支持api4 ~ api20(android5.0以下)。
涉及的類
主要類在 android.support.multidex包中
MultiDex.java
加載多個dex,主要過程:- 刪除舊的secondarydex緩存
clearOldDexDir
; - 嘗試加載已存在的secondarydexes,失敗則從新解壓apk然後加載secondarydex;
- 刪除舊的secondarydex緩存
MultiDexExtractor .java
解壓apk,並從中提取dexes
動態加載dex
參考QQ空間熱修復方案,在app啓動時把補丁dex加載到Element[]
數組的前端(參見classloader類圖);
測試時可以先修改某個類,然後把這個修改過的類轉爲jar,再把jar轉爲dex;
再把修改的內容改回去;
把下面代碼放入項目中去加載dex;
import android.content.Context;
import java.lang.reflect.Array;
import java.lang.reflect.Field;
import dalvik.system.BaseDexClassLoader;
import dalvik.system.DexClassLoader;
/**
* 加載補丁dex文件,並插入系統dex數組中的第一個位置
* <p>
* Created by June on 2017/7/14.
*/
public class PatchDexLoader {
private Context mContext;
public PatchDexLoader(Context context) {
this.mContext = context;
}
public void load(String path) {
try {
// 已加載的dex
Object dexPathList = getField(BaseDexClassLoader.class, "pathList", mContext.getClassLoader());
Object dexElements = getField(dexPathList.getClass(), "dexElements", dexPathList);
// patchdex
String dexOptDir = mContext.getDir("patchDex_optDir", 0).getAbsolutePath();
DexClassLoader dcl = new DexClassLoader(path, dexOptDir, null, mContext.getClassLoader());
Object patchDexPathList = getField(BaseDexClassLoader.class, "pathList", dcl);
Object patchDexElements = getField(patchDexPathList.getClass(), "dexElements", patchDexPathList);
// 將patchdex和已加載的dexes數組拼接連接
Object concatDexElements = concatArray(patchDexElements, dexElements);
// 重新給dexPathList#dexElements賦值
setField(dexPathList.getClass(), "dexElements", dexPathList, concatDexElements);
} catch (NoSuchFieldException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
/**
* @param cls 被訪問對象的class
* @param fieldName 對象的成員變量名
* @param object 被訪問對象
* @return
* @throws NoSuchFieldException
* @throws IllegalAccessException
*/
public Object getField(Class<?> cls, String fieldName, Object object) throws NoSuchFieldException, IllegalAccessException {
Field field = cls.getDeclaredField(fieldName);
field.setAccessible(true);
return field.get(object);
}
/**
* @param cls 被訪問對象的class
* @param fieldName 對象的成員變量名
* @param object 被訪問對象
* @param value 賦值給成員變量
* @throws NoSuchFieldException
* @throws IllegalAccessException
*/
public void setField(Class<?> cls, String fieldName, Object object, Object value) throws NoSuchFieldException, IllegalAccessException {
Field field = cls.getDeclaredField(fieldName);
field.setAccessible(true);
field.set(object, value);
}
/**
* 連接兩個數組(指定位置)
*
* @param left 連接後在新數組的左側
* @param right 連接後在新數組的右側
* @return
*/
public Object concatArray(Object left, Object right) {
int len1 = Array.getLength(left);
int len2 = Array.getLength(right);
int totalLen = len1 + len2;
Object concatArray = Array.newInstance(left.getClass().getComponentType(), totalLen);
for(int i = 0; i < len1; i++) {
Array.set(concatArray, i, Array.get(left, i));
}
for(int j = 0; j < len2; j++) {
Array.set(concatArray, len1 + j, Array.get(right, j));
}
return concatArray;
}
}