How to use and implement Android multidex

Time:2020-2-13

How to use and implement Android multidex

In Android, a DEX file can store up to 65536 methods, which is a short type range. However, with the increasing number of application methods, when the DEX file breaks through the number of 65536 methods, an exception will be thrown during packaging.

To solve this problem, Android 5.0 launched the official solution: multidex.

  • During packaging, an application is divided into multiple DEX, such as classes. DEX, classes2. DEX, and classes3. DEX. When loading, these DEX are appended to the array corresponding to the dexpathlist, which solves the limitation of the number of methods.
  • After andorid 5.0, art virtual machine naturally supports multidex.
  • Before andorid 5.0, the system only loaded one main DEX, and other DEX was loaded by multi DEX.

I. use

How to use it is best to refer to the official documents of Google and write them in detail:

Applications with more than 64K configuration methods

Here is a brief description:

1. Minsdkversion is 21 or higher

If it isandroid 5.0The above devices only need to be set tomultiDexEnabled true

android {
    defaultConfig {
        ...
        minSdkVersion 21 
        targetSdkVersion 26
        multiDexEnabled true
    }
    ...
}

2. Minsdkversion is 20 or less

If adaptation is requiredandroid 5.0The following devices need to use Dalvik executable subcontracting support library

android {
    defaultConfig {
        ...
        minSdkVersion 15 
        targetSdkVersion 26
        multiDexEnabled true
    }
    ...
}

dependencies {
  compile 'com.android.support:multidex:1.0.3'
}

Java code aspect, inheritanceMultiDexApplicationOr inApplicationAdd inMultiDex.install(this);

//Inherit multidexapplication
public class MyApplication extends MultiDexApplication { ... }


//Or add multidex. Install (this) to the application;
public class MyApplication extends Application {
  @Override
  protected void attachBaseContext(Context base) {
     super.attachBaseContext(base);
     MultiDex.install(this);
  }
}

2、 Android 5.0 and the following multidex principles

Note:
Source based versioncom.android.support:multidex:1.0.3

Through the application of Dalvik executable subcontracting support library and configuration methods with more than 64K, we learned that:

  • android 5.0FollowingDalvik virtual machine Only one can be loadedMain class.dex
  • android.support.multidex.MultiDex.install(this)Yes.android 5.0FollowingDalvik virtual machine Compatibility;

Here we are divided into two parts, one is the loading of DEX file, the other is the extraction of DEX file.

2.1 loading DEX files

Now by trackingMultiDex.install(this);Source code, understand its implementation principle.

MultiDex.install(this);

TrackMultiDex.install(this);Source code

public static void install(Context context) {
    //If the system version is greater than Android 5.0, multidex is naturally supported
    if (IS_VM_MULTIDEX_CAPABLE) {
        Log.i("MultiDex", "VM has multidex support, MultiDex support library is disabled.");
    } 
    //Exception thrown when system version is lower than Android 1.6
    else if (VERSION.SDK_INT < 4) {
        throw new RuntimeException("MultiDex installation failed. SDK " + VERSION.SDK_INT + " is unsupported. Min SDK version is " + 4 + ".");
    } 
    // android 1.6 < android < android 5.0
    else {
        try {
            //Get the current application information. If the application information does not exist, return
            ApplicationInfo applicationInfo = getApplicationInfo(context);
            if (applicationInfo == null) {
                Log.i("MultiDex", "No ApplicationInfo available, i.e. running on a test Context: MultiDex support library is disabled.");
                return;
            }
            // MultiDex
            // sourceDir: /data/app/com.xiaxl.demo-2/base.apk
            // dataDir:   /data/user/0/com.xiaxl.demo
            doInstallation(context, new File(applicationInfo.sourceDir), new File(applicationInfo.dataDir), "secondary-dexes", "", true);
        } catch (Exception var2) {
            Log.e("MultiDex", "MultiDex installation failure", var2);
            throw new RuntimeException("MultiDex installation failed (" + var2.getMessage() + ").");
        }

        Log.i("MultiDex", "install done");
    }
}

In the upper code, judge 1.6 < Android < Android 5.0 and throw an exception if it is lower than 1.6; if it is higher than 5.0, multidex is naturally supported, so ignore

  • If the system version is greater than Android 5.0ART virtual machineNatural support for multidex
  • Exception thrown when system version is lower than Android 1.6
  • Doinstallation multidex processing

