Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> 關於Android編程 >> MultiDex與熱修復實現原理

MultiDex與熱修復實現原理

編輯:關於Android編程

一、Android的ClassLoader體系

這裡寫圖片描述vcrWu/rW0LXEYXBroaPV4rj20rLKx1BhdGhDbGFzc0xvYWRlctf3zqrErMjPtcTA4LzT1NjG97XE1K3S8qOs0vLOqtK7sOOzzNDytrzKx7Cy17DBy6Os1Nq08r+qo6zV4sqxuvJQYXRoQ2xhc3NMb2FkZXK+zcilvNPU2Na4tqi1xGFwayi94tG5s8lkZXijrMi7uvPU2tPFu6+zyW9kZXgpvs2/ydLUwcuhozwvcD4NCjxwPkRleENsYXNzTG9hZGVyv8nS1LzT1NjIzrrOwre+trXEYXBrL2RleC9qYXKjrFBhdGhDbGFzc0xvYWRlcta7xNy809TY0tGwstewtb3Ptc2z1tCjqLy0L2RhdGEvYXBwxL/CvM/Co6m1xGFwa87EvP6hozwvcD4NCjxwPrTTyc/D5s7Sw8fWqrXAo6xEZXhDbGFzc0xvYWRlcrrNUGF0aENsYXNzTG9hZGVyvNPU2NStwO3G5Mq1ysfSu9H5tcSjrL7NysfKudPDs6G+sLK70rvR+aGjPC9wPg0KPHA+PHN0cm9uZz62/qGiRGV4Q2xhc3NMb2FkZXK2r8ysvNPU2LXEyrXP1jwvc3Ryb25nPjwvcD4NCjxwPjxzdHJvbmc+tdrSu7K9o7q0tL2oRGV4Q2xhc3NMb2FkZXK21M/zo6y809TYttTTprXEYXBrL2RleC9qYXLOxLz+oaM8L3N0cm9uZz48L3A+DQo8cHJlIGNsYXNzPQ=="brush:java;"> is = getAssets().open("app.apk"); file = new File(getFilesDir(), "plugin.apk"); fos = new FileOutputStream(file); byte[] buffer = new byte[1024]; int len; while ((len = is.read(buffer)) != -1) { fos.write(buffer, 0, len); } fos.flush(); String apkPath = file.getAbsolutePath(); dexClassLoader = new DexClassLoader(apkPath, getFilesDir().getAbsolutePath(), null, getClassLoader());

下面來看看DexClassLoader的構造函數

public class DexClassLoader extends BaseDexClassLoader {
    // dexPath:是加載apk/dex/jar的路徑
    // optimizedDirectory:是dex的輸出路徑(因為加載apk/jar的時候會解壓除dex文件,這個路徑就是保存dex文件的)
    // libraryPath:是加載的時候需要用到的lib庫,這個一般不用
    // parent:給DexClassLoader指定父加載器
    public DexClassLoader(String dexPath, String optimizedDirectory,
            String libraryPath, ClassLoader parent) {
        super(dexPath, new File(optimizedDirectory), libraryPath, parent);
    }
}

可以看到它調用的是父類的構造函數,所以直接來看BaseDexClassLoader的構造函數。

public BaseDexClassLoader(String dexPath, File optimizedDirectory,
        String libraryPath, ClassLoader parent) {
    super(parent);
    this.pathList = new DexPathList(this, dexPath, libraryPath, optimizedDirectory);
}

可以看到,它創建了一個DexPathList實例,下面來看看構造函數。

private final Element[] dexElements;

// definingContext對應的就是當前classLoader
// dexPath對應的就是上面傳進來的apk/dex/jar的路徑
// libraryPath就是上面傳進來的加載的時候需要用到的lib庫的目錄,這個一般不用
// optimizedDirectory就是上面傳進來的dex的輸出路徑
public DexPathList(ClassLoader definingContext, String dexPath,
        String libraryPath, File optimizedDirectory) {
    ArrayList suppressedExceptions = new ArrayList();
    this.dexElements = makeDexElements(splitDexPath(dexPath), optimizedDirectory,
                                       suppressedExceptions);
}

