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