Android分包MultiDex源碼分析

概述

Android開發者應該都遇到了64K最大方法數限制的問題,針對這個問題,google也推出了multidex分包機制,在生成apk的時候,把整個應用拆成n個dex包(classes.dex、classes2.dex、classes3.dex),每一個dex不超過64k個方法。使用multidex,在5.0之前的系統,應用安裝時只安裝main dex(包含了應用啓動須要的必要class),在應用啓動以後,需在Application的attachBaseContext中調用MultiDex.install(base)方法,在這時候才加載第2、第三...個dex文件,從而規避了64k問題。 固然,在attachBaseContext方法中直接install啓動second dex會有一些問題,好比install方法是一個同步方法,當在主線程中加載的dex太大的時候,耗時會比較長,可能會觸發ANR。不過這是另一個問題了,解決方法能夠參考:Android最大方法數和解決方案 blog.csdn.net/shensky711/…java

本文主要分析的是MultiDex.install()到底作了什麼,如何把secondary dexes中的類動態加載進來。數組

MultiDex使用到的路徑解析

  • ApplicationInfo.sourceDir:apk的安裝路徑,如/data/app/com.hanschen.multidex-1.apk
  • Context.getFilesDir():返回/data/data/<packagename>/files目錄,通常經過openFileOutput方法輸出文件到該目錄
  • ApplicationInfo.dataDir: 返回/data/data/<packagename>目錄

源碼分析

代碼入口

代碼入口很簡單,簡單粗暴,就調用了一個靜態方法MultiDex.install(base);,傳入一個Context對象緩存

@Override
    protected void attachBaseContext(Context base) {
        super.attachBaseContext(base);
        MultiDex.install(base);
    }
複製代碼

MultiDex.install分析

下面是主要的代碼app

public static void install(Context context) {
        Log.i("MultiDex", "install");
        if (IS_VM_MULTIDEX_CAPABLE) {
            //VM版本大於2.1時,IS_VM_MULTIDEX_CAPABLE爲true,這時候MultiDex.install什麼也不用作,直接返回。由於大於2.1的VM會在安裝應用的時候,就把多個dex合併到一塊
        } else if (VERSION.SDK_INT < 4) {
            //Multi dex最小支持的SDK版本爲4
            throw new RuntimeException("Multi dex installation failed. SDK " + VERSION.SDK_INT + " is unsupported. Min SDK version is " + 4 + ".");
        } else {
            try {
                ApplicationInfo e = getApplicationInfo(context);
                if (e == null) {
                    return;
                }

                Set var2 = installedApk;
                synchronized (installedApk) {
                    String apkPath = e.sourceDir;
                    //檢測應用是否已經執行過install()了,防止重複install
                    if (installedApk.contains(apkPath)) {
                        return;
                    }

                    installedApk.add(apkPath);

                    //獲取ClassLoader,後面會用它來加載second dex
                    DexClassLoader classLoader;
                    ClassLoader loader;
                    try {
                        loader = context.getClassLoader();
                    } catch (RuntimeException var9) {
                        return;
                    }

                    if (loader == null) {
                        return;
                    }

                    //清空目錄:/data/data/<packagename>/files/secondary-dexes/,其實我沒搞明白這個的做用,由於從後面的代碼來看,這個目錄是沒有使用到的
                    try {
                        clearOldDexDir(context);
                    } catch (Throwable var8) {
                    }

                    File dexDir = new File(e.dataDir, "code_cache/secondary-dexes");
                    //把dex文件緩存到/data/data/<packagename>/code_cache/secondary-dexes/目錄,[後有詳細分析]
                    List files = MultiDexExtractor.load(context, e, dexDir, false);
                    if (checkValidZipFiles(files)) {
                        //進行安裝,[後有詳細分析]
                        installSecondaryDexes(loader, dexDir, files);
                    } else {
                        //文件無效,從apk文件中再次解壓secondary dex文件後進行安裝
                        files = MultiDexExtractor.load(context, e, dexDir, true);
                        if (!checkValidZipFiles(files)) {
                            throw new RuntimeException("Zip files were not valid.");
                        }

                        installSecondaryDexes(loader, dexDir, files);
                    }
                }
            } catch (Exception var11) {
                throw new RuntimeException("Multi dex installation failed (" + var11.getMessage() + ").");
            }
        }
    }
複製代碼

這段代碼的主要邏輯整理以下:ide

  1. VM版本檢測,若是大於2.1就什麼都不作(系統在安裝應用的時候已經幫咱們把dex合併了),若是系統SDK版本小於4就拋出運行時異常
  2. 把apk中的secondary dexes解壓到緩存目錄,並把這些緩存讀取出來。應用第二次啓動的時候,會嘗試從緩存目錄中讀取,除非讀取出的文件校驗失敗,不然再也不從apk中解壓dexes
  3. 根據當前的SDK版本,執行不一樣的安裝方法