Trace multidex.doinstallation

Trace multidex.doinstallation and check the implementation principle of multidex

//Relevant entry parameters
// sourceDir: /data/app/com.xiaxl.demo-2/base.apk
// dataDir:   /data/user/0/com.xiaxl.demo
// secondaryFolderName: "secondary-dexes"
// prefsKeyPrefix: ""
// reinstallOnPatchRecoverableException: true
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 {
    //Apk installed
    Set var6 = installedApk;
    // synchronization
    synchronized(installedApk) {
        //If / data / APP / com.xixl.demo-2/base.apk is not installed
        if (!installedApk.contains(sourceApk)) {
            //Add to the installedapk collection
            installedApk.add(sourceApk);
            //Android system version is about 5.0 ("wrong version number of Java. VM. Version"). It naturally supports multidex
            if (VERSION.SDK_INT > 20) {
                Log.w("MultiDex", "MultiDex is not guaranteed to work in SDK version " + VERSION.SDK_INT + ": SDK version higher than " + 20 + " 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") + "\"");
            }
            //Get classloader according to context
            ClassLoader loader;
            try {
                //Get classloader, which is actually PathClassLoader
                loader = mainContext.getClassLoader();
            } catch (RuntimeException var25) {
                Log.w("MultiDex", "Failure while trying to obtain Context class loader. Must be running in test mode. Skip patching.", var25);
                return;
            }
            //Classloader failed to get
            if (loader == null) {
                Log.e("MultiDex", "Context class loader is null. Must be running in test mode. Skip patching.");
            }
            //  
            else {
                //Clear the old cache DEX directory. The source cache directory is "/ data / user / 0 / ${packagename} / files / secondary dexes"
                //Clear / data / user / 0 / com.xixl.demo/files/secondary-dexes
                try {
                    clearOldDexDir(mainContext);
                } catch (Throwable var24) {
                    Log.w("MultiDex", "Something went wrong when trying to clear old MultiDex extraction, continuing without cleaning.", var24);
                }
                
                //Create a new directory to store DEX. The path is "/ data / user / 0 / ${packagename} / code_cache / secondary dexes", which is used to store the optimized DEX files
                //Create the / data / user / 0 / com.xixl.demo/code'cache/secondary-dexes directory
                File dexDir = getDexDir(mainContext, dataDir, secondaryFolderName);
                
                //Use the utility class of multidexextractor to extract the Dex in APK into the dexdir directory. The returned files collection may be empty, indicating that there is no secondarydex
                //Do not force reload, that is, if it has been extracted, it can be directly used from the cache directory, which is faster
                // sourceApk: /data/app/com.xiaxl.demo-2/base.apk
                // dexDir: /data/user/0/com.xiaxl.demo/code_cache/secondary-dexes
                MultiDexExtractor extractor = new MultiDexExtractor(sourceApk, dexDir);
                IOException closeException = null;

                try {
                    // prefsKeyPrefix: ""
                    //Return to DEX file list
                    List files = extractor.load(mainContext, prefsKeyPrefix, false);
                    try {
                        //Install secondarydex
                        // /data/user/0/com.xiaxl.demo/code_cache/secondary-dexes
                        installSecondaryDexes(loader, dexDir, files);
                    } catch (IOException var26) {
                        if (!reinstallOnPatchRecoverableException) {
                            throw var26;
                        }

                        Log.w("MultiDex", "Failed to install extracted secondary dex files, retrying with forced extraction", var26);
                        files = extractor.load(mainContext, prefsKeyPrefix, true);
                        installSecondaryDexes(loader, dexDir, files);
                    }
                } finally {
                    try {
                        extractor.close();
                    } catch (IOException var23) {
                        closeException = var23;
                    }

                }

                if (closeException != null) {
                    throw closeException;
                }
            }
        }
    }
}

Ignoring the extraction logic and verification logic of DEX files, the above code mainly does the following three things:

  • Empty cache directory “/ data / user / 0 / ${packagename} / files / secondary dexes”
  • Use the multi dexextractor tool to extract the Dex in the APK into the directory “/ data / user / 0 / ${packagename} / code_cache / secondary dexes”
  • Load the DEX under the directory “/ data / user / 0 / ${packagename} / code_cache / secondary dexes”

