-
Dex分包的由来
分包的概念想必我们都不陌生了,因为一个dex文件中的方法数使用一个short类型的字段来记录方法数,所以最多只能存储2^16=65536个方法数(16是2个字节能表示的最大值)。随着项目的迭代以及各种库的引入,单个dex的方法数迟早会超过这个数值,因此,dex分包应运而生。
所谓dex分包就是把原先apk中的单个dex文件分成多个dex文件,这样就能使每个dex中的方法数控制在65536这个阈值之下。
-
如何生成多个Dex文件
dex是一种具有特定格式的文件,通过对class文件的优化来生成,这意味着可以通过程序自动将class文件转换成dex文件,在android工具包中已经有这样的程序可供使用了,那就是
dx和d8。使用
dx命令将单个class文件转成dex文件:dx --dex --no-strict --output=/path/result.dex Demo.class注意要加上--no-strict,可以防止包名匹配错误,不加的话你需要在对应包名的顶层父目录中执行,比如“com.mph.bpp.Demo”的话,你必须在com的所在目录下执行,且源文件制定为相对路径"com/mph/bpp/Demo.class"。
d8是一款用于取代dx、更快的 Dex 编译器,可以生成更小的 APK,在Android Studio3.1以上版本已经成为默认选项了。使用
d8命令将单个class文件转成dex文件:d8 --release path/Demo.class --output path/result.dex--release表示正式编译,编译 DEX 字节码时不包含调试信息,相反的--debug则表示编译 DEX 字节码时在其中包含调试信息,例如调试符号表,此选项默认处于启用状态,因此不需要额外指定。
如果没指定--output则会输出到当前执行目录,默认输出为classed.dex。
如果我们想要把多个class文件转到同一个dex文件中,则只需要把源文件都放在一个jar包中,然后同样的命令,源文件换成该jar包即可。
借助这两个工具,我们就可以指定任何的class文件打包成任意数量的dex文件,但需要注意的是,在生成dex的过程中,我们需要去计算每个class的方法数,保证它们的总量不会超过65536的上限,基于这套流程,我们可以写一个脚本来完成dex分包的过程,这就是手动分包。
在android5.0版本及以上,虚拟机开始支持多dex加载,在打包过程中,gradle会自动检测dex文件中的方法数是否超过65536,如果超过了则自动拆成多dex文件,这个过程的原理其实就是我们上面所说的那样,只不过在gradle自动构建中我们无法指定那些class放在同一个dex中,这就是自动分包。
-
自动分包配置
app/build.gradle :
android{ defaultConfig{ multiDexEnabled true } }然后指定你的BaseApp继承MultiDexApplication,无法继承的话可以重写BaseApp的attachBaseContext方法:
protected void attachBaseContext(Context base) { super.attachBaseContext(base); MultiDex.install(this); }其实这就是MultiDexApplication中的实现。
如果你没有使用androidx,那么想要使用MultiDex类的话还需要添加'com.android.support:multidex:x.x.x'依赖。
gradle的分包完全是自动化的,也就是说你无法决定哪些类应该放到主dex中,虽然它会自动分析哪些类是应用启动时必须的以及识别它们的引用类,但是对于一些不太可见的复杂依赖,比如从native代码逻辑中创建的Java类对象的情况,这时在启动的时候就会发生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注意,这个配置只在minSdk为21以下才会生效,而且只会保证配置内容添加到主dex,并不能保证其他未指定的不会添加到主dex。至于为什么这样,因为这个配置是为了解决某些情况下虚拟机内部检测不到主dex中相关的关联类的问题,可能是21及以上的虚拟机内部已经优化好了,不需要额外配置了,这也关闭了我们手动添加主dex的大门,也是出于安全的考虑吧。
Tip1:因在实践过程中发现multiDexKeepProguard中指定的class并没有出现在classes.dex中,在查询这个问题过程中看到了一个不起眼的回复说是minSdk21版本及以上不会执行这个配置,但是我在官方文档中并没有找到这个说明,所以起初没有当真,没想到真是这个问题。
Tip2:在我的项目中使用了很多JetPack的库,很多库的版本都很高,minSdk需要最低21,当改成20后会报错,根据提示在AndroidManifest.xml中增加<uses-sdk tools:overrideLibrary="xxx1, xxx2"/> 即可通过编译,比如<uses-sdk tools:overrideLibrary="androidx.navigation.compose"/>,哪个库报错就写哪个,多个用逗号隔开,这里只是说能通过编译,但可能会有运行时错误,我这里只是看看apk中的classes.dex里有没有配置的类而已。
Multidex库也有所限制,一个是如果非主dex文件过大的话,在加载的时候可能会出现ANR;另一个是在4.0以下,linearalloc limit(虚拟机相关的)的大小不足以支撑dex的最大方法引用数,这就会导致未满65536的情况下依然会崩溃,因此在4.0一下要做好相关平台的测试工作。
使用代码瘦身功能可以很大程度上避免这个问题:android { buildTypes { release { minifyEnabled true //shrinkResources true //资源瘦身 ... } } }minifyEnabled设置成true也可能让原本多个dex合成一个dex文件。比如我在测试热修复的demo中,debug模式下生成的apk没有配置minifyEnabled属性,因此会自动被分成多个dex文件,但是在开启了minifyEnabled的release模式下就只存在一个dex了。
在我的热修复demo中,我发现,如果需要被修复的类存在于主dex中的话,则使用类加载机制插入dexElements的方式是无法奏效的,但是如果要被修复的类存在于非主dex中的时候就没问题,于是,我猜测:
被分到主dex中的class在应用启动的时候就已经被加载了(这里的加载指的是通过ClassLoader的loadClass方法加载过),哪怕尚未用到它,因此之后再去添加补丁的dexElements也不会被替换了。 -
MultiDex.install加载源码分析
在app启动时,虚拟机只会加载apk中名为“classed.dex”的这个dex文件,也就是主dex,因此,不管是自动分包还是手动分包,最终我们还要手动加载其他的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会判断虚拟机时候有底层的加载分包的能力,如果虚拟机可以在底层直接加载dex分包的话就不需要在这里重复操作了。如果虚拟机层面没有这个能力,那就需要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的通常虚拟机都有multidex能力,能走到这里说明不支持,因此是个需要注意的地方 } 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; } } }然后调用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 { //加载已经解压过的dex文件 files = loadExistingExtractions(context, prefsKeyPrefix); } catch (IOException ioe) { //如果发生异常了就直接解压dex文件 files = performExtractions(); putStoredApkInfo(context, prefsKeyPrefix, getTimeStamp(sourceApk), sourceCrc, files); } } else { //强行解压文件 files = performExtractions(); putStoredApkInfo(context, prefsKeyPrefix, getTimeStamp(sourceApk), sourceCrc, files); } return files; }注意上面传过来的最后一个参数forceReload,如果doInstallation的第一遍加载发生异常了,会再次调用load方法,传入true来调用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会是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) { //创建要输出的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(循环冗余校验)) 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的路径是一样的,只不过名字多了个tmp的前缀 File tmp = File.createTempFile("tmp-" + extractedFilePrefix, EXTRACTED_SUFFIX, extractTo.getParentFile()); try { //开始压缩操作,其实就是把dexFile文件复制到tmp out = new ZipOutputStream(new BufferedOutputStream(new FileOutputStream(tmp))); try { //dexFile会复制到tmp这个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文件为了安全考虑要设置为只读 if (!tmp.setReadOnly()) { throw new IOException("Failed to mark readonly \"" + tmp.getAbsolutePath() + "\" (tmp of \"" + extractTo.getAbsolutePath() + "\")"); } //注意File的renameTo方法,它的原理是会创建extractTo,然后把tmp的内容复制过去 if (!tmp.renameTo(extractTo)) { throw new IOException("Failed to rename \"" + tmp.getAbsolutePath() + "\" to \"" + extractTo.getAbsolutePath() + "\""); } } finally { closeQuietly(in); //删除tmp文件 tmp.delete(); } }总结一下extract方法就是:把dexFile复制到extractedFile中去。
回到load方法中,performExtractions方法执行完之后紧接着会调用putStoredApkInfo方法,这个方法中会把解压后的文件关键信息保存到SharedPreferences中,比如TIME_STAMP、
CRC(Cyclic Redundancy Check(循环冗余校验))、DEX_NUMBER(非主dex文件数量)等信息: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(); }现在再来看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数量 int totalDexNumber = multiDexPreferences.getInt(prefsKeyPrefix + KEY_DEX_NUMBER, 1); final List<ExtractedDex> files = new ArrayList<ExtractedDex>(totalDexNumber - 1); //根据保存的信息来构造ExtractedDex for (int secondaryNumber = 2; secondaryNumber <= totalDexNumber; secondaryNumber++) { //找到之前含有dexFile的压缩文件 String fileName = extractedFilePrefix + secondaryNumber + EXTRACTED_SUFFIX; ExtractedDex extractedFile = new ExtractedDex(dexDir, fileName); if (extractedFile.isFile()) { //安全验证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; }到此,我们已经清楚了整个读取dexFile的过程,下面我们继续看如何加载到ClassLoader的。
MultiDexExtractor的load方法完成后会拿到所有的含有dexFile的压缩zip(内部都是叫classes.dex),然后调用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); } } }这里会根据不同的SDK版本来分别执行不同的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); }这里会利用反射拿到传进来的ClassLoader的pathList属性,类型是DexPathList,再次反射获取它的dexElements属性,他是个Element数组,makeDexElements方法中利用反射调用DexPathList的makeDexElements方法构造一个Element数组,里面包含我们之前获取的所有非主dex的zip文件,然后在expandFieldArray方法中把pathList中原有的dexElements数组和新创建的整合到一起(新创建的放在后面,也就是主dex的位置在非主dex前面),最后再设置到pathList的dexElements属性上。
那为什么要这么做呢?那就得提到android中的类加载机制了。
-
Android中的类加载机制
Android的类加载基于Java的类加载机制,都是双亲委托机制,什么是双亲委托呢?就是每次加载一个类的时候都先从更上一级的父加载器中查找,如果父加载器已经加载过了则直接使用不再加载,这样做可以避免重复加载,最重要的是防止系统类的恶意修改和替换。
在Android系统中有两个ClassLoader可以供我们加载自定义资源,一个是DexClassLoader,一个是PathClassLoader,他们的区别就是DexClassLoader多了一个可以指定odex(dex的优化文件)的输出目录的选项,它们都继承自BaseDexClassLoader。
当我们用到某个类的时候会由虚拟机调用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 { //默认为空 c = findBootstrapClassOrNull(name); } } catch (ClassNotFoundException e) { } if (c == null) { //如果都未找到则调用自身的findClass方法 c = findClass(name); } } return c; }BaseDexClassLoader中实现了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; }可见,这里会调用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; }看到这里,我们就明白了,原来最终会从dexElements中查找类啊,因此也就理解了上面的dex加载操作。
这也是热修复中类加载修复方式的原理依据,也是插件化思想的原理依据之一。
关于Dex分包
最后编辑于 :
©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。
相关阅读更多精彩内容
- 转载:tech.meituan.com/mt-android-auto-split-dex.html 概述 作为一...
- 0x01 开篇 官方文档MultiDex解释: 1.Dalvik Executable (DEX)文件的总方法数限...
- 在android apk中如果项目引用的方法数超过64k(包括android框架方法、库方法、自己代码中的方法)的...
- 概述 Android开发者应该都遇到了64K最大方法数限制的问题,针对这个问题,google也推出了multide...