先來看看MultiDexExtractor.load(context, e, dexDir, false)源碼分析

/** * 解壓apk文件中的classes2.dex、classes3.dex等文件解壓到dexDir目錄中 * * @param dexDir 解壓目錄 * @param forceReload 是否須要強制從apk文件中解壓,否的話會直接讀取舊文件 * @return 解壓後的文件列表 * @throws IOException */
    static List<File> load(Context context, ApplicationInfo applicationInfo, File dexDir, boolean forceReload) throws IOException {
        File sourceApk = new File(applicationInfo.sourceDir);
        long currentCrc = getZipCrc(sourceApk);
        List files;
        if (!forceReload && !isModified(context, sourceApk, currentCrc)) {
            try {
                //從緩存目錄中直接查找緩存文件,跳過解壓
                files = loadExistingExtractions(context, sourceApk, dexDir);
            } catch (IOException var9) {
                files = performExtractions(sourceApk, dexDir);
                putStoredApkInfo(context, getTimeStamp(sourceApk), currentCrc, files.size() + 1);
            }
        } else {
            //把apk中的secondary dex文件解壓到緩存目錄,並把解壓後的文件返回
            files = performExtractions(sourceApk, dexDir);
            //把解壓信息保存到sharedPreferences中
            putStoredApkInfo(context, getTimeStamp(sourceApk), currentCrc, files.size() + 1);
        }

        return files;
    }
複製代碼

首先判斷如下是否須要強制從apk文件中解壓,再進行下CRC校驗,若是不須要從apk從新解壓,就直接從緩存目錄中讀取已解壓的文件返回,不然解壓apk中的classes文件到緩存目錄,再把相應的文件返回。這個方法再往下的分析就不貼出來了,不復雜,你們能夠本身去看看。讀取後會把解壓信息保存到sharedPreferences中,裏面會保存時間戳、CRC校驗和dex數量。google

獲得dex文件列表後,要作的就是把dex文件關聯到應用,這樣應用findclass的時候才能成功。這個主要是經過installSecondaryDexes方法來完成的spa

/** * 安裝dex文件 * * @param loader 類加載器 * @param dexDir 緩存目錄,用以存放opt以後的dex文件 * @param files 須要安裝的dex * @throws IllegalArgumentException * @throws IllegalAccessException * @throws NoSuchFieldException * @throws InvocationTargetException * @throws NoSuchMethodException * @throws IOException */
    private static void installSecondaryDexes(ClassLoader loader, File dexDir, List<File> files) throws IllegalArgumentException, IllegalAccessException, NoSuchFieldException, InvocationTargetException, NoSuchMethodException, IOException {
                                              
        if (!files.isEmpty()) {
            //對不一樣版本的SDK作不一樣處理
            if (VERSION.SDK_INT >= 19) {
                MultiDex.V19.install(loader, files, dexDir);
            } else if (VERSION.SDK_INT >= 14) {
                MultiDex.V14.install(loader, files, dexDir);
            } else {
                MultiDex.V4.install(loader, files);
            }
        }

    }
複製代碼

能夠看到,對於不一樣的SDK版本,分別採用了不一樣的處理方法,咱們主要分析SDK>=19的狀況,其餘狀況大同小異,讀者能夠本身去分析。.net

private static final class V19 {
        private V19() {
        }