可以看到它調用的是makeDexElements方法,這個方法就是得到一個裝有dex文件的數組Element[],每個Element對象裡面包含一個DexFile對象成員,它對應的就是dex文件。

static class Element {
    private final File file; 
    private final boolean isDirectory; 
    private final File zip;
    private final DexFile dexFile;
    ......
}

具體的我們後面再說,下面先看看makeDexElements方法。

// files是一個ArrayList列表,它對應的就是apk/dex/jar文件,因為我們可以指定多個文件。
// optimizedDirectory是前面傳入dex的輸出路徑
// suppressedExceptions為一個異常列表
private static Element[] makeDexElements(ArrayList files, File optimizedDirectory,
                                         ArrayList suppressedExceptions) {
    ArrayList elements = new ArrayList();
    /*
     * Open all files and load the (direct or contained) dex files
     * up front.
     */
    for (File file : files) {
        File zip = null;
        DexFile dex = null;
        String name = file.getName();

        // 如果是一個dex文件
        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);
            }
        // 如果是一個apk或者jar或者zip文件
        } else if (name.endsWith(APK_SUFFIX) || name.endsWith(JAR_SUFFIX)
                || name.endsWith(ZIP_SUFFIX)) {
            zip = file;

            try {
                dex = loadDexFile(file, optimizedDirectory);
            } catch (IOException suppressed) {
                /*
                 * IOException might get thrown "legitimately" by the DexFile constructor if the
                 * zip file turns out to be resource-only (that is, no classes.dex file in it).
                 * Let dex == null and hang on to the exception to add to the tea-leaves for
                 * when findClass returns null.
                 */
                suppressedExceptions.add(suppressed);
            }
        } else if (file.isDirectory()) {
            // We support directories for looking up resources.
            // This is only useful for running libcore tests.
            elements.add(new Element(file, true, null, null));
        } else {
            System.logW("Unknown file type for: " + file);
        }

        if ((zip != null) || (dex != null)) {
            elements.add(new Element(file, false, zip, dex));
        }
    }

    return elements.toArray(new Element[elements.size()]);
}

前面我們提到過Element,它裡面具體包含哪些元素,現在從上面代碼我們就可以知道了。

static class Element {
    private final File file;  // 它對應的就是需要加載的apk/dex/jar文件
    private final boolean isDirectory; // 第一個參數file是否為一個目錄,一般為false,因為我們傳入的是要加載的文件
    private final File zip;  // 如果加載的是一個apk或者jar或者zip文件,該對象對應的就是該apk或者jar或者zip文件
    private final DexFile dexFile; // 它是得到的dex文件
    ......
}

上面我們可以看到,它調用的是loadDexFile方法。

// file為需要加載的apk/dex/jar文件
// optimizedDirectorydex的輸出路徑
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);
    }
}

如果我們沒有指定dex輸出目錄的話,就直接創建一個DexFile對象,如果我們指定了dex輸出目錄,我們就需要構造dex輸出路徑。

optimizedPathFor方法用來得到輸出文件dex路徑,就是optimizedDirectory/filename.dex,optimizedDirectory是前面指定的輸出目錄,filename就是加載的文件名,後綴為.dex,最終構造得到一個輸出dex文件路徑.

下面我們重點看看DexFile.loadDex方法。

static public DexFile loadDex(String sourcePathName, String outputPathName,
    int flags) throws IOException {
    return new DexFile(sourcePathName, outputPathName, flags);
}

下面我們就不往下看了,我們這裡可以進行總結。

