MultiDex工做原理分析和優化方案

動態加載技術(插件化)系列已經坑了有一段時間了,不過UP主我並無放棄治療哈,相信在不就的將來就能夠看到「系統Api Hook模式」和插件化框架Frontia的更新了。今天要講的是動態加載技術的親戚 —— MultiDex。他們的核心原理之一都是dex文件的加載。html

MultiDex是Google爲了解決「65535方法數超標」以及「INSTALL_FAILED_DEXOPT」問題而開發的一個Support庫,具體如何使用MultiDex如今市面已經有一大堆教程(能夠參考給 App 啓用 MultiDex 功能),這裏再也不贅述。這篇日誌主要是配合源碼分析MultiDex的工做原理,以及提供一些MultiDex優化的方案。java

Dex的工做機制

等等,這個章節講的不是MultiDex嗎,怎麼變成Dex了?沒錯哈,沒有Dex,哪來的MultiDex。在Android中,對Dex文件操做對應的類叫作DexFile。在CLASSLOADER 的工做機制中,咱們說到:android

對於 Java 程序來講,編寫程序就是編寫類,運行程序也就是運行類(編譯獲得的class文件),其中起到關鍵做用的就是類加載器 ClassLoader。編程

Android程序的每個Class都是由ClassLoader#loadClass方法加載進內存的,更準確來講,一個ClassLoader實例會有一個或者多個DexFile實例,調用了ClassLoader#loadClass以後,ClassLoader會經過類名,在本身的DexFile數組裏面查找有沒有那個DexFile對象裏面存在這個類,若是都沒有就拋ClassNotFound異常。ClassLoader經過調用DexFile的一個叫defineClass的Native方法去加載指定的類,這點與JVM略有不一樣,後者是直接調用ClassLoader#defineCLass方法,反正最後實際加載類的方法都叫defineClass就沒錯了?。數組

建立DexFile對象

首先來看看造DexFile對象的構方法。緩存

public final class DexFile {
    private int mCookie;
    private final String mFileName;
    ...

    public DexFile(File file) throws IOException {
        this(file.getPath());
    }
   
    public DexFile(String fileName) throws IOException {
        mCookie = openDexFile(fileName, null, 0);
        mFileName = fileName;
        guard.open("close");
    }
   
    private DexFile(String sourceName, String outputName, int flags) throws IOException {
        mCookie = openDexFile(sourceName, outputName, flags);
        mFileName = sourceName;
        guard.open("close");
    }

    static public DexFile loadDex(String sourcePathName, String outputPathName,
        int flags) throws IOException {
        return new DexFile(sourcePathName, outputPathName, flags);
    }
  
    public Class loadClass(String name, ClassLoader loader) {
        String slashName = name.replace('.', '/');
        return loadClassBinaryName(slashName, loader);
    }
  
    public Class loadClassBinaryName(String name, ClassLoader loader) {
        return defineClass(name, loader, mCookie);
    }
    private native static Class defineClass(String name, ClassLoader loader, int cookie);

    native private static int openDexFile(String sourceName, String outputName,
        int flags) throws IOException;

    native private static int openDexFile(byte[] fileContents)
    ...
}

經過之前分析過的源碼,咱們知道ClassLoader主要是經過DexFile.loadDex這個靜態方法來建立它須要的DexFile實例的,這裏建立DexFile的時候,保存了Dex文件的文件路徑mFileName,同時調用了openDexFile的Native方法打開Dex文件並返回了一個mCookie的整型變量(我不知道這個幹啥用的,我猜它是一個C++用的資源句柄,用於Native層訪問具體的Dex文件)。在Native層的openDexFile方法裏,主要作了檢查當前建立來的Dex文件是不是有效的Dex文件,仍是是一個帶有Dex文件的壓縮包,仍是一個無效的Dex文件。安全

加載Dex文件裏的類

加載類的時候,ClassLoader又是經過DexFile#loadClass這個方法來完成的,這個方法裏調用了defineClass這個Native方法,看來DexFile纔是加載Class的具體API,加載Dex文件和加載具體Class都是經過Native方法完成,ClassLoader有點名存實亡啊。cookie