        /** * 安裝dex文件 * * @param loader 類加載器 * @param additionalClassPathEntries 須要安裝的dex * @param optimizedDirectory 緩存目錄,用以存放opt以後的dex文件 * @throws IllegalArgumentException * @throws IllegalAccessException * @throws NoSuchFieldException * @throws InvocationTargetException * @throws NoSuchMethodException */
        private static void install(ClassLoader loader, List<File> additionalClassPathEntries, File optimizedDirectory) throws IllegalArgumentException, IllegalAccessException, NoSuchFieldException, InvocationTargetException, NoSuchMethodException {

            //經過反射獲取ClassLoader對象中的pathList屬性,實際上是ClassLoader的父類BaseDexClassLoader中的成員
            Field pathListField = MultiDex.findField(loader, "pathList");
            //經過屬性獲取該屬性的值,該屬性的類型是DexPathList
            Object dexPathList = pathListField.get(loader);

            ArrayList suppressedExceptions = new ArrayList();
            //經過反射調用dexPathList的makeDexElements返回Element對象數組。方法裏面會讀取每個輸入文件,生成DexFile對象,並將其封裝進Element對象
            Object[] elements = makeDexElements(dexPathList, new ArrayList(additionalClassPathEntries), optimizedDirectory, suppressedExceptions);
            
            //將elements數組跟dexPathList對象的dexElements數組合並,並把合併後的數組做爲dexPathList新的值
            MultiDex.expandFieldArray(dexPathList, "dexElements", elements);

            //處理異常
            if (suppressedExceptions.size() > 0) {
                Iterator suppressedExceptionsField = suppressedExceptions.iterator();

                while (suppressedExceptionsField.hasNext()) {
                    IOException dexElementsSuppressedExceptions = (IOException) suppressedExceptionsField.next();
                    Log.w("MultiDex", "Exception in makeDexElement", dexElementsSuppressedExceptions);
                }

                Field suppressedExceptionsField1 = MultiDex.findField(loader, "dexElementsSuppressedExceptions");
                IOException[] dexElementsSuppressedExceptions1 = (IOException[]) ((IOException[]) suppressedExceptionsField1.get(loader));
                if (dexElementsSuppressedExceptions1 == null) {
                    dexElementsSuppressedExceptions1 = (IOException[]) suppressedExceptions.toArray(new IOException[suppressedExceptions
                            .size()]);
                } else {
                    IOException[] combined = new IOException[suppressedExceptions.size() + dexElementsSuppressedExceptions1.length];
                    suppressedExceptions.toArray(combined);
                    System.arraycopy(dexElementsSuppressedExceptions1, 0, combined, suppressedExceptions.size(), dexElementsSuppressedExceptions1.length);
                    dexElementsSuppressedExceptions1 = combined;
                }

                suppressedExceptionsField1.set(loader, dexElementsSuppressedExceptions1);
            }

        }

        private static Object[] makeDexElements(Object dexPathList,
                                                ArrayList<File> files,
                                                File optimizedDirectory,
                                                ArrayList<IOException> suppressedExceptions) throws IllegalAccessException, InvocationTargetException, NoSuchMethodException {
            Method makeDexElements = MultiDex.findMethod(dexPathList, "makeDexElements", new Class[]{ArrayList.class, File.class, ArrayList.class});
            return (Object[]) ((Object[]) makeDexElements.invoke(dexPathList, new Object[]{files, optimizedDirectory, suppressedExceptions}));
        }
    }
複製代碼

在Android中,有兩個ClassLoader,分別是DexPathListPathClassLoader,它們的父類都是BaseDexClassLoader,DexPathList和PathClassLoader的實現都是在BaseDexClassLoader之中,而BaseDexClassLoader的實現又基本是經過調用DexPathList的方法完成的。DexPathList裏面封裝了加載dex文件爲DexFile對象(調用了native方法,有興趣的童鞋能夠繼續跟蹤下去)的方法。 上述代碼中的邏輯以下:線程

  1. 經過反射獲取pathList對象
  2. 經過pathList把輸入的dex文件輸出爲elements數組,elements數組中的元素封裝了DexFile對象
  3. 把新輸出的elements數組合併到原pathList的dexElements數組中
  4. 異常處理

當把dex文件加載到pathList的dexElements數組以後,整個multidex.install基本上就完成了。 但可能還有些童鞋還會有些疑問,僅僅只是把Element數組合併到ClassLoader就能夠了嗎?仍是沒有找到加載類的地方啊?那咱們再繼續看看,當用到一個類的時候,會用ClassLoader去加載一個類,加載類會調用類加載器的findClass方法

@Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        List<Throwable> suppressedExceptions = new ArrayList<Throwable>();
        //調用pathList的findClass方法
        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;
    }
複製代碼

因而繼續跟蹤:

public Class findClass(String name, List<Throwable> suppressed) {
        //遍歷dexElements數組
        for (Element element : dexElements) {

            DexFile dex = element.dexFile;
            if (dex != null) {
                //繼續跟蹤會發現調用的是一個native方法
                Class clazz = dex.loadClassBinaryName(name, definingContext, suppressed);
                if (clazz != null) {
                    return clazz;
                }
            }
        }
        if (dexElementsSuppressedExceptions != null) {
            suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions));
        }
        return null;
    }
複製代碼

到如今就清晰了,當加載一個類的時候,會遍歷dexElements數組,經過native方法從Element元素中加載類名相應的類

總結

到最後,總結整個multidex.install流程,其實很簡單,就作了一件事情,把apk中的secondary dex文件經過ClassLoader轉換成Element數組,並把輸出的數組合與ClassLoader的Element數組合並。

相關文章
相關標籤/搜索