1、在DexClassLoader我們指定了加載的apk/dex/jar文件和dex輸出路徑optimizedDirectory,它最終會被解析得到DexFile文件。
2、將DexFile文件對象放在Element對象裡面,它對應的就是Element對象的dexFile成員變量。
3、將這個Element對象放在一個Element[]數組中,然後將這個數組返回給DexPathList的dexElements成員變量。
4、DexPathList是BaseDexClassLoader的一個成員變量。

最終得到一個裝有dex文件的數組Element[],每個Element對象裡面包含一個DexFile對象成員,它對應的就是dex文件。

這裡寫圖片描述

第二步:調用dexClassLoader的loadClass,得到加載的dex裡面的指定的Class.

clazz = dexClassLoader.loadClass("com.example.apkplugin.PluginTest");

下面我們來分析一下loadClass方法。因為DexClassLoader和BaseDexClassLoader都沒有實現loadClass方法,所以最終調用的是ClassLoader的loadClass方法。

public Class loadClass(String className) throws ClassNotFoundException {
    return loadClass(className, false);
}

protected Class loadClass(String className, boolean resolve) throws ClassNotFoundException {
    Class clazz = findLoadedClass(className);

    if (clazz == null) {
        ClassNotFoundException suppressed = null;
        try {
            clazz = parent.loadClass(className, false);
        } catch (ClassNotFoundException e) {
            suppressed = e;
        }

        if (clazz == null) {
            try {
                clazz = findClass(className);
            } catch (ClassNotFoundException e) {
                e.addSuppressed(suppressed);
                throw e;
            }
        }
    }

    return clazz;
}

可以看到它調用的是findClass方法,由於DexClassLoader沒有實現這個方法,所以我們看BaseDexClassLoader的findClass

@Override
protected Class findClass(String name) throws ClassNotFoundException {
    List suppressedExceptions = new ArrayList();
    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;
}

pathList就是前面創建的DexPathList對象,從上面我們知道,我們加載的dex文件都存放在它的exElements成員變量上面,dexElements就是Element[]數組,所以可以看到BaseDexClassLoader的findClass方法調用的是pathList的findClass方法,我們具體來看看。

可以看到BaseDexClassLoader的findClass方法調用的是DexPathList的findClass方法。

public Class findClass(String name, List suppressed) {
    for (Element element : dexElements) {
        DexFile dex = element.dexFile;

        if (dex != null) {
            Class clazz = dex.loadClassBinaryName(name, definingContext, suppressed);
            if (clazz != null) {
                return clazz;
            }
        }
    }
    if (dexElementsSuppressedExceptions != null) {
        suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions));
    }
    return null;
}

可以看到它就是遍歷dexElements數組,從每個Element對象中拿到DexFile類型的dex文件,然後就是從dex去加載所需要的class文件,直到找到為止。

總結:一個ClassLoader可以包含多個dex文件,每個dex文件是一個Element,多個dex文件排列成一個有序的數組dexElements,當找類的時候,會按順序遍歷dex文件,然後從當前遍歷的dex文件中找類,如果找類則返回,如果找不到從下一個dex文件繼續查找。

三、MultiDex基本原理
當一個app的功能越來越復雜,代碼量越來越多,可以遇到下面兩種情況:
1. 生成的apk在2.3以前的機器無法安裝,提示INSTALL_FAILED_DEXOPT
2. 方法數量過多,編譯時出錯,提示:Conversion to Dalvik format failed:Unable to execute dex: method ID not in [0, 0xffff]: 65536

原因:
1. Android2.3及以前版本用來執行dexopt(用於優化dex文件)的內存只分配了5M
2. 一個dex文件最多只支持65536個方法。

解決方案:
1、使用Multidex,將編譯好的class文件拆分打包成兩個dex,繞過dex方法數量的限制以及安裝時的檢查,在運行時再動態加載第二個dex文件中。
2、使用插件化,將功能模塊分離,減少宿主apk的大小和代碼。

插件化我們這裡先不討論,這裡主要來說說Multidex的原理。