MultiDex的工做機制

當一個Dex文件太肥的時候(方法數目太多、文件太大),在打包Apk文件的時候就會出問題,就算打包的時候不出問題,在Android 5.0如下設備上安裝或運行Apk也會出問題(具體緣由能夠參考給 App 啓用 MultiDex 功能)。既然一個Dex文件不行的話,那就把這個碩大的Dex文件拆分紅若干個小的Dex文件,恰好一個ClassLoader能夠有多個DexFile,這就是MultiDex的基本設計思路。app

工做流程

MultiDex的工做流程具體分爲兩個部分,一個部分是打包構建Apk的時候,將Dex文件拆分紅若干個小的Dex文件,這個Android Studio已經幫咱們作了(設置 「multiDexEnabled true」),另外一部分就是在啓動Apk的時候,同時加載多個Dex文件(具體是加載Dex文件優化後的Odex文件,不過文件名仍是.dex),這一部分工做從Android 5.0開始系統已經幫咱們作了,可是在Android 5.0之前仍是須要經過MultiDex Support庫來支持(MultiDex.install(Context))。框架

因此咱們須要關心的是第二部分,這個過程的簡單示意流程圖以下。

jpg

(圖中紅色部分爲耗時比較大的地方)

源碼分析

如今官方已經部署的MultiDex Support版本是com.android.support:multidex:1.0.1,可是如今倉庫的master分支已經有了許多新的提交(其中最明顯的區別是加入了FileLock來控制多進程同步問題),因此這裏分析的源碼都是最新的master分支上的。

MultiDex Support的入口是MultiDex.install(Context),先從這裏入手吧。(此次我把具體的分析都寫在代碼的註釋了,這樣看是否是更簡潔明瞭些?)