View belowMultiDex.installSecondaryDexesMethods, understandingMultiDexThe concrete realization of

MultiDex.V4.install(loader, files);

// dexDir:  /data/user/0/com.xiaxl.demo/code_cache/secondary-dexes
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 (VERSION.SDK_INT >= 19) {
            MultiDex.V19.install(loader, files, dexDir);
        } else if (VERSION.SDK_INT >= 14) {
            MultiDex.V14.install(loader, files);
        } else {
            MultiDex.V4.install(loader, files);
        }
    }
}

Different versions of Android systems have different class loading mechanisms, so they are divided intoV19, V14 and V4Installation in three cases.

Here we see the source code of the next v19

private static final class V19 {
    private V19() {
    }
    //Additionalclasspathentries: DEX list
    // optimizedDirectory: /data/user/0/com.xiaxl.demo/code_cache/secondary-dexes
    static void install(ClassLoader loader, List<? extends File> additionalClassPathEntries, File optimizedDirectory) throws IllegalArgumentException, IllegalAccessException, NoSuchFieldException, InvocationTargetException, NoSuchMethodException, IOException {
        //The loader passed is PathClassLoader, and the findfidld() method finds the pathlist attribute in the parent baseclassloader
        //Get the pathlist property in basedexclassloader
        // this.pathList = new DexPathList(this, dexPath, libraryPath, optimizedDirectory);
        Field pathListField = MultiDex.findField(loader, "pathList");
        Object dexPathList = pathListField.get(loader);
        //Add a DEX file to the end of the dexelements array in the dexpathlist
        ArrayList<IOException> suppressedExceptions = new ArrayList();
        MultiDex.expandFieldArray(dexPathList, "dexElements", makeDexElements(dexPathList, new ArrayList(additionalClassPathEntries), optimizedDirectory, suppressedExceptions));
        //The next step is to add some IO exception information, because there will be some IO operations when calling makedexelements of dexpathlist, and corresponding exceptions may occur
        if (suppressedExceptions.size() > 0) {
            Iterator var6 = suppressedExceptions.iterator();

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

            Field suppressedExceptionsField = MultiDex.findField(dexPathList, "dexElementsSuppressedExceptions");
            IOException[] dexElementsSuppressedExceptions = (IOException[])((IOException[])suppressedExceptionsField.get(dexPathList));
            if (dexElementsSuppressedExceptions == null) {
                dexElementsSuppressedExceptions = (IOException[])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((Throwable)suppressedExceptions.get(0));
            throw exception;
        }
    }
    
    //Call the dexpathlist ා makedexelements() method by reflection
    // dexPathList: DexPathList
    //Files: DEX file list
    private static Object[] makeDexElements(Object dexPathList, ArrayList<File> files, File optimizedDirectory, ArrayList<IOException> suppressedExceptions) throws IllegalAccessException, InvocationTargetException, NoSuchMethodException {
        //Load "DEX file" through makedexelements method of dexpathlist
        Method makeDexElements = MultiDex.findMethod(dexPathList, "makeDexElements", ArrayList.class, File.class, ArrayList.class);
        return (Object[])((Object[])makeDexElements.invoke(dexPathList, files, optimizedDirectory, suppressedExceptions));
    }
}

Through the install() method of v19, the problem about how to load DEX files by multidex is clear:

  • Append the DEX file in the APK file except the main DEX file to thePathClassLoader (baseclassloader)inDexPathListde Element[]Array. In this way, when loading a class, it will traverse all the DEX files, ensuring that the packed classes can be loaded normally.

~~~~~~~~This completes the loading of DEX. Next, check the extraction logic of DEX~~~~~~~~~

2.2 extraction of DEX files

As I said before:
MultiDexExtractorThe function of this tool class is todexFile extraction to/data/user/0/com.xiaxl.demo/code_cache/secondary-dexesDirectory

Construction method of multidexextractor

// sourceApk:  /data/app/com.xiaxl.demo-2/base.apk
// dexDir:  /data/user/0/com.xiaxl.demo/code_cache/secondary-dexes
MultiDexExtracto(File sourceApk, File dexDir) throws IOException {
    this.sourceApk = sourceApk;
    this.dexDir = dexDir;
    //Cyclic redundancy check code (CRC)
    this.sourceCrc = getZipCrc(sourceApk);
    //Create / data / user / 0 / com.xixl.demo/code'cache/secondary-dexes/multidex.lock
    File lockFile = new File(dexDir, "MultiDex.lock");
    //Access to the contents of the file can be read or written. Any location where the file can be accessed is applicable to the file composed of records with known size
    //Read and write to / data / user / 0 / com.xixl.demo/code ﹐ cache / secondary dexes / multidex.lock
    this.lockRaf = new RandomAccessFile(lockFile, "rw");

    try {
        //Back to file channel
        this.lockChannel = this.lockRaf.getChannel();

        try {
            Log.i("MultiDex", "Blocking on lock " + lockFile.getPath());
            this.cacheLock = this.lockChannel.lock();
        } catch (RuntimeException | Error | IOException var5) {
            closeQuietly(this.lockChannel);
            throw var5;
        }

        Log.i("MultiDex", lockFile.getPath() + " locked");
    } catch (RuntimeException | Error | IOException var6) {
        closeQuietly(this.lockRaf);
        throw var6;
    }
}

MultiDexExtractor.load

In APKdexExtraction of documents

//Return to DEX file list
// prefsKeyPrefix: ""
// forceReload: false
List<? extends File> load(Context context, String prefsKeyPrefix, boolean forceReload) throws IOException {
    //Multidexextractor not available
    if (!this.cacheLock.isValid()) {
        throw new IllegalStateException("MultiDexExtractor was closed");
    } else {
        List files;
        // forceReload ==false;
        // isModified == true;
        //If there is no need to reload and the file has not been modified
        //The ismodified () method judges whether the file has been modified according to the timestamp and currentcrc of the last modification of the APK file stored in sharedpreference
        if (!forceReload && !isModified(context, this.sourceApk, this.sourceCrc, prefsKeyPrefix)) {
            try {
                //Load the extracted files from the cache directory and return the DEX file list
                files = this.loadExistingExtractions(context, prefsKeyPrefix);
            } catch (IOException var6) {
                Log.w("MultiDex", "Failed to reload existing extracted secondary dex files, falling back to fresh extraction", var6);
                files = this.performExtractions();
                putStoredApkInfo(context, prefsKeyPrefix, getTimeStamp(this.sourceApk), this.sourceCrc, files);
            }
        } else {
            if (forceReload) {
                Log.i("MultiDex", "Forced extraction must be performed.");
            } else {
                Log.i("MultiDex", "Detected that extraction must be performed.");
            }
            //If the forced load or APK file has been modified, extract the DEX file again
            files = this.performExtractions();
            putStoredApkInfo(context, prefsKeyPrefix, getTimeStamp(this.sourceApk), this.sourceCrc, files);
        }

        Log.i("MultiDex", "load found " + files.size() + " secondary dex files");
        return files;
    }
}

MultiDexExtractor.performExtractions()

private List<MultiDexExtractor.ExtractedDex> performExtractions() throws IOException {
    //The extracted DEX filename prefix is "base. APK. Classes"
    String extractedFilePrefix = this.sourceApk.getName() + ".classes";
    this.clearDexDir();
    //Returned DEX list
    List<MultiDexExtractor.ExtractedDex> files = new ArrayList();
    //Apk compressed package
    ZipFile apk = new ZipFile(this.sourceApk);

    try {
        int secondaryNumber = 2;

        for(ZipEntry dexFile = apk.getEntry("classes" + secondaryNumber + ".dex"); dexFile != null; dexFile = apk.getEntry("classes" + secondaryNumber + ".dex")) {
            // base.apk.classes2.zip
            String fileName = extractedFilePrefix + secondaryNumber + ".zip";
            //Create file / data / APP / com.xiaxl.demo-2/base.apk.classes2.zip
            MultiDexExtractor.ExtractedDex extractedFile = new MultiDexExtractor.ExtractedDex(this.dexDir, fileName);
            //Add to file list
            files.add(extractedFile);
            Log.i("MultiDex", "Extraction is needed for file " + extractedFile);
            int numAttempts = 0;
            boolean isExtractionSuccessful = false;
            // extract Dex
            while(numAttempts < 3 && !isExtractionSuccessful) {
                ++numAttempts;
                extract(apk, dexFile, extractedFile, extractedFilePrefix);

                try {
                    extractedFile.crc = getZipCrc(extractedFile);
                    isExtractionSuccessful = true;
                } catch (IOException var18) {
                    isExtractionSuccessful = false;
                    Log.w("MultiDex", "Failed to read crc from " + extractedFile.getAbsolutePath(), var18);
                }

                Log.i("MultiDex", "Extraction " + (isExtractionSuccessful ? "succeeded" : "failed") + " '" + extractedFile.getAbsolutePath() + "': length " + extractedFile.length() + " - crc: " + extractedFile.crc);
                if (!isExtractionSuccessful) {
                    extractedFile.delete();
                    if (extractedFile.exists()) {
                        Log.w("MultiDex", "Failed to delete corrupted secondary dex '" + extractedFile.getPath() + "'");
                    }
                }
            }

            if (!isExtractionSuccessful) {
                throw new IOException("Could not create zip file " + extractedFile.getAbsolutePath() + " for secondary dex (" + secondaryNumber + ")");
            }

            ++secondaryNumber;
        }
    } finally {
        try {
            apk.close();
        } catch (IOException var17) {
            Log.w("MultiDex", "Failed to close resource", var17);
        }

    }

    return files;
}

2.3 other relevant codes

clearOldDexDir(Context context)

private static void clearOldDexDir(Context context) throws Exception {
    // /data/user/0/com.xiaxl.demo/files/secondary-dexes
    File dexDir = new File(context.getFilesDir(), "secondary-dexes");
    if (dexDir.isDirectory()) {
        Log.i("MultiDex", "Clearing old secondary dex dir (" + dexDir.getPath() + ").");
        //Get file list
        File[] files = dexDir.listFiles();
        //File is empty
        if (files == null) {
            Log.w("MultiDex", "Failed to list secondary dex dir content (" + dexDir.getPath() + ").");
            return;
        }
        //File is not empty
        File[] var3 = files;
        int var4 = files.length;
        //Cycle to clear all files under / data / user / 0 / com.xixl.demo/files/secondary-dexes
        for(int var5 = 0; var5 < var4; ++var5) {
            File oldFile = var3[var5];
            Log.i("MultiDex", "Trying to delete old file " + oldFile.getPath() + " of size " + oldFile.length());
            if (!oldFile.delete()) {
                Log.w("MultiDex", "Failed to delete old file " + oldFile.getPath());
            } else {
                Log.i("MultiDex", "Deleted old file " + oldFile.getPath());
            }
        }
        //Delete / data / user / 0 / com.xixl.demo/files/secondary-dexes folder
        if (!dexDir.delete()) {
            Log.w("MultiDex", "Failed to delete secondary dex dir " + dexDir.getPath());
        } else {
            Log.i("MultiDex", "Deleted old secondary dex dir " + dexDir.getPath());
        }
    }
}
private static File getDexDir(Context context, File dataDir, String secondaryFolderName) throws IOException {
    //Create / data / user / 0 / com.xixl.demo/code'cache directory
    File cache = new File(dataDir, "code_cache");
    try {
        mkdirChecked(cache);
    } catch (IOException var5) {
        cache = new File(context.getFilesDir(), "code_cache");
        mkdirChecked(cache);
    }
    //Create the / data / user / 0 / com.xixl.demo/code'cache/secondary-dexes directory
    File dexDir = new File(cache, secondaryFolderName);
    mkdirChecked(dexDir);
    return dexDir;
}

Three, summary

By now, the principle of multiple DEX installation is clear.

  • Extract the DEX file in a certain way;
  • Take theseThe DEX file is appended to the element [] array of the dexpathlist

This process should be as early as possible, so it is usually in the attachbasecontext () method of application.

In addition,hotfixThermal repair technology is through a certain wayInsert the fixed Dex in front of the element [] array of dexpathlistThe repaired class is loaded first.

Reference resources:

Android source code

Class loading mechanism series 3 — Analysis of multidex principle