基本原理:
1、除了第一個dex文件(即正常apk包唯一包含的Dex文件),其它dex文件都以資源的方式放在安裝包中。所以我們需要將其他dex文件並在Application的onCreate回調中注入到系統的ClassLoader。並且對於那些在注入之前已經引用到的類(以及它們所在的jar),必須放入第一個Dex文件中。

2、PathClassLoader作為默認的類加載器,在打開應用程序的時候PathClassLoader就去加載指定的apk(解壓成dex,然後在優化成odex),也就是第一個dex文件是PathClassLoader自動加載的。所以,我們需要做的就是將其他的dex文件注入到這個PathClassLoader中去。

3、因為PathClassLoader和DexClassLoader的原理基本一致,從前面的分析來看,我們知道PathClassLoader裡面的dex文件是放在一個Element數組裡面,可以包含多個dex文件,每個dex文件是一個Element,所以我們只需要將其他的dex文件放到這個數組中去就可以了。

實現:
1、通過反射獲取PathClassLoader中的DexPathList中的Element數組(已加載了第一個dex包,由系統加載)
2、通過反射獲取DexClassLoader中的DexPathList中的Element數組(將第二個dex包加載進去)
3、將兩個Element數組合並之後,再將其賦值給PathClassLoader的Element數組

谷歌提供的MultiDex支持庫就是按照這個思路來實現的,我們可以直接來看看源碼。

首先來看看使用:
1、修改Gradle的配置,支持multidex:

android {
    compileSdkVersion 21
    buildToolsVersion "21.1.0"
    defaultConfig {
        ...
        minSdkVersion 14
        targetSdkVersion 21
        ...
        // Enabling multidex support.
        multiDexEnabled true
    }
    ...
}
dependencies {
  compile 'com.android.support:multidex:1.0.0'
}

在manifest文件中,在application標簽下添加MultidexApplication Class的引用,如下所示:



    
        ...
    

使用起來很簡單,下面我們來看看源碼,看是不是按照前面介紹的思路實現的。

首先我們來看看MultiDexApplication類。

public class MultiDexApplication extends Application {
    public MultiDexApplication() {
    }

    protected void attachBaseContext(Context base) {
        super.attachBaseContext(base);
        MultiDex.install(this);
    }
}

原來要求使用MultiDexApplication的原因就是它重寫了Application,主要是為了將其他dex文件注入到系統的ClassLoader。

進入MultiDex.install(this)方法。

public static void install(Context context) {
    if(IS_VM_MULTIDEX_CAPABLE) {
        Log.i("MultiDex", "VM has multidex support, MultiDex support library is disabled.");
    // 可以看到,MultiDex不支持SDK版本小於4的系統
    } else if(VERSION.SDK_INT < 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) {
                // 得到我們這個應用的apk文件路徑
                // 拿到這個apk文件路徑之後,後面就可以從中提取出其他的dex文件
                // 並且加載dex放到一個Element數組中
                String apkPath = e.sourceDir;
                if(installedApk.contains(apkPath)) {
                    return;
                }
                // 將這個apk文件路徑放到一個set中
                installedApk.add(apkPath);

                // 得到classLoader,它就是PathClassLoader
                // 後面就可以從這個PathClassLoader中拿到DexPathList中的Element數組
                // 這個數組裡面就包括由系統加載第一個dex包
                ClassLoader loader;
                try {
                    loader = context.getClassLoader();
                } catch (RuntimeException var9) {
                    Log.w("MultiDex", "Failure while trying to obtain Context class loader. Must be running in test mode. Skip patching.", var9);
                    return;
                }

                // 得到apk解壓後得到的dex文件的存放目錄,放到應用的data目錄下
                File dexDir = new File(e.dataDir, SECONDARY_FOLDER_NAME);

                // 這個方法就是從apk中提取dex文件,放到data目錄下,就不展開了
                List files = MultiDexExtractor.load(context, e, dexDir, false);
                if(checkValidZipFiles(files)) {
                    // 這個方法就是將其他的dex文件注入到系統classloader中的具體操作
                    installSecondaryDexes(loader, dexDir, files);
                } else {
                    files = MultiDexExtractor.load(context, e, dexDir, true);
                    installSecondaryDexes(loader, dexDir, files);
                }
            }
        } catch (Exception var11) {
            Log.e("MultiDex", "Multidex installation failure", var11);
            throw new RuntimeException("Multi dex installation failed (" + var11.getMessage() + ").");
        }
    }
}