public static void install(Context context) {
        Log.i(TAG, "install");
        
        // 1. 判讀是否須要執行MultiDex。
        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) {
                // Looks like running on a test Context, so just return without patching.
                return;
            }
            
            // 2. 若是這個方法已經調用過一次,就不能再調用了。
            synchronized (installedApk) {
                String apkPath = applicationInfo.sourceDir;
                if (installedApk.contains(apkPath)) {
                    return;
                }
                installedApk.add(apkPath);
                
                // 3. 若是當前Android版本已經自身支持了MultiDex,依然能夠執行MultiDex操做,
                // 可是會有警告。
                if (Build.VERSION.SDK_INT > MAX_SUPPORTED_SDK_VERSION) {
                    Log.w(TAG, "MultiDex is not guaranteed to work in SDK version "
                            + Build.VERSION.SDK_INT + ": SDK version higher than "
                            + MAX_SUPPORTED_SDK_VERSION + " should be backed by "
                            + "runtime with built-in multidex capabilty but it's not the "
                            + "case here: java.vm.version=\""
                            + System.getProperty("java.vm.version") + "\"");
                }
        
                // 4. 獲取當前的ClassLoader實例,後面要作的工做,就是把其餘dex文件加載後,
                // 把其DexFile對象添加到這個ClassLoader實例裏就完事了。
                ClassLoader loader;
                try {
                    loader = context.getClassLoader();
                } catch (RuntimeException e) {
                    Log.w(TAG, "Failure while trying to obtain Context class loader. " +
                            "Must be running in test mode. Skip patching.", e);
                    return;
                }
                if (loader == null) {
                    Log.e(TAG,
                            "Context class loader is null. Must be running in test mode. "
                            + "Skip patching.");
                    return;
                }
                try {
                  // 5. 清除舊的dex文件,注意這裏不是清除上次加載的dex文件緩存。
                  // 獲取dex緩存目錄是,會優先獲取/data/data/<package>/code-cache做爲緩存目錄。
                  // 若是獲取失敗,則使用/data/data/<package>/files/code-cache目錄。
                  // 這裏清除的是第二個目錄。
                  clearOldDexDir(context);
                } catch (Throwable t) {
                  Log.w(TAG, "Something went wrong when trying to clear old MultiDex extraction, "
                      + "continuing without cleaning.", t);
                }
                
                // 6. 獲取緩存目錄(/data/data/<package>/code-cache)。
                File dexDir = getDexDir(context, applicationInfo);
                
                // 7. 加載緩存文件(若是有)。
                List<File> files = MultiDexExtractor.load(context, applicationInfo, dexDir, false);
                
                // 8. 檢查緩存的dex是否安全
                if (checkValidZipFiles(files)) {
                    // 9. 安裝緩存的dex
                    installSecondaryDexes(loader, dexDir, files);
                } else {
                    // 9. 從apk壓縮包裏面提取dex文件
                    Log.w(TAG, "Files were not valid zip files.  Forcing a reload.");
                    files = MultiDexExtractor.load(context, applicationInfo, dexDir, true);
                    if (checkValidZipFiles(files)) {
                        // 10. 安裝提取的dex
                        installSecondaryDexes(loader, dexDir, files);
                    } else {
                        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");
    }

具體代碼的分析已經在上面代碼的註釋裏給出了,從這裏咱們也能夠看出,整個MultiDex.install(Context)的過程當中,關鍵的步驟就是MultiDexExtractor#load方法和MultiDex#installSecondaryDexes方法。

(這部分是題外話)其中有個MultiDex#clearOldDexDir(Context)方法,這個方法的做用是刪除/data/data/<package>/files/code-cache,一開始我覺得這個方法是刪除上一次執行MultiDex後的緩存文件,不過這明顯不對,不可能每次MultiDex都從新解壓dex文件一邊,這樣每次啓動會很耗時,只有第一次冷啓動的時候才須要解壓dex文件。後來我又想是否是之前舊版的MultiDex曾經把緩存文件放在這個目錄裏,如今新版本只是清除之前舊版的遺留文件?可是我找遍了整個MultiDex Repo的提交也沒有見過相似的舊版本代碼。後面我仔細看MultiDex#getDexDir這個方法才發現,原來MultiDex在獲取dex緩存目錄是,會優先獲取/data/data/<package>/code-cache做爲緩存目錄,若是獲取失敗,則使用/data/data/<package>/files/code-cache目錄,然後者的緩存文件會在每次App從新啓動的時候被清除。感受MultiDex獲取緩存目錄的邏輯不是很嚴謹,而獲取緩存目錄失敗也是MultiDex工做工程中少數有重試機制的地方,看來MultiDex真的是一個臨時的兼容方案,Google也許並不打算認真處理這些歷史的黑鍋。

接下來再看看MultiDexExtractor#load這個方法。

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);
        
        // 1. 獲取當前Apk文件的crc值。
        long currentCrc = getZipCrc(sourceApk);
        // Validity check and extraction must be done only while the lock file has been taken.
        File lockFile = new File(dexDir, LOCK_FILENAME);
        RandomAccessFile lockRaf = new RandomAccessFile(lockFile, "rw");
        FileChannel lockChannel = null;
        FileLock cacheLock = null;
        List<File> files;
        IOException releaseLockException = null;
        try {
            lockChannel = lockRaf.getChannel();
            Log.i(TAG, "Blocking on lock " + lockFile.getPath());
            
            // 2. 加上文件鎖,防止多進程衝突。
            cacheLock = lockChannel.lock();
            Log.i(TAG, lockFile.getPath() + " locked");
            
            // 3. 先判斷是否強制從新解壓,這裏第一次會優先使用已解壓過的dex文件,若是加載失敗就強制從新解壓。
            // 此外,經過crc和文件修改時間,判斷若是Apk文件已經被修改(覆蓋安裝),就會跳過緩存從新解壓dex文件。
            if (!forceReload && !isModified(context, sourceApk, currentCrc)) {
                try {
                
                    // 4. 加載緩存的dex文件
                    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);
                    
                    // 5. 加載失敗的話從新解壓,並保存解壓出來的dex文件的信息。
                    files = performExtractions(sourceApk, dexDir);
                    putStoredApkInfo(context,
                            getTimeStamp(sourceApk), currentCrc, files.size() + 1);
                }
            } else {
                // 4. 從新解壓,並保存解壓出來的dex文件的信息。
                Log.i(TAG, "Detected that extraction must be performed.");
                files = performExtractions(sourceApk, dexDir);
                putStoredApkInfo(context, getTimeStamp(sourceApk), currentCrc, files.size() + 1);
            }
        } finally {
            if (cacheLock != null) {
                try {
                    cacheLock.release();
                } catch (IOException e) {
                    Log.e(TAG, "Failed to release lock on " + lockFile.getPath());
                    // Exception while releasing the lock is bad, we want to report it, but not at
                    // the price of overriding any already pending exception.
                    releaseLockException = e;
                }
            }
            if (lockChannel != null) {
                closeQuietly(lockChannel);
            }
            closeQuietly(lockRaf);
        }
        if (releaseLockException != null) {
            throw releaseLockException;
        }
        Log.i(TAG, "load found " + files.size() + " secondary dex files");
        return files;
    }

