1、Multidex的產生
在android5.0之前,每一個android應用中只會含有一個dex文件,但是因為Android系統本身的BUG,使得這個dex的方法數量被限制在65535之內,這就是著名的"64K(64*1024)"事件。為了解決這個問題,Google官方推出了這個類似於補丁一樣的support-library。關於這個庫的詳細使用,可以參考官方文檔,當然使用起來也會有些坑的,美團填坑記或者這位老兄。使用這個庫后,我們的APP不再只會僅有一個dex文件,可能會產生多個dex文件,這樣就避免了64K問題。
2、使用方式
對於Multidex的使用,大致有以下幾種方式
直接繼承MultiDexApplication
public class MyApplication extends MultiDexApplication{ // ........... }
直接調用MultiDex.install(Context);
public class MyApplication extends Application{ public void onCreate(){ MultiDex.install(this); } }
這兩種使用方式,其實本質是一樣的,都是通過MultiDex.install(this)來完成dex的加載,看看MultiDexApplication的實現:
public class MultiDexApplication extends Application { @Override protected void attachBaseContext(Context base) { super.attachBaseContext(base); MultiDex.install(this); } }
3、android中的類加載機制
在分析MultiDex.install(Context)之前,先了解一下android中的類是如何加載的。在android中,類的加載可以分為DexClassLoader.和PathClassLoader,這里先看看他們各自的實現:
DexClassLoader
/** * A class loader that loads classes from {@code .jar} and {@code .apk} files * containing a {@code classes.dex} entry. This can be used to execute code not * installed as part of an application. * 從包含dex文件的jar或是apk中加載classes。該ClassLoader可以用來加載外部的classes, * 也就是可以加載沒有預先安裝的含有dex文件的jar或是apk。 * * <p>This class loader requires an application-private, writable directory to * cache optimized classes. Use {@code Context.getDir(String, int)} to create * such a directory: <pre> {@code * File dexOutputDir = context.getDir("dex", 0); * }</pre> * * <p><strong>Do not cache optimized classes on external storage.</strong> * External storage does not provide access controls necessary to protect your * application from code injection attacks. */ public class DexClassLoader extends BaseDexClassLoader { /** * Creates a {@code DexClassLoader} that finds interpreted and native * code. Interpreted classes are found in a set of DEX files contained * in Jar or APK files. * * <p>The path lists are separated using the character specified by the * {@code path.separator} system property, which defaults to {@code :}. * * @param dexPath the list of jar/apk files containing classes and * resources, delimited by {@code File.pathSeparator}, which * defaults to {@code ":"} on Android * 含有classes和resources的文件(jar/apk)路徑,多個文件以 File.pathSeparator(Linux為":"Windows為";")分割開來,比如:xx/xx/aa.apk:yy/yy/bb.apk * @param optimizedDirectory directory where optimized dex files * should be written; must not be {@code null} * dex文件所在的根路徑,不能為null * @param libraryPath the list of directories containing native * libraries, delimited by {@code File.pathSeparator}; may be * {@code null} * SO文件所在的路徑 * @param parent the parent class loader */ public DexClassLoader(String dexPath, String optimizedDirectory, String libraryPath, ClassLoader parent) { super(dexPath, new File(optimizedDirectory), libraryPath, parent); } }
PathClassLoader
/** * Provides a simple {@link ClassLoader} implementation that operates on a list * of files and directories in the local file system, but does not attempt to * load classes from the network. Android uses this class for its system class * loader and for its application class loader(s). * 不能加載從網絡上獲取的classes,Android用這個類來加載系統classes和已經安裝的應用的classes */ public class PathClassLoader extends BaseDexClassLoader { /** * Creates a {@code PathClassLoader} that operates on a given list of files * and directories. This method is equivalent to calling * {@link #PathClassLoader(String, String, ClassLoader)} with a * {@code null} value for the second argument (see description there). * * @param dexPath the list of jar/apk files containing classes and * resources, delimited by {@code File.pathSeparator}, which * defaults to {@code ":"} on Android * @param parent the parent class loader */ public PathClassLoader(String dexPath, ClassLoader parent) { super(dexPath, null, null, parent); } /** * Creates a {@code PathClassLoader} that operates on two given * lists of files and directories. The entries of the first list * should be one of the following: * * <ul> * <li>JAR/ZIP/APK files, possibly containing a "classes.dex" file as * well as arbitrary resources. * <li>Raw ".dex" files (not inside a zip file). * </ul> * dexPath只支持jar/zip/apk/dex四種文件 * * The entries of the second list should be directories containing * native library files. * * @param dexPath the list of jar/apk files containing classes and * resources, delimited by {@code File.pathSeparator}, which * defaults to {@code ":"} on Android * @param libraryPath the list of directories containing native * libraries, delimited by {@code File.pathSeparator}; may be * {@code null} * @param parent the parent class loader */ public PathClassLoader(String dexPath, String libraryPath, ClassLoader parent) { super(dexPath, null, libraryPath, parent); } }
從類的說明以及構造函數可以明顯的知道DexClassLoader和PathClassLoader區別:DexClassLoader可以加載未安裝的含有dex文件的jar或是apk,而PathClassLoader只能加載已安裝好的含有dex文件的jar/apk/zip/dex。
BaseDexClassLoader
public class BaseDexClassLoader extends ClassLoader { /** structured lists of path elements */ private final DexPathList pathList; public BaseDexClassLoader(String dexPath, File optimizedDirectory, String libraryPath, ClassLoader parent) { super(parent); this.originalPath = dexPath; this.pathList = new DexPathList(this, dexPath, libraryPath, optimizedDirectory); } }
DexPathList
/** * A pair of lists of entries, associated with a {@code ClassLoader}. * One of the lists is a dex/resource path — typically referred * to as a "class path" — list, and the other names directories * containing native code libraries. Class path entries may be any of: * a {@code .jar} or {@code .zip} file containing an optional * top-level {@code classes.dex} file as well as arbitrary resources, * or a plain {@code .dex} file (with no possibility of associated * resources). * * <p>This class also contains methods to use these lists to look up * classes and resources.</p> */ /*package*/ final class DexPathList { private static final String DEX_SUFFIX = ".dex"; private static final String JAR_SUFFIX = ".jar"; private static final String ZIP_SUFFIX = ".zip"; private static final String APK_SUFFIX = ".apk"; /** class definition context */ private final ClassLoader definingContext; /** list of dex/resource (class path) elements */ private final Element[] dexElements; /** list of native library directory elements */ private final File[] nativeLibraryDirectories; public DexPathList(ClassLoader definingContext, String dexPath, String libraryPath, File optimizedDirectory) { if (definingContext == null) { throw new NullPointerException("definingContext == null"); } if (dexPath == null) { throw new NullPointerException("dexPath == null"); } if (optimizedDirectory != null) { // 對於PathClassloader來說optimizedDirectory是恆等於null的 if (!optimizedDirectory.exists()) { throw new IllegalArgumentException( "optimizedDirectory doesn't exist: " + optimizedDirectory); } if (!(optimizedDirectory.canRead() && optimizedDirectory.canWrite())) { throw new IllegalArgumentException( "optimizedDirectory not readable/writable: " + optimizedDirectory); } } // classLoader this.definingContext = definingContext; // 把每個dex文件的相關信息存儲到Element這個對象中 this.dexElements = makeDexElements(splitDexPath(dexPath), optimizedDirectory); // 本地so文件路徑 this.nativeLibraryDirectories = splitLibraryPath(libraryPath); } //...... }
首先是通過splitDexPath()得到含有dex文件的jar/zip/dex/apk集合:
/** * Splits the given dex path string into elements using the path * separator, pruning out any elements that do not refer to existing * and readable files. (That is, directories are not included in the * result.) */ private static ArrayList<File> splitDexPath(String path) { return splitPaths(path, null, false); } /** * Splits the given path strings into file elements using the path * separator, combining the results and filtering out elements * that don't exist, aren't readable, or aren't either a regular * file or a directory (as specified). Either string may be empty * or {@code null}, in which case it is ignored. If both strings * are empty or {@code null}, or all elements get pruned out, then * this returns a zero-element list. */ private static ArrayList<File> splitPaths(String path1, String path2, boolean wantDirectories) { ArrayList<File> result = new ArrayList<File>(); splitAndAdd(path1, wantDirectories, result); splitAndAdd(path2, wantDirectories, result); return result; } /** * Helper for {@link #splitPaths}, which does the actual splitting * and filtering and adding to a result. */ private static void splitAndAdd(String path, boolean wantDirectories, ArrayList<File> resultList) { if (path == null) { return; } // 路徑分離 String[] strings = path.split(Pattern.quote(File.pathSeparator)); for (String s : strings) { File file = new File(s); if (! (file.exists() && file.canRead())) { continue; } /* * Note: There are other entities in filesystems than * regular files and directories. */ if (wantDirectories) { if (!file.isDirectory()) { continue; } } else { if (!file.isFile()) { continue; } } resultList.add(file); } }
然后通過makeDexElements()來把dex信息保存到Element對象中:
/** * Makes an array of dex/resource path elements, one per element of * the given array. */ private static Element[] makeDexElements(ArrayList<File> files, File optimizedDirectory) { ArrayList<Element> elements = new ArrayList<Element>(); /* * Open all files and load the (direct or contained) dex files * up front. */ for (File file : files) { ZipFile zip = null; DexFile dex = null; String name = file.getName(); if (name.endsWith(DEX_SUFFIX)) {// 直接是.dex文件 // 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); } } else if (name.endsWith(APK_SUFFIX) || name.endsWith(JAR_SUFFIX) || name.endsWith(ZIP_SUFFIX)) {// 其他三種格式文件:都是壓縮包啊 try { zip = new ZipFile(file); } catch (IOException ex) { /* * Note: ZipException (a subclass of IOException) * might get thrown by the ZipFile constructor * (e.g. if the file isn't actually a zip/jar * file). */ System.logE("Unable to open zip file: " + file, ex); } try { dex = loadDexFile(file, optimizedDirectory); } catch (IOException ignored) { /* * 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). Safe to just ignore * the exception here, and let dex == null. */ } } else { System.logW("Unknown file type for: " + file); } // 格式符合要求,放到數組中 if ((zip != null) || (dex != null)) { // 依靠DexFile創建Element對象 elements.add(new Element(file, zip, dex)); } } return elements.toArray(new Element[elements.size()]); }
/** * Constructs a {@code DexFile} instance, as appropriate depending * on whether {@code optimizedDirectory} is {@code null}. * .dex文件轉成DexFile對象 */ 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); } } /** * Converts a dex/jar file path and an output directory to an * output file path for an associated optimized dex file. * 主要是對非.dex結尾的文件加上.dex后綴,統一為.dex后綴 * 沒有使用.odex為后綴,是因為構建系統會把.odex文件認為是只含有資源的文件 */ private static String optimizedPathFor(File path, File optimizedDirectory) { /* * Get the filename component of the path, and replace the * suffix with ".dex" if that's not already the suffix. * * We don't want to use ".odex", because the build system uses * that for files that are paired with resource-only jar * files. If the VM can assume that there's no classes.dex in * the matching jar, it doesn't need to open the jar to check * for updated dependencies, providing a slight performance * boost at startup. The use of ".dex" here matches the use on * files in /data/dalvik-cache. */ String fileName = path.getName(); if (!fileName.endsWith(DEX_SUFFIX)) { int lastDot = fileName.lastIndexOf("."); if (lastDot < 0) { fileName += DEX_SUFFIX; } else { StringBuilder sb = new StringBuilder(lastDot + 4); sb.append(fileName, 0, lastDot); sb.append(DEX_SUFFIX); fileName = sb.toString(); } } File result = new File(optimizedDirectory, fileName); return result.getPath(); }
以上,是android中類加載過程,從中可以發現,android中的classloader能加載的文件格式只能為zip/apk/dex/jar,加載目標是把dex文件轉化成一個Element對象,以供后續查找使用。
4、MultiDex.install(Context)
/** * Patches the application context class loader by appending extra dex files * loaded from the application apk. This method should be called in the * attachBaseContext of your {@link Application}, see * {@link MultiDexApplication} for more explanation and an example. * * @param context application context. * @throws RuntimeException if an error occurred preventing the classloader * extension. */ 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) {// 最低版本是4 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) { // Looks like running on a test Context, so just return without patching. return; } synchronized (installedApk) { // dataDir = /data/data/com.example.android.animationsdemo // apkPath = sourceDir = /data/app/com.example.android.animationsdemo-1.apk String apkPath = applicationInfo.sourceDir; if (installedApk.contains(apkPath)) { return; } installedApk.add(apkPath); if (Build.VERSION.SDK_INT > MAX_SUPPORTED_SDK_VERSION) { Log.w(TAG, "MultiDex is not guaranteed to work in SDK version " + Build.VERSION.SDK_INT + ": SDK version higher than " + MAX_SUPPORTED_SDK_VERSION + " 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") + "\""); } /* The patched class loader is expected to be a descendant of * dalvik.system.BaseDexClassLoader. We modify its * dalvik.system.DexPathList pathList field to append additional DEX * file entries. */ ClassLoader loader; try { //dalvik.system.PathClassLoader loader = context.getClassLoader(); } catch (RuntimeException e) { /* Ignore those exceptions so that we don't break tests relying on Context like * a android.test.mock.MockContext or a android.content.ContextWrapper with a * null base Context. */ Log.w(TAG, "Failure while trying to obtain Context class loader. " + "Must be running in test mode. Skip patching.", e); return; } if (loader == null) { // Note, the context class loader is null when running Robolectric tests. Log.e(TAG, "Context class loader is null. Must be running in test mode. " + "Skip patching."); return; } try { // 清空放置dex文件的文件夾 clearOldDexDir(context); } catch (Throwable t) { Log.w(TAG, "Something went wrong when trying to clear old MultiDex extraction, " + "continuing without cleaning.", t); } // 創建放置dex文件的文件夾 // dexDir = /data/data/com.example.android.animationsdemo/code_cache/secondary-dexes File dexDir = getDexDir(context, applicationInfo); // 得到APK中所有dex對應的zip文件 List<File> files = MultiDexExtractor.load(context, applicationInfo, dexDir, false); // if (checkValidZipFiles(files)) { // 加載dex到 installSecondaryDexes(loader, dexDir, files); } else { // 強制再轉換一次 Log.w(TAG, "Files were not valid zip files. Forcing a reload."); // Try again, but this time force a reload of the zip file. files = MultiDexExtractor.load(context, applicationInfo, dexDir, true); if (checkValidZipFiles(files)) { installSecondaryDexes(loader, dexDir, files); } else { // Second time didn't work, give up throw new RuntimeException("Zip files were not valid."); } } } } catch (Exception e) { Log.e(TAG, "Multidex installation failure", e); throw new RuntimeException("Multi dex installation failed (" + e.getMessage() + ")."); } Log.i(TAG, "install done"); }
整個加載過程主要分為3步驟:clearOldDexDir():清理過期數據MultiDexExtractor.load:把dex轉換成對應的zip文件installSecondaryDexes:dex轉換成對應的Element對象清理過期文件 :刪除/data/data/(pkg)/files/secondary-dexes路徑下的所有文件和該文件夾作者:紫苓鏈接:http://www.jianshu.com/p/dd90d7e7c691來源:簡書著作權歸作者所有。商業轉載請聯系作者獲得授權,非商業轉載請注明出處。
private static void clearOldDexDir(Context context) throws Exception { // dexDir = /data/data/com.example.android.animationsdemo/files/secondary-dexes File dexDir = new File(context.getFilesDir(), OLD_SECONDARY_FOLDER_NAME); if (dexDir.isDirectory()) { Log.i(TAG, "Clearing old secondary dex dir (" + dexDir.getPath() + ")."); File[] files = dexDir.listFiles(); if (files == null) { Log.w(TAG, "Failed to list secondary dex dir content (" + dexDir.getPath() + ")."); return; } for (File oldFile : files) { Log.i(TAG, "Trying to delete old file " + oldFile.getPath() + " of size " + oldFile.length()); if (!oldFile.delete()) { Log.w(TAG, "Failed to delete old file " + oldFile.getPath()); } else { Log.i(TAG, "Deleted old file " + oldFile.getPath()); } } if (!dexDir.delete()) { Log.w(TAG, "Failed to delete secondary dex dir " + dexDir.getPath()); } else { Log.i(TAG, "Deleted old secondary dex dir " + dexDir.getPath()); } } }
把dex文件轉換成對應的zip文件 :classesN.dex-->/data/data/(包名)/code_cache/secondary-dexes/(包名)-1.apk.classesN.zip
static List<File> load(Context context, ApplicationInfo applicationInfo, File dexDir, boolean forceReload) throws IOException { Log.i(TAG, "MultiDexExtractor.load(" + applicationInfo.sourceDir + ", " + forceReload + ")"); // sourceApk = /data/app/com.example.android.animationsdemo-1.apk final File sourceApk = new File(applicationInfo.sourceDir); long currentCrc = getZipCrc(sourceApk); // Validity check and extraction must be done only while the lock file has been taken. File lockFile = new File(dexDir, LOCK_FILENAME); RandomAccessFile lockRaf = new RandomAccessFile(lockFile, "rw"); FileChannel lockChannel = null; FileLock cacheLock = null; List<File> files; IOException releaseLockException = null; try { lockChannel = lockRaf.getChannel(); Log.i(TAG, "Blocking on lock " + lockFile.getPath()); cacheLock = lockChannel.lock(); Log.i(TAG, lockFile.getPath() + " locked"); if (!forceReload && !isModified(context, sourceApk, currentCrc)) { // APK信息沒有發生變化 try { // dex對應的zip文件已經存在 files = loadExistingExtractions(context, sourceApk, dexDir); } catch (IOException ioe) { Log.w(TAG, "Failed to reload existing extracted secondary dex files," + " falling back to fresh extraction", ioe); files = performExtractions(sourceApk, dexDir); putStoredApkInfo(context, getTimeStamp(sourceApk), currentCrc, files.size() + 1); } } else { // second dex 發生了改變 Log.i(TAG, "Detected that extraction must be performed."); files = performExtractions(sourceApk, dexDir); // 保存APK相關信息:時間戳、總dex數等 putStoredApkInfo(context, getTimeStamp(sourceApk), currentCrc, files.size() + 1); } } finally { if (cacheLock != null) { try { cacheLock.release(); } catch (IOException e) { Log.e(TAG, "Failed to release lock on " + lockFile.getPath()); // Exception while releasing the lock is bad, we want to report it, but not at // the price of overriding any already pending exception. releaseLockException = e; } } if (lockChannel != null) { closeQuietly(lockChannel); } closeQuietly(lockRaf); } if (releaseLockException != null) { throw releaseLockException; } Log.i(TAG, "load found " + files.size() + " secondary dex files"); return files; } /** * 把APK文件中的classesN.dex(N >= 2)轉換成對應的zip文件,保存到集合中 * zip文件:/data/data/(包名)/code_cache/secondary-dexes/(包名)-1.apk.classesN.zip */ private static List<File> performExtractions(File sourceApk, File dexDir) throws IOException { // extractedFilePrefix = com.example.android.animationsdemo-1.apk.classes final String extractedFilePrefix = sourceApk.getName() + EXTRACTED_NAME_EXT; // Ensure that whatever deletions happen in prepareDexDir only happen if the zip that // contains a secondary dex file in there is not consistent with the latest apk. Otherwise, // multi-process race conditions can cause a crash loop where one process deletes the zip // while another had created it. prepareDexDir(dexDir, extractedFilePrefix); List<File> files = new ArrayList<File>(); final ZipFile apk = new ZipFile(sourceApk); try { int secondaryNumber = 2; // dexFile = classes2.dex ZipEntry dexFile = apk.getEntry(DEX_PREFIX + secondaryNumber + DEX_SUFFIX); while (dexFile != null) { // fileName = com.example.android.animationsdemo-1.apk.classes2.zip String fileName = extractedFilePrefix + secondaryNumber + EXTRACTED_SUFFIX; // extractedFile = /data/data/com.example.android.animationsdemo/code_cache/secondary-dexes/com.example.android.animationsdemo-1.apk.classes2.zip File extractedFile = new File(dexDir, fileName); files.add(extractedFile); Log.i(TAG, "Extraction is needed for file " + extractedFile); int numAttempts = 0; boolean isExtractionSuccessful = false; while (numAttempts < MAX_EXTRACT_ATTEMPTS && !isExtractionSuccessful) { // 最多嘗試3次來把classesN.dex轉成對應的zip文件 numAttempts++; // Create a zip file (extractedFile) containing only the secondary dex file // (dexFile) from the apk. // 把classesN.dex文件轉化為zip文件 extract(apk, dexFile, extractedFile, extractedFilePrefix); // Verify that the extracted file is indeed a zip file. isExtractionSuccessful = verifyZipFile(extractedFile); // Log the sha1 of the extracted zip file Log.i(TAG, "Extraction " + (isExtractionSuccessful ? "success" : "failed") + " - length " + extractedFile.getAbsolutePath() + ": " + extractedFile.length()); if (!isExtractionSuccessful) { // Delete the extracted file extractedFile.delete(); if (extractedFile.exists()) { Log.w(TAG, "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++; 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; } /** * apk = /data/app/com.example.android.animationsdemo-1.apk * dexFile = classesN.dex * extractTo = /data/data/com.example.android.animationsdemo/code_cache/secondary-dexes/com.example.android.animationsdemo-1.apk.classesN.zip * extractedFilePrefix = com.example.android.animationsdemo-1.apk.classes * 把classesN.dex文件轉化為zip文件 */ private static void extract(ZipFile apk, ZipEntry dexFile, File extractTo, String extractedFilePrefix) throws IOException, FileNotFoundException { InputStream in = apk.getInputStream(dexFile); ZipOutputStream out = null; // tmp = /data/data/com.example.android.animationsdemo/code_cache/secondary-dexes/com.example.android.animationsdemo-1.apk.classes.zip File tmp = File.createTempFile(extractedFilePrefix, EXTRACTED_SUFFIX, extractTo.getParentFile()); Log.i(TAG, "Extracting " + tmp.getPath()); try { // 把classesN.dex寫到tmp這個zip文件中,然后指定這個zip文件的下一個ZipEntry為classes.dex // 最后把zip文件重命名為extractTo out = new ZipOutputStream(new BufferedOutputStream(new FileOutputStream(tmp))); try { 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(); } Log.i(TAG, "Renaming to " + extractTo.getPath()); if (!tmp.renameTo(extractTo)) { throw new IOException("Failed to rename \"" + tmp.getAbsolutePath() + "\" to \"" + extractTo.getAbsolutePath() + "\""); } } finally { closeQuietly(in); tmp.delete(); // return status ignored } }
加載dex信息到內存中
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); } } }
/** * Installer for platform versions 19. */ private static final class V19 { private static void install(ClassLoader loader, List<File> additionalClassPathEntries, File optimizedDirectory) throws IllegalArgumentException, IllegalAccessException, NoSuchFieldException, InvocationTargetException, NoSuchMethodException { /* The patched class loader is expected to be a descendant of * dalvik.system.BaseDexClassLoader. We modify its * dalvik.system.DexPathList pathList field to append additional DEX * file entries. */ // 通過反射的方式獲取PathClassLoader中的pathList屬性 Field pathListField = findField(loader, "pathList"); Object dexPathList = pathListField.get(loader); // 通過反射的方式重新給DexPathList中的dexElements重新賦值 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); } } /** * 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 { // 通過反射的方式調用dalvik.system.DexPathList.makeDexElements(),得到classesN.dex對應的Element Method makeDexElements = findMethod(dexPathList, "makeDexElements", ArrayList.class, File.class, ArrayList.class); return (Object[]) makeDexElements.invoke(dexPathList, files, optimizedDirectory, suppressedExceptions); } }
private static void expandFieldArray(Object instance, String fieldName, Object[] extraElements) throws NoSuchFieldException, IllegalArgumentException, IllegalAccessException { // 找到DexPathList類中的dexElements屬性,它是Element[] Field jlrField = findField(instance, fieldName); // 目前內存具有的Elements,應該只有classes.dex對應的Element Object[] original = (Object[]) jlrField.get(instance); // classesN.dex對應的Element Object[] combined = (Object[]) Array.newInstance( original.getClass().getComponentType(), original.length + extraElements.length); System.arraycopy(original, 0, combined, 0, original.length); System.arraycopy(extraElements, 0, combined, original.length, extraElements.length); // 給instance對象中的屬性jlrField重新賦值為combined jlrField.set(instance, combined); }
