1. 程式人生 > >Android熱修復學習(二)

Android熱修復學習(二)

Multidex

在上一篇(一)中,我們知道應用載入class會用到PathClassLoader,最終會呼叫DexPathList的findClass()方法,通過輪詢Element陣列用DexFile來載入類。而每個Element又對應一個dex單元(檔案),所以我們可以改變這個陣列的大小和順序做一些動態功能。前面我們看到這個陣列的初始化是通過呼叫DexPathList的makeDexElements()來生成,如果我們拿到DexPathList的例項,則可以反射呼叫該方法來新增Element。

當 Android 系統安裝一個應用的時候,有一步是對 Dex 進行優化,這個過程有一個專門的工具來處理,叫 DexOpt。DexOpt 是在第一次載入 
Dex 檔案的時候執行的。這個過程會生成一個 ODEX 檔案,即 Optimised Dex。執行 ODEX 的效率會比直接執行 Dex 檔案的效率要高很多。

但是在早期的 Android 系統中,DexOpt 有兩個問題。
(一):DexOpt 會把每一個類的方法 id 檢索起來,存在一個連結串列結構裡面,但是這個連結串列的長度是用一個 short 型別來儲存的,導致了方法 
id 的數目不能夠超過65536個。當一個專案足夠大的時候,顯然這個方法數的上限是不夠的。(二):Dexopt 使用 LinearAlloc 來儲存應用
的方法資訊。Dalvik LinearAlloc 是一個固定大小的緩衝區。在Android 版本的歷史上,LinearAlloc 分別經歷了4M/5M/8M/16M限制。
Android 2.2和2.3的緩衝區只有5MB,Android 4.x提高到了8MB 或16MB。當方法數量過多導致超出緩衝區大小時,也會造成dexopt崩潰。

儘管在新版本的 Android 系統中,DexOpt 修復了方法數65K的限制問題,並且擴大了 LinearAlloc 限制,但是我們仍然需要對低版本的Android 
系統做相容。

針對上訴的問題,google官方推出了multidex包做相容。下面我們來看multidex包是怎麼做的,它提供了三種接入方式,不過最終都是在Application的attachBaseContext()方法中呼叫MultiDex的install()方法。

public static void install(Context context) {
        Log.i(TAG, "install");
        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) { return; } synchronized (installedApk) { String apkPath = applicationInfo.sourceDir; if (installedApk.contains(apkPath)) { return; } installedApk.add(apkPath); …… …… ClassLoader loader; try { loader = context.getClassLoader(); } catch (RuntimeException e) { return; } if (loader == null) { return; } …… …… File dexDir = new File(applicationInfo.dataDir, SECONDARY_FOLDER_NAME); List<File> files = MultiDexExtractor.load(context, applicationInfo, dexDir, false); if (checkValidZipFiles(files)) { installSecondaryDexes(loader, dexDir, files); } else { files = MultiDexExtractor.load(context, applicationInfo, dexDir, true); if (checkValidZipFiles(files)) { installSecondaryDexes(loader, dexDir, files); } else { throw new RuntimeException("Zip files were not valid."); } } } } catch (Exception e) { throw new RuntimeException("Multi dex installation failed (" + e.getMessage() + ")."); } }

其中IS_VM_MULTIDEX_CAPABLE是通過虛擬機器的版本號來判斷的。

private static final int VM_WITH_MULTIDEX_VERSION_MAJOR = 2;
private static final int VM_WITH_MULTIDEX_VERSION_MINOR = 1;
private static final int MIN_SDK_VERSION = 4;

static boolean isVMMultidexCapable(String versionString) {
        boolean isMultidexCapable = false;
        if (versionString != null) {
            Matcher matcher = Pattern.compile("(\\d+)\\.(\\d+)(\\.\\d+)?").matcher(versionString);
            if (matcher.matches()) {
                try {
                    int major = Integer.parseInt(matcher.group(1));
                    int minor = Integer.parseInt(matcher.group(2));
                    isMultidexCapable = (major > VM_WITH_MULTIDEX_VERSION_MAJOR)
                            || ((major == VM_WITH_MULTIDEX_VERSION_MAJOR)
                                    && (minor >= VM_WITH_MULTIDEX_VERSION_MINOR));
                } catch (NumberFormatException e) {
                    // let isMultidexCapable be false
                }
            }
        }
        return isMultidexCapable;
    }

可以看出當虛擬機器的版本號大於等於2.1的時候,虛擬機器本身已經支援多dex載入了,而主版本號2表示art,所以說基本上從5.0開始google已經解決了這個問題。而支援的最小版本MIN_SDK_VERSION為4,所以最小隻支援到1.6。

接下來獲取應用本身資訊ApplicationInfo,通過同步處理和把包路徑放入列表中防止多次處理。然後通過方法MultiDexExtractor.load()開始載入apk中的其它dex檔案,並且每個都以zip存放的形式拷貝到指定的資料夾下。可以看到該資料夾路徑為:

private static final String SECONDARY_FOLDER_NAME = "code_cache" + File.separator +"secondary-dexes";

File dexDir = new File(applicationInfo.dataDir, SECONDARY_FOLDER_NAME);

而applicationInfo.dataDir為應用本身的內部路徑,dataDir為分配給此Application的存放資料的位置,通常是/data/data/packageName/。通過該路徑我們可以通過DexClassLoader來載入,前面(一)中說過,DexClassLoader載入類儲存優化後的檔案路徑必須是應用使用者自己的。我們來看multidex又是怎麼做的。

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

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(loader, "dexElementsSuppressedExceptions");
                IOException[] dexElementsSuppressedExceptions =
                        (IOException[]) suppressedExceptionsField.get(loader);

                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(loader, 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);
        }
}

從上面可以看出multidex會根據不同的版本來做不同的處理,以V19為例,可以看出是通過獲取到當前應用的classloader,即為PathClassLoader,然後通過反射獲取到他的DexPathList屬性物件pathList,然後繼續通過反射呼叫pathList的makeDexElements()方法把之前拷貝的其它dex的zip檔案轉化為Element[],然後合併到pathList現有的陣列中去,這樣就完成了其它dex的動態載入。V14和V19類似,而 V4中沒有Element型別,則直接採用DexFile的loadloadDex(String sourcePathName, String outputPathName,int flags)方法來把zip檔案轉化為DexFile[],從(一)中我們可以知道DexClassLoader最終也會走到該方法,而且第二個引數為我們上面說的dataDir路徑下,為應用本身的路徑。

小結:
1.雖然multidex可以很方便的解決64k的問題,不過可以看出它是在應用啟動時主執行緒來載入的,而且第一次載入dex檔案,會觸發dexopt,首次啟動可能會造成應用啟動緩慢。所以可能需要考慮非同步載入和按需動態載入。
2.multidex拆分dex為自己分析的過程,從參考文章看出可能會出現啟動必要的類沒有打包進主dex檔案的情況,所以可能需要自己干預拆分dex的過程(通過–multi-dex、–main-dex-list=、–minimal-main-dex、–set-max-idx-number等引數)