【分析】多dex載入機制
阿新 • • 發佈:2019-01-22
相關文章連結:
---------------------------------------------------------------------------------
Android原始碼版本:5.0.2_r1
下面是多dex載入的時序圖:
Android專案有兩種方式支援多dex:
1. 專案中的Application類繼承MultiDexApplication。
2. 在自己的Application類的attachBaseContext方法中呼叫MultiDex.install(this);。
我從MultiDexApplication這個類開始分析。
MultiDexApplication類繼承了Application,並重載了attachBaseContext方法,在這個方法中呼叫了MultiDex.install(this); 。
public class MultiDexApplication extends Application {
@Override
protected void attachBaseContext(Context base) {
super.attachBaseContext(base);
MultiDex.install(this);
}
}
MultiDex.install方法:
/** * Patches the application context class loader by appending extra dex files * loaded from the application apk. This method should be called in the * attachBaseContext of your {@link Application}, see * {@link MultiDexApplication} for more explanation and an example. * * @param context application context. * @throws RuntimeException if an error occurred preventing the classloader * extension. */ public static void install(Context context) { Log.i(TAG, "install"); ...... try { ApplicationInfo applicationInfo = getApplicationInfo(context); if (applicationInfo == null) { // Looks like running on a test Context, so just return without patching. return; } synchronized (installedApk) { String apkPath = applicationInfo.sourceDir; // installedApk的型別是:Set<String>。 // 如果這個apk已經安裝,則不重複安裝。 if (installedApk.contains(apkPath)) { return; } installedApk.add(apkPath); ...... // 類載入器應該直接或間接繼承於BaseDexClassLoader。 // 修改BaseDexClassLoader類中的DexPathList pathList欄位,追加額外的DEX檔案項。 /* The patched class loader is expected to be a descendant of * dalvik.system.BaseDexClassLoader. We modify its * dalvik.system.DexPathList pathList field to append additional DEX * file entries. */ ClassLoader loader; ...... // dex將會輸出到SECONDARY_FOLDER_NAME目錄。 File dexDir = new File(applicationInfo.dataDir, SECONDARY_FOLDER_NAME); List<File> files = MultiDexExtractor.load(context, applicationInfo, dexDir, false); // 校驗這些zip檔案是否合法。 if (checkValidZipFiles(files)) { // 安裝提取出來的zip檔案。 installSecondaryDexes(loader, dexDir, files); } else { Log.w(TAG, "Files were not valid zip files. Forcing a reload."); // 最後一個引數是true,代表強制載入。 // Try again, but this time force a reload of the zip file. files = MultiDexExtractor.load(context, applicationInfo, dexDir, true); // 校驗這些zip檔案是否合法。 if (checkValidZipFiles(files)) { // 安裝提取出來的zip檔案。 installSecondaryDexes(loader, dexDir, files); } else { // Second time didn't work, give up throw new RuntimeException("Zip files were not valid."); } } } } catch (Exception e) { Log.e(TAG, "Multidex installation failure", e); throw new RuntimeException("Multi dex installation failed (" + e.getMessage() + ")."); } Log.i(TAG, "install done"); }
MultiDexExtractor.load獲得/匯出apk中多出的dex,這些dex匯出後會被打包成zip檔案:
/** * 提取/獲得apk中多dex的提取zip檔案。 * 如果不是載入已經存在的檔案的情況,則還要儲存apk的資訊:時間戳、crc值、apk中dex的總個數。 * * Extracts application secondary dexes into files in the application data * directory. * * @return a list of files that were created. The list may be empty if there * are no secondary dex files. * @throws IOException if encounters a problem while reading or writing * secondary dex files */ static List<File> load(Context context, ApplicationInfo applicationInfo, File dexDir, boolean forceReload) throws IOException { Log.i(TAG, "MultiDexExtractor.load(" + applicationInfo.sourceDir + ", " + forceReload + ")"); final File sourceApk = new File(applicationInfo.sourceDir); long currentCrc = getZipCrc(sourceApk); List<File> files; // isModified方法判斷apk是否被修改過。 if (!forceReload && !isModified(context, sourceApk, currentCrc)) { try { // 載入已經存在的檔案,如果有的檔案不存在,或者不是zip檔案,則會丟擲異常。 files = loadExistingExtractions(context, sourceApk, dexDir); } catch (IOException ioe) { Log.w(TAG, "Failed to reload existing extracted secondary dex files," + " falling back to fresh extraction", ioe); // 從apk中提取出多dex,然後將這些dex逐個打包為zip檔案,最終返回提取出來的zip檔案列表。 files = performExtractions(sourceApk, dexDir); // getTimeStamp方法中呼叫的是sourceApk.lastModified()方法。 // putStoredApkInfo方法儲存apk的資訊:時間戳、crc值、apk中dex的總個數。 putStoredApkInfo(context, getTimeStamp(sourceApk), currentCrc, files.size() + 1); } } else { Log.i(TAG, "Detected that extraction must be performed."); // 這裡的performExtractions和putStoredApkInfo同上。 files = performExtractions(sourceApk, dexDir); putStoredApkInfo(context, getTimeStamp(sourceApk), currentCrc, files.size() + 1); } Log.i(TAG, "load found " + files.size() + " secondary dex files"); return files; }
MultiDexExtractor.performExtractions方法:
/**
* 從apk中提取出多dex,然後將這些dex逐個打包為zip檔案。
* @param sourceApk apk檔案。
* @param dexDir 輸出目錄。
* @return 提取出來的zip檔案列表。
*/
private static List<File> performExtractions(File sourceApk, File dexDir)
throws IOException {
final String extractedFilePrefix = sourceApk.getName() + EXTRACTED_NAME_EXT;
// 如果檔案沒有正確的字首,則刪除。
// Ensure that whatever deletions happen in prepareDexDir only happen if the zip that
// contains a secondary dex file in there is not consistent with the latest apk. Otherwise,
// multi-process race conditions can cause a crash loop where one process deletes the zip
// while another had created it.
prepareDexDir(dexDir, extractedFilePrefix);
List<File> files = new ArrayList<File>();
final ZipFile apk = new ZipFile(sourceApk);
try {
int secondaryNumber = 2;
ZipEntry dexFile = apk.getEntry(DEX_PREFIX + secondaryNumber + DEX_SUFFIX);
while (dexFile != null) {
// 輸出的檔名。
String fileName = extractedFilePrefix + secondaryNumber + EXTRACTED_SUFFIX;
// 輸出的檔案。
File extractedFile = new File(dexDir, fileName);
files.add(extractedFile);
Log.i(TAG, "Extraction is needed for file " + extractedFile);
int numAttempts = 0;
boolean isExtractionSuccessful = false;
while (numAttempts < MAX_EXTRACT_ATTEMPTS && !isExtractionSuccessful) {
numAttempts++;
// 提取apk中的多dex檔案,然後打包成一個zip檔案。
// Create a zip file (extractedFile) containing only the secondary dex file
// (dexFile) from the apk.
extract(apk, dexFile, extractedFile, extractedFilePrefix);
// 驗證提取的檔案是否是一個zip檔案。
// Verify that the extracted file is indeed a zip file.
isExtractionSuccessful = verifyZipFile(extractedFile);
// Log the sha1 of the extracted zip file
Log.i(TAG, "Extraction " + (isExtractionSuccessful ? "success" : "failed") +
" - length " + extractedFile.getAbsolutePath() + ": " +
extractedFile.length());
if (!isExtractionSuccessful) {
// Delete the extracted file
extractedFile.delete();
if (extractedFile.exists()) {
Log.w(TAG, "Failed to delete corrupted secondary dex '" +
extractedFile.getPath() + "'");
}
}
}
if (!isExtractionSuccessful) {
throw new IOException("Could not create zip file " +
extractedFile.getAbsolutePath() + " for secondary dex (" +
secondaryNumber + ")");
}
secondaryNumber++;
dexFile = apk.getEntry(DEX_PREFIX + secondaryNumber + DEX_SUFFIX);
}
} finally {
try {
apk.close();
} catch (IOException e) {
Log.w(TAG, "Failed to close resource", e);
}
}
return files;
}
MultiDexExtractor.putStoredApkInfo方法:
private static void putStoredApkInfo(Context context, long timeStamp, long crc,
int totalDexNumber) {
SharedPreferences prefs = getMultiDexPreferences(context);
SharedPreferences.Editor edit = prefs.edit();
edit.putLong(KEY_TIME_STAMP, timeStamp); // 時間戳
edit.putLong(KEY_CRC, crc); // crc值。
/* SharedPreferences.Editor doc says that apply() and commit() "atomically performs the
* requested modifications" it should be OK to rely on saving the dex files number (getting
* old number value would go along with old crc and time stamp).
*/
edit.putInt(KEY_DEX_NUMBER, totalDexNumber); // dex總個數。
apply(edit);
}
MultiDex.installSecondaryDexes方法,對dex進行安裝:
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);
}
}
}
這個方法的程式碼非常簡單,挑選V19.install分析:
/**
* 安裝多dex。
* @param loader
* @param additionalClassPathEntries zip檔案列表,這些zip檔案中都只有一個檔案classes.dex。
* @param optimizedDirectory 優化的dex存放的目錄。
*/
private static void install(ClassLoader loader, List<File> additionalClassPathEntries,
File optimizedDirectory)
throws IllegalArgumentException, IllegalAccessException,
NoSuchFieldException, InvocationTargetException, NoSuchMethodException {
// 被打補丁的類載入器應該直接或間接繼承BaseDexClassLoader。
// 我們修改DexPathList pathList欄位,追加額外的DEX檔案項。
/* The patched class loader is expected to be a descendant of
* dalvik.system.BaseDexClassLoader. We modify its
* dalvik.system.DexPathList pathList field to append additional DEX
* file entries.
*/
// dexPathList = load.pathList;
Field pathListField = findField(loader, "pathList");
Object dexPathList = pathListField.get(loader);
ArrayList<IOException> suppressedExceptions = new ArrayList<IOException>();
// makeDexElements方法呼叫了DexPathList中的makeDexElements方法,這個方法可以載入並優化dex、zip、jar。
// expandFieldArray方法將makeDexElements返回的陣列patch到dexPathList.dexElements中。
expandFieldArray(dexPathList, "dexElements", makeDexElements(dexPathList,
new ArrayList<File>(additionalClassPathEntries), optimizedDirectory,
suppressedExceptions));
......
}
V19.makeDexElements方法:
/**
* A wrapper around
* {@code private static final dalvik.system.DexPathList#makeDexElements}.
*/
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 (DexPathList.Element[]) dexPathList.makeDexElements(files, optimizedDirectory, suppressedExceptions)
return (Object[]) makeDexElements.invoke(dexPathList, files, optimizedDirectory,
suppressedExceptions);
}
MultiDex.expandFieldArray方法:
/**
* 原欄位內容加上擴充套件的陣列元素替換相應欄位的內容,這個欄位是一個數組。
*
* Replace the value of a field containing a non null array, by a new array containing the
* elements of the original array plus the elements of extraElements.
* @param instance the instance whose field is to be modified.
* @param fieldName the field to modify.
* @param extraElements elements to append at the end of the array.
*/
private static void expandFieldArray(Object instance, String fieldName,
Object[] extraElements) throws NoSuchFieldException, IllegalArgumentException,
IllegalAccessException {
// combined = new <Type>[original.length + extraElements.length];
Field jlrField = findField(instance, fieldName);
Object[] original = (Object[]) jlrField.get(instance);
Object[] combined = (Object[]) Array.newInstance(
original.getClass().getComponentType(), original.length + extraElements.length);
System.arraycopy(original, 0, combined, 0, original.length);
System.arraycopy(extraElements, 0, combined, original.length, extraElements.length);
// 替換物件中的陣列欄位。
jlrField.set(instance, combined);
}