1. 程式人生 > >【分析】多dex載入機制

【分析】多dex載入機制

相關文章連結:

---------------------------------------------------------------------------------

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);
}