關(guān)于Dex分包

  • Dex分包的由來

    分包的概念想必我們都不陌生了,因?yàn)橐粋€(gè)dex文件中的方法數(shù)使用一個(gè)short類型的字段來記錄方法數(shù),所以最多只能存儲(chǔ)2^16=65536個(gè)方法數(shù)(16是2個(gè)字節(jié)能表示的最大值)。隨著項(xiàng)目的迭代以及各種庫的引入,單個(gè)dex的方法數(shù)遲早會(huì)超過這個(gè)數(shù)值,因此,dex分包應(yīng)運(yùn)而生。

    所謂dex分包就是把原先apk中的單個(gè)dex文件分成多個(gè)dex文件,這樣就能使每個(gè)dex中的方法數(shù)控制在65536這個(gè)閾值之下。

  • 如何生成多個(gè)Dex文件

    dex是一種具有特定格式的文件,通過對class文件的優(yōu)化來生成,這意味著可以通過程序自動(dòng)將class文件轉(zhuǎn)換成dex文件,在android工具包中已經(jīng)有這樣的程序可供使用了,那就是dxd8

    使用dx命令將單個(gè)class文件轉(zhuǎn)成dex文件:

    dx --dex --no-strict --output=/path/result.dex Demo.class
    

    注意要加上--no-strict,可以防止包名匹配錯(cuò)誤,不加的話你需要在對應(yīng)包名的頂層父目錄中執(zhí)行,比如“com.mph.bpp.Demo”的話,你必須在com的所在目錄下執(zhí)行,且源文件制定為相對路徑"com/mph/bpp/Demo.class"。

    d8是一款用于取代 dx、更快的 Dex 編譯器,可以生成更小的 APK,在Android Studio3.1以上版本已經(jīng)成為默認(rèn)選項(xiàng)了。

    使用d8命令將單個(gè)class文件轉(zhuǎn)成dex文件:

    d8 --release path/Demo.class --output path/result.dex
    

    --release表示正式編譯,編譯 DEX 字節(jié)碼時(shí)不包含調(diào)試信息,相反的--debug則表示編譯 DEX 字節(jié)碼時(shí)在其中包含調(diào)試信息,例如調(diào)試符號(hào)表,此選項(xiàng)默認(rèn)處于啟用狀態(tài),因此不需要額外指定。

    如果沒指定--output則會(huì)輸出到當(dāng)前執(zhí)行目錄,默認(rèn)輸出為classed.dex。

    如果我們想要把多個(gè)class文件轉(zhuǎn)到同一個(gè)dex文件中,則只需要把源文件都放在一個(gè)jar包中,然后同樣的命令,源文件換成該jar包即可。

    借助這兩個(gè)工具,我們就可以指定任何的class文件打包成任意數(shù)量的dex文件,但需要注意的是,在生成dex的過程中,我們需要去計(jì)算每個(gè)class的方法數(shù),保證它們的總量不會(huì)超過65536的上限,基于這套流程,我們可以寫一個(gè)腳本來完成dex分包的過程,這就是手動(dòng)分包。

    在android5.0版本及以上,虛擬機(jī)開始支持多dex加載,在打包過程中,gradle會(huì)自動(dòng)檢測dex文件中的方法數(shù)是否超過65536,如果超過了則自動(dòng)拆成多dex文件,這個(gè)過程的原理其實(shí)就是我們上面所說的那樣,只不過在gradle自動(dòng)構(gòu)建中我們無法指定那些class放在同一個(gè)dex中,這就是自動(dòng)分包。

  • 自動(dòng)分包配置

    app/build.gradle :

    android{
          defaultConfig{
                  multiDexEnabled true
          }
    }
    

    然后指定你的BaseApp繼承MultiDexApplication,無法繼承的話可以重寫B(tài)aseApp的attachBaseContext方法:

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

    其實(shí)這就是MultiDexApplication中的實(shí)現(xiàn)。

    如果你沒有使用androidx,那么想要使用MultiDex類的話還需要添加'com.android.support:multidex:x.x.x'依賴。
    gradle的分包完全是自動(dòng)化的,也就是說你無法決定哪些類應(yīng)該放到主dex中,雖然它會(huì)自動(dòng)分析哪些類是應(yīng)用啟動(dòng)時(shí)必須的以及識(shí)別它們的引用類,但是對于一些不太可見的復(fù)雜依賴,比如從native代碼邏輯中創(chuàng)建的Java類對象的情況,這時(shí)在啟動(dòng)的時(shí)候就會(huì)發(fā)生ava.lang.NoClassDefFoundError。
    好在,gradle允許你配置哪些類放到主dex中:

    android {
      buildTypes {
          release {
              multiDexKeepProguard file('multidex-config.pro')
              ...
          }
      }
    }
    

    然后在build.gradle同一目錄下添加multidex-config.pro文件:

    -keep class com.example.MyClass
    -keep class com.example.MyClassToo
    -keep class com.example.** { *; } // All classes in the com.example package
    

    注意,這個(gè)配置只在minSdk為21以下才會(huì)生效,而且只會(huì)保證配置內(nèi)容添加到主dex,并不能保證其他未指定的不會(huì)添加到主dex。至于為什么這樣,因?yàn)檫@個(gè)配置是為了解決某些情況下虛擬機(jī)內(nèi)部檢測不到主dex中相關(guān)的關(guān)聯(lián)類的問題,可能是21及以上的虛擬機(jī)內(nèi)部已經(jīng)優(yōu)化好了,不需要額外配置了,這也關(guān)閉了我們手動(dòng)添加主dex的大門,也是出于安全的考慮吧。
    Tip1:因在實(shí)踐過程中發(fā)現(xiàn)multiDexKeepProguard中指定的class并沒有出現(xiàn)在classes.dex中,在查詢這個(gè)問題過程中看到了一個(gè)不起眼的回復(fù)說是minSdk21版本及以上不會(huì)執(zhí)行這個(gè)配置,但是我在官方文檔中并沒有找到這個(gè)說明,所以起初沒有當(dāng)真,沒想到真是這個(gè)問題。
    Tip2:在我的項(xiàng)目中使用了很多JetPack的庫,很多庫的版本都很高,minSdk需要最低21,當(dāng)改成20后會(huì)報(bào)錯(cuò),根據(jù)提示在AndroidManifest.xml中增加<uses-sdk tools:overrideLibrary="xxx1, xxx2"/> 即可通過編譯,比如<uses-sdk tools:overrideLibrary="androidx.navigation.compose"/>,哪個(gè)庫報(bào)錯(cuò)就寫哪個(gè),多個(gè)用逗號(hào)隔開,這里只是說能通過編譯,但可能會(huì)有運(yùn)行時(shí)錯(cuò)誤,我這里只是看看apk中的classes.dex里有沒有配置的類而已。
    Multidex庫也有所限制,一個(gè)是如果非主dex文件過大的話,在加載的時(shí)候可能會(huì)出現(xiàn)ANR;另一個(gè)是在4.0以下,linearalloc limit(虛擬機(jī)相關(guān)的)的大小不足以支撐dex的最大方法引用數(shù),這就會(huì)導(dǎo)致未滿65536的情況下依然會(huì)崩潰,因此在4.0一下要做好相關(guān)平臺(tái)的測試工作。
    使用代碼瘦身功能可以很大程度上避免這個(gè)問題:

    android {
      buildTypes {
          release {
              minifyEnabled true
              //shrinkResources true   //資源瘦身
              ...
          }
      }
    }
    

    minifyEnabled設(shè)置成true也可能讓原本多個(gè)dex合成一個(gè)dex文件。比如我在測試熱修復(fù)的demo中,debug模式下生成的apk沒有配置minifyEnabled屬性,因此會(huì)自動(dòng)被分成多個(gè)dex文件,但是在開啟了minifyEnabled的release模式下就只存在一個(gè)dex了。
    在我的熱修復(fù)demo中,我發(fā)現(xiàn),如果需要被修復(fù)的類存在于主dex中的話,則使用類加載機(jī)制插入dexElements的方式是無法奏效的,但是如果要被修復(fù)的類存在于非主dex中的時(shí)候就沒問題,于是,我猜測:
    被分到主dex中的class在應(yīng)用啟動(dòng)的時(shí)候就已經(jīng)被加載了(這里的加載指的是通過ClassLoader的loadClass方法加載過),哪怕尚未用到它,因此之后再去添加補(bǔ)丁的dexElements也不會(huì)被替換了。

  • MultiDex.install加載源碼分析

    在app啟動(dòng)時(shí),虛擬機(jī)只會(huì)加載apk中名為“classed.dex”的這個(gè)dex文件,也就是主dex,因此,不管是自動(dòng)分包還是手動(dòng)分包,最終我們還要手動(dòng)加載其他的dex文件,這里通過MultiDex.install流程來一探究竟。

    //dentifies if the current VM has a native support for multidex, meaning there is no need for additional installation by this library.
    private static final boolean IS_VM_MULTIDEX_CAPABLE =
                isVMMultidexCapable(System.getProperty("java.vm.version"));
    public static void install(Context context) {
        Log.i(TAG, "Installing application");
        if (IS_VM_MULTIDEX_CAPABLE) {
            Log.i(TAG, "VM has multidex support, MultiDex support library is disabled.");
            return;
        }
        ...
        try {
            ApplicationInfo applicationInfo = getApplicationInfo(context);
            if (applicationInfo == null) {
              Log.i(TAG, "No ApplicationInfo available, i.e. running on a test Context:"
                  + " MultiDex support library is disabled.");
              return;
            }
    
            doInstallation(context,
                    new File(applicationInfo.sourceDir),
                    new File(applicationInfo.dataDir),
                    CODE_CACHE_SECONDARY_FOLDER_NAME,
                    NO_KEY_PREFIX,
                    true);
    
        } catch (Exception e) {
            Log.e(TAG, "MultiDex installation failure", e);
            throw new RuntimeException("MultiDex installation failed (" + e.getMessage() + ").");
        }
        Log.i(TAG, "install done");
    }
    

    isVMMultidexCapable會(huì)判斷虛擬機(jī)時(shí)候有底層的加載分包的能力,如果虛擬機(jī)可以在底層直接加載dex分包的話就不需要在這里重復(fù)操作了。如果虛擬機(jī)層面沒有這個(gè)能力,那就需要MultiDex來做了。

    private static void doInstallation(Context mainContext, File sourceApk, File dataDir,
            String secondaryFolderName, String prefsKeyPrefix,
            boolean reinstallOnPatchRecoverableException) throws IOException,
                IllegalArgumentException, IllegalAccessException, NoSuchFieldException,
                InvocationTargetException, NoSuchMethodException, SecurityException,
                ClassNotFoundException, InstantiationException {
        synchronized (installedApk) {
              //是否已加載過
            if (installedApk.contains(sourceApk)) {
                return;
            }
            installedApk.add(sourceApk);
    
            if (Build.VERSION.SDK_INT > MAX_SUPPORTED_SDK_VERSION) {
                  //打印log:版本大于20的通常虛擬機(jī)都有multidex能力,能走到這里說明不支持,因此是個(gè)需要注意的地方
            }
    
            ClassLoader loader = getDexClassloader(mainContext);
            if (loader == null) {
                return;
            }
    
            try {
              clearOldDexDir(mainContext);
            } catch (Throwable t) {
              ...
            }
                  //路徑為context.getApplicationInfo().getDataDir()/code_cache/secondary-dexes
            File dexDir = getDexDir(mainContext, dataDir, secondaryFolderName);
              //sourceApk路徑為context.getApplicationInfo().getSourceDir()獲取的值,比如我用的模擬器上的值是“/data/app/~~v1lKTsJtGX9XmDeVroYyxA==/com.mph.bpp-vhn5lJ9Ml_iXQ9hedBHCKQ==/base.apk”
            MultiDexExtractor extractor = new MultiDexExtractor(sourceApk, dexDir);
            IOException closeException = null;
            try {
                List<? extends File> files =
                        extractor.load(mainContext, prefsKeyPrefix, false);
                try {
                    installSecondaryDexes(loader, dexDir, files);
                } catch (IOException e) {
                    if (!reinstallOnPatchRecoverableException) {
                        throw e;
                    }
                    files = extractor.load(mainContext, prefsKeyPrefix, true);
                    installSecondaryDexes(loader, dexDir, files);
                }
            } finally {
                try {
                    extractor.close();
                } catch (IOException e) {
                    closeException = e;
                }
            }
            if (closeException != null) {
                throw closeException;
            }
        }
    }
    

    然后調(diào)用MultiDexExtractor的load方法加載dex文件:

    List<? extends File> load(Context context, String prefsKeyPrefix, boolean forceReload)
            throws IOException {
        ...
        List<ExtractedDex> files;
        if (!forceReload && !isModified(context, sourceApk, sourceCrc, prefsKeyPrefix)) {
            try {
                  //加載已經(jīng)解壓過的dex文件
                files = loadExistingExtractions(context, prefsKeyPrefix);
            } catch (IOException ioe) {
                  //如果發(fā)生異常了就直接解壓dex文件
                files = performExtractions();
                putStoredApkInfo(context, prefsKeyPrefix, getTimeStamp(sourceApk), sourceCrc,
                        files);
            }
        } else {
                  //強(qiáng)行解壓文件
            files = performExtractions();
            putStoredApkInfo(context, prefsKeyPrefix, getTimeStamp(sourceApk), sourceCrc,
                    files);
        }
        return files;
    }
    

    注意上面?zhèn)鬟^來的最后一個(gè)參數(shù)forceReload,如果doInstallation的第一遍加載發(fā)生異常了,會(huì)再次調(diào)用load方法,傳入true來調(diào)用performExtractions方法。

    我們先看performExtractions方法:

    private List<ExtractedDex> performExtractions() throws IOException {
          //EXTRACTED_NAME_EXT是.classes,sourceApk.getName()值為base.apk,因此extractedFilePrefix值為base.apk.classes
        final String extractedFilePrefix = sourceApk.getName() + EXTRACTED_NAME_EXT;
        clearDexDir();
        List<ExtractedDex> files = new ArrayList<ExtractedDex>();
          //開始解壓base.apk
        final ZipFile apk = new ZipFile(sourceApk);
        try {
            //secondaryNumber是除主dex之外的dex名字后綴,比如主dex是classes.dex,則其余的dex會(huì)是classes2.dex、classes3.dex等
            int secondaryNumber = 2;
              //嘗試獲取base.apk中的非主dex文件,DEX_PREFIX是classes,DEX_SUFFIX是.dex
            ZipEntry dexFile = apk.getEntry(DEX_PREFIX + secondaryNumber + DEX_SUFFIX);
              //如果存在其他的dex
            while (dexFile != null) {
                  //創(chuàng)建要輸出的dex文件,比如base.apk.classes3.zip,EXTRACTED_SUFFIX是.zip
                String fileName = extractedFilePrefix + secondaryNumber + EXTRACTED_SUFFIX;
                ExtractedDex extractedFile = new ExtractedDex(dexDir, fileName);
                  //files最終保存這些dex文件
                files.add(extractedFile);
                int numAttempts = 0;
                boolean isExtractionSuccessful = false;
                  //MAX_EXTRACT_ATTEMPTS是3,表示不成功的話最多嘗試解壓3次
                while (numAttempts < MAX_EXTRACT_ATTEMPTS && !isExtractionSuccessful) {
                    numAttempts++;
                    //解壓
                    extract(apk, dexFile, extractedFile, extractedFilePrefix);
                    try {
                          //賦值crc(Cyclic Redundancy Check(循環(huán)冗余校驗(yàn)))
                        extractedFile.crc = getZipCrc(extractedFile);
                        isExtractionSuccessful = true;
                    } catch (IOException e) {
                        isExtractionSuccessful = false;
                    }
                      
                    if (!isExtractionSuccessful) {
                        // Delete the extracted file
                        extractedFile.delete();
                    }
                }
                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;
    }
    

    看一下extract方法:

    private static void extract(ZipFile apk, ZipEntry dexFile, File extractTo,
            String extractedFilePrefix) throws IOException, FileNotFoundException {
    
        InputStream in = apk.getInputStream(dexFile);
        ZipOutputStream out = null;
          //tmp就是要輸出的dex文件,路徑和performExtractions方法中的extractedFile的路徑是一樣的,只不過名字多了個(gè)tmp的前綴
        File tmp = File.createTempFile("tmp-" + extractedFilePrefix, EXTRACTED_SUFFIX,
                extractTo.getParentFile());
        try {
              //開始壓縮操作,其實(shí)就是把dexFile文件復(fù)制到tmp
            out = new ZipOutputStream(new BufferedOutputStream(new FileOutputStream(tmp)));
            try {
                  //dexFile會(huì)復(fù)制到tmp這個(gè)zip中并命名為classes.dex
                ZipEntry classesDex = new ZipEntry("classes.dex");
                // keep zip entry time since it is the criteria used by Dalvik
                classesDex.setTime(dexFile.getTime());
                out.putNextEntry(classesDex);
                byte[] buffer = new byte[BUFFER_SIZE];
                int length = in.read(buffer);
                while (length != -1) {
                    out.write(buffer, 0, length);
                    length = in.read(buffer);
                }
                out.closeEntry();
            } finally {
                out.close();
            }
              //最終的dex文件為了安全考慮要設(shè)置為只讀
            if (!tmp.setReadOnly()) {
                throw new IOException("Failed to mark readonly \"" + tmp.getAbsolutePath() +
                        "\" (tmp of \"" + extractTo.getAbsolutePath() + "\")");
            }
              //注意File的renameTo方法,它的原理是會(huì)創(chuàng)建extractTo,然后把tmp的內(nèi)容復(fù)制過去
            if (!tmp.renameTo(extractTo)) {
                throw new IOException("Failed to rename \"" + tmp.getAbsolutePath() +
                        "\" to \"" + extractTo.getAbsolutePath() + "\"");
            }
        } finally {
            closeQuietly(in);
              //刪除tmp文件
            tmp.delete(); 
        }
    }
    

    總結(jié)一下extract方法就是:把dexFile復(fù)制到extractedFile中去。

    回到load方法中,performExtractions方法執(zhí)行完之后緊接著會(huì)調(diào)用putStoredApkInfo方法,這個(gè)方法中會(huì)把解壓后的文件關(guān)鍵信息保存到SharedPreferences中,比如TIME_STAMP、CRC(Cyclic Redundancy Check(循環(huán)冗余校驗(yàn)))、DEX_NUMBER(非主dex文件數(shù)量)等信息:

    private static void putStoredApkInfo(Context context, String keyPrefix, long timeStamp,
            long crc, List<ExtractedDex> extractedDexes) {
        SharedPreferences prefs = getMultiDexPreferences(context);
        SharedPreferences.Editor edit = prefs.edit();
        edit.putLong(keyPrefix + KEY_TIME_STAMP, timeStamp);
        edit.putLong(keyPrefix + KEY_CRC, crc);
        edit.putInt(keyPrefix + KEY_DEX_NUMBER, extractedDexes.size() + 1);
        int extractedDexId = 2;
        for (ExtractedDex dex : extractedDexes) {
            edit.putLong(keyPrefix + KEY_DEX_CRC + extractedDexId, dex.crc);
            edit.putLong(keyPrefix + KEY_DEX_TIME + extractedDexId, dex.lastModified());
            extractedDexId++;
        }
        edit.commit();
    }
    

    現(xiàn)在再來看loadExistingExtractions方法就很清晰了:

    private List<ExtractedDex> loadExistingExtractions(
            Context context,
            String prefsKeyPrefix)
            throws IOException {
          //也就是base.apk.classes
        final String extractedFilePrefix = sourceApk.getName() + EXTRACTED_NAME_EXT;
          //獲取之前保存信息的SharedPreferences
        SharedPreferences multiDexPreferences = getMultiDexPreferences(context);
          //SharedPreferences獲取之前保存的非主dex數(shù)量
        int totalDexNumber = multiDexPreferences.getInt(prefsKeyPrefix + KEY_DEX_NUMBER, 1);
        final List<ExtractedDex> files = new ArrayList<ExtractedDex>(totalDexNumber - 1);
          //根據(jù)保存的信息來構(gòu)造ExtractedDex
        for (int secondaryNumber = 2; secondaryNumber <= totalDexNumber; secondaryNumber++) {
              //找到之前含有dexFile的壓縮文件
            String fileName = extractedFilePrefix + secondaryNumber + EXTRACTED_SUFFIX;
            ExtractedDex extractedFile = new ExtractedDex(dexDir, fileName);
            if (extractedFile.isFile()) {
                  //安全驗(yàn)證CRC和time,來保證這中間沒被惡意修改過
                extractedFile.crc = getZipCrc(extractedFile);
                long expectedCrc = multiDexPreferences.getLong(
                        prefsKeyPrefix + KEY_DEX_CRC + secondaryNumber, NO_VALUE);
                long expectedModTime = multiDexPreferences.getLong(
                        prefsKeyPrefix + KEY_DEX_TIME + secondaryNumber, NO_VALUE);
                long lastModified = extractedFile.lastModified();
                if ((expectedModTime != lastModified)
                        || (expectedCrc != extractedFile.crc)) {
                    throw new IOException("Invalid extracted dex: " + extractedFile +
                            " (key \"" + prefsKeyPrefix + "\"), expected modification time: "
                            + expectedModTime + ", modification time: "
                            + lastModified + ", expected crc: "
                            + expectedCrc + ", file crc: " + extractedFile.crc);
                }
                files.add(extractedFile);
            } else {
                throw new IOException("Missing extracted secondary dex file '" +
                        extractedFile.getPath() + "'");
            }
        }
        return files;
    }
    

    到此,我們已經(jīng)清楚了整個(gè)讀取dexFile的過程,下面我們繼續(xù)看如何加載到ClassLoader的。

    MultiDexExtractor的load方法完成后會(huì)拿到所有的含有dexFile的壓縮zip(內(nèi)部都是叫classes.dex),然后調(diào)用installSecondaryDexes(loader, dexDir, files):

    private static void installSecondaryDexes(ClassLoader loader, File dexDir,
        List<? extends File> files)
            throws IllegalArgumentException, IllegalAccessException, NoSuchFieldException,
            InvocationTargetException, NoSuchMethodException, IOException, SecurityException,
            ClassNotFoundException, InstantiationException {
        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);
            } else {
                V4.install(loader, files);
            }
        }
    }
    

    這里會(huì)根據(jù)不同的SDK版本來分別執(zhí)行不同的install方法,以V19為例:

    static void install(ClassLoader loader,
            List<? extends File> additionalClassPathEntries,
            File optimizedDirectory)
                    throws IllegalArgumentException, IllegalAccessException,
                    NoSuchFieldException, InvocationTargetException, NoSuchMethodException,
                    IOException {
        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);
    
            IOException exception = new IOException("I/O exception during makeDexElement");
            exception.initCause(suppressedExceptions.get(0));
            throw exception;
        }
    }
    
    /**
     * 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 (Object[]) makeDexElements.invoke(dexPathList, files, optimizedDirectory,
                suppressedExceptions);
    }
    

    這里會(huì)利用反射拿到傳進(jìn)來的ClassLoader的pathList屬性,類型是DexPathList,再次反射獲取它的dexElements屬性,他是個(gè)Element數(shù)組,makeDexElements方法中利用反射調(diào)用DexPathList的makeDexElements方法構(gòu)造一個(gè)Element數(shù)組,里面包含我們之前獲取的所有非主dex的zip文件,然后在expandFieldArray方法中把pathList中原有的dexElements數(shù)組和新創(chuàng)建的整合到一起(新創(chuàng)建的放在后面,也就是主dex的位置在非主dex前面),最后再設(shè)置到pathList的dexElements屬性上。

    那為什么要這么做呢?那就得提到android中的類加載機(jī)制了。

  • Android中的類加載機(jī)制

    Android的類加載基于Java的類加載機(jī)制,都是雙親委托機(jī)制,什么是雙親委托呢?就是每次加載一個(gè)類的時(shí)候都先從更上一級的父加載器中查找,如果父加載器已經(jīng)加載過了則直接使用不再加載,這樣做可以避免重復(fù)加載,最重要的是防止系統(tǒng)類的惡意修改和替換。

    在Android系統(tǒng)中有兩個(gè)ClassLoader可以供我們加載自定義資源,一個(gè)是DexClassLoader,一個(gè)是PathClassLoader,他們的區(qū)別就是DexClassLoader多了一個(gè)可以指定odex(dex的優(yōu)化文件)的輸出目錄的選項(xiàng),它們都繼承自BaseDexClassLoader。

    當(dāng)我們用到某個(gè)類的時(shí)候會(huì)由虛擬機(jī)調(diào)用ClassLoader的loadClass方法:

    protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
            //之前是否已加載過
            Class<?> c = findLoadedClass(name);
            if (c == null) {
                try {
                      //嘗試從父加載器加載(最頂層是BootClassLoader)
                    if (parent != null) {
                        c = parent.loadClass(name, false);
                    } else {
                          //默認(rèn)為空
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                }
    
                if (c == null) {
                    //如果都未找到則調(diào)用自身的findClass方法
                    c = findClass(name);
                }
            }
            return c;
    }
    

    BaseDexClassLoader中實(shí)現(xiàn)了findClass方法:

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

    可見,這里會(huì)調(diào)用pathList的findClass方法:

    public Class<?> findClass(String name, List<Throwable> suppressed) {
        for (Element element : dexElements) {
            Class<?> clazz = element.findClass(name, definingContext, suppressed);
            if (clazz != null) {
                return clazz;
            }
        }
    
        if (dexElementsSuppressedExceptions != null) {
            suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions));
        }
        return null;
    }
    

    看到這里,我們就明白了,原來最終會(huì)從dexElements中查找類啊,因此也就理解了上面的dex加載操作。

    這也是熱修復(fù)中類加載修復(fù)方式的原理依據(jù),也是插件化思想的原理依據(jù)之一。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,117評論 6 537
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,860評論 3 423
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 177,128評論 0 381
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,291評論 1 315
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 72,025評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,421評論 1 324
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,477評論 3 444
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,642評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 49,177評論 1 335
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 40,970評論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 43,157評論 1 371
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,717評論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,410評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,821評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,053評論 1 289
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 51,896評論 3 395
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 48,157評論 2 375

推薦閱讀更多精彩內(nèi)容