Android熱修復學習(二)
Multidex
在上一篇(一)中,我們知道應用載入class會用到PathClassLoader,最終會呼叫DexPathList的findClass()
方法,通過輪詢Element陣列用DexFile來載入類。而每個Element又對應一個dex單元(檔案),所以我們可以改變這個陣列的大小和順序做一些動態功能。前面我們看到這個陣列的初始化是通過呼叫DexPathList的makeDexElements()
來生成,如果我們拿到DexPathList的例項,則可以反射呼叫該方法來新增Element。
當 Android 系統安裝一個應用的時候,有一步是對 Dex 進行優化,這個過程有一個專門的工具來處理,叫 DexOpt。DexOpt 是在第一次載入 Dex 檔案的時候執行的。這個過程會生成一個 ODEX 檔案,即 Optimised Dex。執行 ODEX 的效率會比直接執行 Dex 檔案的效率要高很多。 但是在早期的 Android 系統中,DexOpt 有兩個問題。 (一):DexOpt 會把每一個類的方法 id 檢索起來,存在一個連結串列結構裡面,但是這個連結串列的長度是用一個 short 型別來儲存的,導致了方法 id 的數目不能夠超過65536個。當一個專案足夠大的時候,顯然這個方法數的上限是不夠的。(二):Dexopt 使用 LinearAlloc 來儲存應用 的方法資訊。Dalvik LinearAlloc 是一個固定大小的緩衝區。在Android 版本的歷史上,LinearAlloc 分別經歷了4M/5M/8M/16M限制。 Android 2.2和2.3的緩衝區只有5MB,Android 4.x提高到了8MB 或16MB。當方法數量過多導致超出緩衝區大小時,也會造成dexopt崩潰。 儘管在新版本的 Android 系統中,DexOpt 修復了方法數65K的限制問題,並且擴大了 LinearAlloc 限制,但是我們仍然需要對低版本的Android 系統做相容。
針對上訴的問題,google官方推出了multidex包做相容。下面我們來看multidex包是怎麼做的,它提供了三種接入方式,不過最終都是在Application的attachBaseContext()
方法中呼叫MultiDex的install()
方法。
public static void install(Context context) {
Log.i(TAG, "install");
if (IS_VM_MULTIDEX_CAPABLE) {
Log.i(TAG, "VM has multidex support, MultiDex support library is disabled." );
return;
}
if (Build.VERSION.SDK_INT < MIN_SDK_VERSION) {
throw new RuntimeException("Multi dex installation failed. SDK " + Build.VERSION.SDK_INT
+ " is unsupported. Min SDK version is " + MIN_SDK_VERSION + ".");
}
try {
ApplicationInfo applicationInfo = getApplicationInfo(context);
if (applicationInfo == null) {
return;
}
synchronized (installedApk) {
String apkPath = applicationInfo.sourceDir;
if (installedApk.contains(apkPath)) {
return;
}
installedApk.add(apkPath);
……
……
ClassLoader loader;
try {
loader = context.getClassLoader();
} catch (RuntimeException e) {
return;
}
if (loader == null) {
return;
}
……
……
File dexDir = new File(applicationInfo.dataDir, SECONDARY_FOLDER_NAME);
List<File> files = MultiDexExtractor.load(context, applicationInfo, dexDir, false);
if (checkValidZipFiles(files)) {
installSecondaryDexes(loader, dexDir, files);
} else {
files = MultiDexExtractor.load(context, applicationInfo, dexDir, true);
if (checkValidZipFiles(files)) {
installSecondaryDexes(loader, dexDir, files);
} else {
throw new RuntimeException("Zip files were not valid.");
}
}
}
} catch (Exception e) {
throw new RuntimeException("Multi dex installation failed (" + e.getMessage() + ").");
}
}
其中IS_VM_MULTIDEX_CAPABLE是通過虛擬機器的版本號來判斷的。
private static final int VM_WITH_MULTIDEX_VERSION_MAJOR = 2;
private static final int VM_WITH_MULTIDEX_VERSION_MINOR = 1;
private static final int MIN_SDK_VERSION = 4;
static boolean isVMMultidexCapable(String versionString) {
boolean isMultidexCapable = false;
if (versionString != null) {
Matcher matcher = Pattern.compile("(\\d+)\\.(\\d+)(\\.\\d+)?").matcher(versionString);
if (matcher.matches()) {
try {
int major = Integer.parseInt(matcher.group(1));
int minor = Integer.parseInt(matcher.group(2));
isMultidexCapable = (major > VM_WITH_MULTIDEX_VERSION_MAJOR)
|| ((major == VM_WITH_MULTIDEX_VERSION_MAJOR)
&& (minor >= VM_WITH_MULTIDEX_VERSION_MINOR));
} catch (NumberFormatException e) {
// let isMultidexCapable be false
}
}
}
return isMultidexCapable;
}
可以看出當虛擬機器的版本號大於等於2.1的時候,虛擬機器本身已經支援多dex載入了,而主版本號2表示art,所以說基本上從5.0開始google已經解決了這個問題。而支援的最小版本MIN_SDK_VERSION為4,所以最小隻支援到1.6。
接下來獲取應用本身資訊ApplicationInfo,通過同步處理和把包路徑放入列表中防止多次處理。然後通過方法MultiDexExtractor.load()
開始載入apk中的其它dex檔案,並且每個都以zip存放的形式拷貝到指定的資料夾下。可以看到該資料夾路徑為:
private static final String SECONDARY_FOLDER_NAME = "code_cache" + File.separator +"secondary-dexes";
File dexDir = new File(applicationInfo.dataDir, SECONDARY_FOLDER_NAME);
而applicationInfo.dataDir為應用本身的內部路徑,dataDir為分配給此Application的存放資料的位置,通常是/data/data/packageName/。通過該路徑我們可以通過DexClassLoader來載入,前面(一)中說過,DexClassLoader載入類儲存優化後的檔案路徑必須是應用使用者自己的。我們來看multidex又是怎麼做的。
private static void installSecondaryDexes(ClassLoader loader, File dexDir, List<File> files)
throws IllegalArgumentException, IllegalAccessException, NoSuchFieldException,
InvocationTargetException, NoSuchMethodException, IOException {
if (!files.isEmpty()) {
if (Build.VERSION.SDK_INT >= 19) {
V19.install(loader, files, dexDir);
} else if (Build.VERSION.SDK_INT >= 14) {
V14.install(loader, files, dexDir);
} else {
V4.install(loader, files);
}
}
}
private static final class V19 {
private static void install(ClassLoader loader, List<File> additionalClassPathEntries,
File optimizedDirectory)throws IllegalArgumentException, IllegalAccessException,
NoSuchFieldException, InvocationTargetException, NoSuchMethodException {
Field pathListField = findField(loader, "pathList");
Object dexPathList = pathListField.get(loader);
ArrayList<IOException> suppressedExceptions = new ArrayList<IOException>();
expandFieldArray(dexPathList, "dexElements", makeDexElements(dexPathList,
new ArrayList<File>(additionalClassPathEntries), optimizedDirectory,
suppressedExceptions));
if (suppressedExceptions.size() > 0) {
for (IOException e : suppressedExceptions) {
Log.w(TAG, "Exception in makeDexElement", e);
}
Field suppressedExceptionsField =
findField(loader, "dexElementsSuppressedExceptions");
IOException[] dexElementsSuppressedExceptions =
(IOException[]) suppressedExceptionsField.get(loader);
if (dexElementsSuppressedExceptions == null) {
dexElementsSuppressedExceptions =
suppressedExceptions.toArray(
new IOException[suppressedExceptions.size()]);
} else {
IOException[] combined =
new IOException[suppressedExceptions.size() +
dexElementsSuppressedExceptions.length];
suppressedExceptions.toArray(combined);
System.arraycopy(dexElementsSuppressedExceptions, 0, combined,
suppressedExceptions.size(), dexElementsSuppressedExceptions.length);
dexElementsSuppressedExceptions = combined;
}
suppressedExceptionsField.set(loader, dexElementsSuppressedExceptions);
}
}
private static Object[] makeDexElements(
Object dexPathList, ArrayList<File> files, File optimizedDirectory,
ArrayList<IOException> suppressedExceptions)
throws IllegalAccessException, InvocationTargetException,
NoSuchMethodException {
Method makeDexElements =
findMethod(dexPathList, "makeDexElements", ArrayList.class, File.class,
ArrayList.class);
return (Object[]) makeDexElements.invoke(dexPathList, files, optimizedDirectory,
suppressedExceptions);
}
}
從上面可以看出multidex會根據不同的版本來做不同的處理,以V19為例,可以看出是通過獲取到當前應用的classloader,即為PathClassLoader,然後通過反射獲取到他的DexPathList屬性物件pathList,然後繼續通過反射呼叫pathList的makeDexElements()
方法把之前拷貝的其它dex的zip檔案轉化為Element[],然後合併到pathList現有的陣列中去,這樣就完成了其它dex的動態載入。V14和V19類似,而 V4中沒有Element型別,則直接採用DexFile的loadloadDex(String sourcePathName, String outputPathName,int flags)
方法來把zip檔案轉化為DexFile[],從(一)中我們可以知道DexClassLoader最終也會走到該方法,而且第二個引數為我們上面說的dataDir路徑下,為應用本身的路徑。
小結:
1.雖然multidex可以很方便的解決64k的問題,不過可以看出它是在應用啟動時主執行緒來載入的,而且第一次載入dex檔案,會觸發dexopt,首次啟動可能會造成應用啟動緩慢。所以可能需要考慮非同步載入和按需動態載入。
2.multidex拆分dex為自己分析的過程,從參考文章看出可能會出現啟動必要的類沒有打包進主dex檔案的情況,所以可能需要自己干預拆分dex的過程(通過–multi-dex、–main-dex-list=、–minimal-main-dex、–set-max-idx-number等引數)