下面我們重點看看installSecondaryDexes方法。

// loader對應的就是PathClassLoader
// dexDir是dex的存放目錄
// files對應的就是dex文件
private static void installSecondaryDexes(ClassLoader loader, File dexDir, List files) throws IllegalArgumentException, IllegalAccessException, NoSuchFieldException, InvocationTargetException, NoSuchMethodException, IOException {
    if(!files.isEmpty()) {
        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版本實現是有差別的,因為它裡面是使用反射實現的,所以會有不同,我們看看MultiDex.V14.install方法。

private static void install(ClassLoader loader, List additionalClassPathEntries, File optimizedDirectory) throws IllegalArgumentException, IllegalAccessException, NoSuchFieldException, InvocationTargetException, NoSuchMethodException {
    // 這個方法就是使用反射來得到loader的pathList字段
    Field pathListField = MultiDex.findField(loader, "pathList");
    // 得到loader的pathList字段後,我們就可以得到這個字段的值,也就是DexPathList對象
    Object dexPathList = pathListField.get(loader);
    // 這個方法就是將其他的dex文件Element數組和第一個dex的Element數組合並
    // makeDexElements方法就是用來得到其他dex的Elements數組
    MultiDex.expandFieldArray(dexPathList, "dexElements", makeDexElements(dexPathList, new ArrayList(additionalClassPathEntries), optimizedDirectory));
}

下面來看看合並的過程

// instance對應的就是pathList對象
// fieldName 對應的就是字段名,我們要得到的就是pathList對象裡面的dexElements數組
// extraElements對應的就是其他dex對應的Element數組
private static void expandFieldArray(Object instance, String fieldName, Object[] extraElements) throws NoSuchFieldException, IllegalArgumentException, IllegalAccessException {
    // 得到Element數組字段
    Field jlrField = findField(instance, fieldName);
    // 得到pathList對象裡面的dexElements數組
    Object[] original = (Object[])((Object[])jlrField.get(instance));
    // 創建一個新的數組用來存放合並之後的結果
    Object[] combined = (Object[])((Object[])Array.newInstance(original.getClass().getComponentType(), original.length + extraElements.length));
    // 將第一個dex的Elements數組復制到創建的數組中去
    System.arraycopy(original, 0, combined, 0, original.length);
    // 將其他dex的Elements數組復制到創建的數組中去
    System.arraycopy(extraElements, 0, combined, original.length, extraElements.length);
    // 將得到的這個合並的新數組的值設置到pathList對象的Element數組字段上
    jlrField.set(instance, combined);
}

整體思路跟上面說的基本一致,理解思路,結合上面的注釋基本還是比較清楚的。

四、熱修復的一種實現原理

一個ClassLoader可以包含多個dex文件,每個dex文件是一個Element,多個dex文件排列成一個有序的數組dexElements,當找類的時候,會按順序遍歷dex文件,然後從當前遍歷的dex文件中找類,如果找類則返回,如果找不到從下一個dex文件繼續查找。

理論上,如果在不同的dex中有相同的類存在,那麼會優先選擇排在前面的dex文件的類,如下圖:
這裡寫圖片描述

所以,如果某些類需要修復,我們可以把有問題的類打包到一個dex(patch.dex)中去,然後把這個dex插入到Elements的最前面,如下圖:
這裡寫圖片描述

 

  1. 上一頁:
  2. 下一頁:
熱門文章
閱讀排行版
Copyright © Android教程網 All Rights Reserved