這個過程主要是獲取能夠安裝的dex文件列表,能夠是上次解壓出來的緩存文件,也能夠是從新從Apk包裏面提取出來的。須要注意的時,若是是從新解壓,這裏會有明顯的耗時,並且解壓出來的dex文件,會被壓縮成.zip壓縮包,壓縮的過程也會有明顯的耗時(這裏壓縮dex文件多是問了節省空間)。

若是dex文件是從新解壓出來的,則會保存dex文件的信息,包括解壓的apk文件的crc值、修改時間以及dex文件的數目,以便下一次啓動直接使用已經解壓過的dex緩存文件,而不是每一次都從新解壓。

須要特別提到的是,裏面的FileLock是最新的master分支裏面新加進去的功能,如今最新的1.0.1版本里面是沒有的。

不管是經過使用緩存的dex文件,仍是從新從apk中解壓dex文件,獲取dex文件列表後,下一步就是安裝(或者說加載)這些dex文件了。最後的工做在MultiDex#installSecondaryDexes這個方法裏面。

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

由於在不一樣的SDK版本上,ClassLoader(更準確來講是DexClassLoader)加載dex文件的方式有所不一樣,因此這裏作了V4/V14/V19的兼容(Magic Code)。

Build.VERSION.SDK_INT < 14

/**
     * Installer for platform versions 4 to 13.
     */
    private static final class V4 {
        private static void install(ClassLoader loader, List<File> additionalClassPathEntries)
                        throws IllegalArgumentException, IllegalAccessException,
                        NoSuchFieldException, IOException {
            
            int extraSize = additionalClassPathEntries.size();
            Field pathField = findField(loader, "path");
            StringBuilder path = new StringBuilder((String) pathField.get(loader));
            String[] extraPaths = new String[extraSize];
            File[] extraFiles = new File[extraSize];
            ZipFile[] extraZips = new ZipFile[extraSize];
            DexFile[] extraDexs = new DexFile[extraSize];
            for (ListIterator<File> iterator = additionalClassPathEntries.listIterator();
                    iterator.hasNext();) {
                File additionalEntry = iterator.next();
                String entryPath = additionalEntry.getAbsolutePath();
                path.append(':').append(entryPath);
                int index = iterator.previousIndex();
                extraPaths[index] = entryPath;
                extraFiles[index] = additionalEntry;
                extraZips[index] = new ZipFile(additionalEntry);
                extraDexs[index] = DexFile.loadDex(entryPath, entryPath + ".dex", 0);
            }
            
            // 這個版本是最簡單的。
            // 只須要建立DexFile對象後,使用反射的方法分別擴展ClassLoader實例的如下字段便可。
            pathField.set(loader, path.toString());
            expandFieldArray(loader, "mPaths", extraPaths);
            expandFieldArray(loader, "mFiles", extraFiles);
            expandFieldArray(loader, "mZips", extraZips);
            expandFieldArray(loader, "mDexs", extraDexs);
        }
    }

14 <= Build.VERSION.SDK_INT < 19

/**
     * Installer for platform versions 14, 15, 16, 17 and 18.
     */
    private static final class V14 {
        private static void install(ClassLoader loader, List<File> additionalClassPathEntries,
                File optimizedDirectory)
                        throws IllegalArgumentException, IllegalAccessException,
                        NoSuchFieldException, InvocationTargetException, NoSuchMethodException {
            
            // 擴展ClassLoader實例的"pathList"字段。
            Field pathListField = findField(loader, "pathList");
            Object dexPathList = pathListField.get(loader);
            expandFieldArray(dexPathList, "dexElements", makeDexElements(dexPathList,
                    new ArrayList<File>(additionalClassPathEntries), optimizedDirectory));
        }
      
        private static Object[] makeDexElements(
                Object dexPathList, ArrayList<File> files, File optimizedDirectory)
                        throws IllegalAccessException, InvocationTargetException,
                        NoSuchMethodException {
            Method makeDexElements =
                    findMethod(dexPathList, "makeDexElements", ArrayList.class, File.class);
            return (Object[]) makeDexElements.invoke(dexPathList, files, optimizedDirectory);
        }
    }

從API14開始,DexClassLoader會使用一個DexpDexPathList類來封裝DexFile數組。

final class DexPathList {
    private static final String DEX_SUFFIX = ".dex";
    private static final String JAR_SUFFIX = ".jar";
    private static final String ZIP_SUFFIX = ".zip";
    private static final String APK_SUFFIX = ".apk";
  
    private static Element[] makeDexElements(ArrayList<File> files,
            File optimizedDirectory) {
        ArrayList<Element> elements = new ArrayList<Element>();
        for (File file : files) {
            ZipFile zip = null;
            DexFile dex = null;
            String name = file.getName();
            if (name.endsWith(DEX_SUFFIX)) {
                // Raw dex file (not inside a zip/jar).
                try {
                    dex = loadDexFile(file, optimizedDirectory);
                } catch (IOException ex) {
                    System.logE("Unable to load dex file: " + file, ex);
                }
            } else if (name.endsWith(APK_SUFFIX) || name.endsWith(JAR_SUFFIX)
                    || name.endsWith(ZIP_SUFFIX)) {
                try {
                    zip = new ZipFile(file);
                } catch (IOException ex) {
                    System.logE("Unable to open zip file: " + file, ex);
                }
                try {
                    dex = loadDexFile(file, optimizedDirectory);
                } catch (IOException ignored) {
     
                }
            } else {
                System.logW("Unknown file type for: " + file);
            }
            if ((zip != null) || (dex != null)) {
                elements.add(new Element(file, zip, dex));
            }
        }
        return elements.toArray(new Element[elements.size()]);
    }
   
    private static DexFile loadDexFile(File file, File optimizedDirectory)
            throws IOException {
        if (optimizedDirectory == null) {
            return new DexFile(file);
        } else {
            String optimizedPath = optimizedPathFor(file, optimizedDirectory);
            return DexFile.loadDex(file.getPath(), optimizedPath, 0);
        }
    }
}

經過調用DexPathList#makeDexElements方法,能夠加載咱們上面解壓獲得的dex文件,從代碼也能夠看出,DexPathList#makeDexElements其實也是經過調用DexFile#loadDex來加載dex文件並建立DexFile對象的。V14中,經過反射調用DexPathList#makeDexElements方法加載咱們須要的dex文件,在把加載獲得的數組擴展到ClassLoader實例的"pathList"字段,從而完成dex文件的安裝。

從DexPathList的代碼中咱們也能夠看出,ClassLoader是支持直接加載.dex/.zip/.jar/.apk的dex文件包的(我記得之前在哪篇日誌中好像提到過相似的問題…)。

19 <= Build.VERSION.SDK_INT

/**
     * Installer for platform versions 19.
     */
    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(dexPathList, "dexElementsSuppressedExceptions");
                IOException[] dexElementsSuppressedExceptions =
                        (IOException[]) suppressedExceptionsField.get(dexPathList);
                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(dexPathList, 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);
        }
    }

V19與V14差異不大,只不過DexPathList#makeDexElements方法多了一個ArrayList<IOException>參數,若是在執行DexPathList#makeDexElements方法的過程當中出現異常,後面使用反射的方式把這些異常記錄進DexPathList的dexElementsSuppressedExceptions字段裏面。

不管是V4/V14仍是V19,在建立DexFile對象的時候,都須要經過DexFile的Native方法openDexFile來打開dex文件,其具體細節暫不討論(涉及到dex的文件結構,很煩,有興趣請閱讀dalvik_system_DexFile.cpp),這個過程的主要目的是給當前的dex文件作Optimize優化處理並生成相同文件名的odex文件,App實際加載類的時候,都是經過odex文件進行的。由於每一個設備對odex格式的要求都不同,因此這個優化的操做只能放在安裝Apk的時候處理,主dex的優化咱們已經在安裝apk的時候搞定了,其他的dex就是在MultiDex#installSecondaryDexes裏面優化的,然後者也是MultiDex過程當中,另一個耗時比較多的操做。(在MultiDex中,提取出來的dex文件被壓縮成.zip文件,又優化後的odex文件則被保存爲.dex文件。)

到這裏,MultiDex的工做流程就結束了。怎麼樣,是否是以爲和之前談到動態加載技術(插件化)的時候說的很像?沒錯,誰叫它們的核心都是dex文件呢。Java老師第一節課就說「類就是編程」,搞定類你就能搞定整個世界啊!

優化方案

MultiDex有個比較蛋疼的問題,就是會產生明顯的卡頓現象,經過上面的分析,咱們知道具體的卡頓產生在解壓dex文件以及優化dex兩個步驟。不過好在,在Application#attachBaseContext(Context)中,UI線程的阻塞是不會引起ANR的,只不過這段長時間的卡頓(白屏)仍是會影響用戶體驗。

目前,優化方案能想到的有兩種。

PreMultiDex方案

大體思路是,在安裝一個新的apk的時候,先在Worker線程裏作好MultiDex的解壓和Optimize工做,安裝apk並啓動後,直接使用以前Optimize產生的odex文件,這樣就能夠避免第一次啓動時候的Optimize工做。

20161204148086219213560.jpg

安裝dex的時候,核心是建立DexFile對象並使用其Native方法對dex文件進行opt處理,同時生產一個與dex文件(.zip)同名的已經opt過的dex文件(.dex)。若是安裝dex的時候,這個opt過的dex文件已經存在,則跳過這個過程,這會節省許多耗時。因此優化的思路就是,下載Apk完成的時候,預先解壓dex文件,並預先觸發安裝dex文件以生產opt過的dex文件。這樣覆蓋安裝Apk並啓動的時候,若是MultiDex能命中解壓好的dex和odex文件,則能避開耗時最大的兩個操做。

不過這個方案的缺點也是明顯的,第一次安裝的apk沒有做用,並且事先須要使用內置的apk更新功能把新版本的apk文件下載下來後,才能作PreMultiDex工做。

異步MultiDex方案

這種方案也是目前比較流行的Dex手動分包方案,啓動App的時候,先顯示一個簡單的Splash閃屏界面,而後啓動Worker線程執行MultiDex#install(Context)工做,就能夠避免UI線程阻塞。不過要確保啓動以及啓動MultiDex#install(Context)所須要的類都在主dex裏面(手動分包),並且須要處理好進程同步問題。

參考資料:



著做信息:
本文章出自 Kaede 的博客,原創文章若無特別說明,均遵循 CC BY-NC 4.0 知識共享許可協議4.0(署名-非商用-相同方式共享),能夠隨意摘抄轉載,但必須標明署名及原地址。

相關文章
相關標籤/搜索