如果你的应用不支持5.0以下的Dalvik VM 那么你没有必要看这篇文章
本文内容由个人按照个人理解,汇总自《参考文献》所示文章,有兴趣者可自行查看相关参考文献
Android分包方案Multidex的相关个人理解,不正之处欢迎指正。
口袋助理是一款移动办公OA,支持手机APP和网页版。目前超过200万家企业正在使用。手机考勤、销售人员外勤管理、客户CRM、销售项目管理、移动日志、流程审批、移动报销、任务管理、企业云盘、企业通讯录、在线聊天 等丰富的功能,帮助企业更好的管理公司业务和服务员工!
其实我们遇到的现象只是1所属文本,在测试1的场景中,部分同事又发现了2状况,,,之初,我们认为这是两个不同问题,不需要并案解决。
随后深入研究过程中,才发现两者的强关联性。 这也带给我们一个思考:研发人员应该注意任何细节问题,一旦忽视某个细微之处,就可能让整个的解决思路走入误区。
话不多说,处理前和处理后的对比,看图说话:
通过实践我们可以得出这样的结论:启动超时是由于 android天生的64K问题所涉及的MultiDex导致的。
MultiDex方面的解决是比较高风险和困难耗时的,因此我们考虑:先把耗时问题搁置在这里,考虑是否能够单纯的通过解决黑屏问题来规避可视性bug,之后再进一步的对MultiDex耗时进行优化。
V实验判断是否是VerifyActivity的问题;
S实验判断是否是SplashActivity的问题;
SplashActivity 配置 android:theme=”@style/AppTheme.Search”
现象:点击桌面图标后,即时出现VerifyActivity.Theme中设置的背景图,静待15s+后,SplashActivity显示; SplashActivity显示前先出现黑色,再出现透明色
测试方法: VerifyActivity跳往一个简易的TestSplashActivity(和SplashActivity具有相同的AMF配置)
TestSplashActivity 配置 android:theme=”@style/AppTheme.Search”
现象:与初始异常状况一样
对比”@style/AppTheme.Search”、”@style/SplashTheme”、”@style/Activity.Basic”可以发现:
自此确定,SplashActivity前显示透明色是由于主题设置了 窗口背景透明导致的
现象:与初始异常状况一样
测试方法: VerifyActivity 改为一个简易的TestBlackActivity(和VerifyActivity具有相同的AMF配置)
TestBlackActivity.onCreate中即时跳往SplashActivity
现象:与初始状况一样
TestBlackActivity.onCreate中即时跳往SplashActivity
现象:点击桌面图标后无响应,静待15s+后,SplashActivity显示。期间无黑色出现;
现象分析: Applaction生命周期超时,导致Activity启动时,相关资源仍未加载完成。此时先显示PreWindows,我们自定义的windows背景为透明。
也就是说在我们看到无反应的期间,之所一直看到桌面,其实是有一层透明的窗口背景
摘选我在秒开应用中的一段描述:Android启动优化之打造秒开应用
当打开一个Activity时,如果这个Activity所属Application还没有在运行,系统会为这个Activity的创建一个进程(每开启一个进程都会有一个Application,所以Application的onCreate()可能会被调用多次),但进程的创建与初始化都需要时间,在这个动作完成之前,如果初始化的时间过长,屏幕上可能没有任何动静,用户会以为没有点到按钮。
所以既不能停在原来的地方又没到显示新的界面,怎么办呢?这就有了StartingWindow(也称之为PreviewWindow)的出现,这样看起来就像Activity已经启动起来了,只是数据内容还没有初始化好。
StartingWindow一般出现在应用程序进程创建并初始化成功前,所以它是个临时窗口,对应的WindowType是TYPE_APPLICATION_STARTING。目的是告诉用户,系统已经接受到操作,正在响应,在程序初始化完成后实现目的UI,同时移除这个窗口.
一般情况下我们会对Application和Activity设置Theme,系统会根据设置的Theme初始化StartingWindow。Window布局的顶层是DecorView,StartingWindow显示一个空DecorView,但是会给这个DecorView应用这个Activity指定的Theme,如果这个Activity没有指定Theme就用Application的(Application系统要求必须设置Theme)
在Theme中可以指定窗口的背景,Activity的ICON,APP整体文字颜色等,如果说没有指定任何属性,就会用默认的属性,也就是上文中提到的空DecorView
无法有效规避,我们在:
==首次启动加载dex导致的黑屏和ANR问题==
如果你使用multidex,你需要意识到它对app启动性能有影响。我们通过跟踪app的启动时间发现了这个问题-用户点击app图标到所有图片都下载完并显示给用户的这段时间。一旦multidex 启用,在所有运行Kitkat (4.4) 及以下的设备上我们的app启动时间就会大约增加15%。更多信息参考 Carlos Sessa的Lazy Loading Dex files 。
这是因为Android 5.0 以及更高版本使用了一个叫做ART的运行时,它天生就支持从应用的apk文件中加载multiple dex文件
由此看来,最终的解决还是需要通过解决根本问题来进行。
当一个app的功能越来越复杂,代码量越来越多,引入的jar第三方包越来越多,也许有一天便会突然遇到AndroidStudio构建失败了:
UNEXPECTED TOP-LEVEL EXCEPTION: java.lang.IllegalArgumentException: method ID not in [0, 0xffff]: 65536
at com.android.dx.merge.DexMerger$6.updateIndex(DexMerger.java:501)
at com.android.dx.merge.DexMerger$IdMerger.mergeSorted(DexMerger.java:276)
at com.android.dx.merge.DexMerger.mergeMethodIds(DexMerger.java:490)
at com.android.dx.merge.DexMerger.mergeDexes(DexMerger.java:167)
at com.android.dx.merge.DexMerger.merge(DexMerger.java:188)
at com.android.dx.command.dexer.Main.mergeLibraryDexBuffers(Main.java:439)
at com.android.dx.command.dexer.Main.runMonoDex(Main.java:287)
at com.android.dx.command.dexer.Main.run(Main.java:230)
at com.android.dx.command.dexer.Main.main(Main.java:199)
at com.android.dx.command.Main.main(Main.java:103):Derp:dexDerpDebug FAILED
这是Android早期研发人员的短视行为导致的,我们先来交代下android打包的背景知识:
更多内容参看着了How Android Apps are Built and Run
然而在早期的android系统开发过程中,在合成classes.dex文件阶段使用了短容量的变量控制,导致一个dex文件系统只允许最多有65k个方法(毕竟当时谁也没想到Android会有今日的容量和体积),如果你的源代码和狂拽炫酷叼炸天的三方库中方法超过了这个限制,就会导致打包失败
根据 StackOverFlow – Does the Android ART runtime have the same method limit limitations as Dalvik? 上面的说法,是因为 Dalvik 的 invoke-kind 指令集中,method reference index 只留了 16 bits,最多能引用 65535 个方法。
Android Dex分包之旅
在安卓的早期,达到65k方法上限的应用解决这个问题的办法就是使用Proguard来减少无用的代码。但是,这个方法有局限,并且只是为生产app拖延了接近65k限制的时间
一种比较流行的方案是插件化方案,工作量太大;
Google在推出MultiDex之前:
Android官方为了弥补当时短视行为所造成的问题,给出了官方的补丁方案MultiDex:
关于 64K 引用限制 \
Android 应用 (APK) 文件包含 Dalvik Executable (DEX) 文件形式的可执行字节码文件,其中包含用来运行您的应用的已编译代码。Dalvik Executable 规范将可在单个 DEX 文件内可引用的方法总数限制在 65,536,其中包括 Android 框架方法、库方法以及您自己代码中的方法。在计算机科学领域内,术语千(简称 K)表示 1024(或 2^10)。由于 65,536 等于 64 X 1024,因此这一限制也称为“64K 引用限制”
简单地说就是:
需要注意:
++这是对Dalvik VM过程的描述,这种动态加载dex的能力运用了能力归根结底还是因为java 的classloader类加载机制,然而对于ART VM而言,其实在安装阶段就会吧所有的dex合并起来,而不再等到程序的运行期++。先有个这样的认识,我们下文会讲到ART和Dalvik不同弄处理方式对MultiDex的影响
这是个动态加载模块的框架,Dalvik VM 的
沿着这条道走,Android模块动态化加载,包括dex级别和apk级别的动态化加载,各种玩法层出不穷,参见这里dynamic-load-apk、android-pluginmgr、AndroidDynamicLoader、Apkplug、DroidPlugin,有兴趣的自行阅读下,不在本篇研究范围内
Android sdk build tool中的mainDexClasses脚本会自动完成。
该脚本在版本21以上才会有,要求输入一个文件组(包含编译后的目录或jar包),然后分析文件组中的类并写入到–output所指定的文件中。使用方法非常很简单:
mainDexClasses [--output <output file>] <application path>
实现原理也不复杂,主要分为三步:
这里只是简单的得到所有入口类(即rules中的Instrumentation、application、Activity、Annotation等等)的直接引用类。
举个栗子:有MainActivity、DirectReferenceClass、InDirectReferenceClass三个类,其中DirectReferenceClass是MainActivity的直接引用类,InDirectReferenceClass是DirectReferenceClass的直接引用类。而InDirectReferenceClass是MainActivity的间接引用类(即直接引用类的所有直接引用类)
public class MainActivity extends Activity {
protected void onCreate(Bundle savedInstanceState) {
DirectReferenceClass test = new DirectReferenceClass();
}
}
public class DirectReferenceClass {
public DirectReferenceClass() {
InDirectReferenceClass test = new InDirectReferenceClass();
}
}
public class InDirectReferenceClass {
public InDirectReferenceClass() {
}
}
Android dex分包方案
通过 gradle 编译生成 apk 的期间,可以通过 Gradle Console视图查看 gradle 执行任务的输出,期间跟 multidex 几个相关的任务如下:
运行collect任务,发现会在build/multi-dex目录下单独生成manifest_keep.txt文件,该文件其实就是通过上述规则扫描AndroidManifest生成。manifest_keep.txt保留的是所有需要放入主Dex里的类。
还没完,接下来transformClassesWithMultidexlist任务会根据manifest_keep.txt生成必要依赖列表maindexlist.txt,这里面所有类才是真正放入主Dex里的
具体过程如下:
//{variant}为打包的构建版本类型
:app:transformClassesWithJarMergingFor{variant}
:app:collect{variant}MultiDexComponents
:app:transformClassesWithMultidexlistFor{variant}
:app:transformClassesWithDexFor{variant}
- 具体的实现请参看Android Too many classes in –main-dex-list 错误原因及Android分包原理
CreateManifestKeepList: collect{variant}MultiDexComponents task
shrink{variant}MultiDexComponents task
MultiDexTransform:create{variant}MainDexClassList task
DexTransform
需要注意的是,maindexlist.txt文件并没有完全列出有所的依赖类,如果发现要查找的那个class不在maindexlist中,也无需奇怪。如果一定要确保某个类分到主dex中,将该类的完整路径加入到maindexlist中即可,同时注意两点:
1. 如果加入的类并不在project中,则gradle构建会忽略这个类,
2. 如果加入了多个相同的类,则只取其中一个
也就是说: 对于一个使用了MultiDex的Android工程,编译后在/build/intermediates/multi-dex/{variant_path}/路径下面,可以看到如下几个文件。
componentClasses.jar
components.flags
manifest_keep.txt
maindexlist.txt
ART
Dalvik(对于5.0以下的系统,我们需要在启动时手动加载其他的dex)
事实上,若我们在attachBaseContext中调用Multidex.install,我们只需引入Application的直接引用类即可,但是MultiDex方案中的mainDexClasses将Activity、ContentProvider、Service等的直接引用类也引入,主要是满足需要在非attachBaseContent加载多dex的需求
需要注意的是,如果存在以下代码,将出现NoClassDefFoundError错误:
public class HelloMultiDexApplication extends Application {
@Override
protected void attachBaseContext(Context base) {
super.attachBaseContext(base);
DirectReferenceClass test = new DirectReferenceClass();
MultiDex.install(this);
}
}
这是因为在实际运行过程中,DirectReferenceClass需要的InDirectReferenceClass并不一定在主dex。解决方法是手动将该类放于dx的-main-dex-list参数中:
afterEvaluate {
tasks.matching {
it.name.startsWith('dex')
}.each { dx ->
if (dx.additionalParameters == null) {
dx.additionalParameters = []
}
//表示当方法数越界时则生成多个dex文件(我的没有越界,貌似也生成了两个)
dx.additionalParameters += '--multi-dex'
//这个指定了$projectDir/自定义路径中的类(即maindexlist.txt中的类)会打包到主dex中,不过注意下一条。
dx.additionalParameters += "--main-dex-list=$projectDir/" .toString()
//表明只有-main-dex-list所指定的类(在我的配置中,就是app目录下的maindexlist.txt中包含的类)才能打包到主dex中,如果没有这个选项,上个选项就会失效
dx.additionalParameters += '--minimal-main-dex'
}
}
下面代码片段是BaseDexClassLoader findClass的过程:
protected Class> findClass(String name) throws ClassNotFoundException {
List suppressedExceptions = new ArrayList();
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;
}
下面代码片段为怎么通过DexFile来加载Secondary DEX并放到BaseDexClassLoader的DexPathList中:
private static void install(ClassLoader loader, List 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.
*/
Field pathListField = findField(loader, "pathList");
Object dexPathList = pathListField.get(loader);
ArrayList suppressedExceptions = new ArrayList();
expandFieldArray(dexPathList, "dexElements", makeDexElements(dexPathList,
new ArrayList(additionalClassPathEntries), optimizedDirectory,
suppressedExceptions));
try {
if (suppressedExceptions.size() > 0) {
for (IOException e : suppressedExceptions) {
//Log.w(TAG, "Exception in makeDexElement", e);
}
Field suppressedExceptionsField =
findField(loader, "dexElementsSuppressedExceptions");
IOException[] dexElementsSuppressedExceptions =
(IOException[]) suppressedExceptionsField.get(loader);
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(loader, dexElementsSuppressedExceptions);
}
} catch(Exception e) {
}
}
在将您的应用配置为支持使用 64K 或更多方法引用之前,您应该采取措施减少应用代码调用的引用总数,包括由您的应用代码或包含的库定义的方法。下列策略可帮助您避免达到 DEX 引用限制:
使用这些技巧使您不必在应用中启用 Dalvik 可执行文件分包,同时还会减小 APK 的总体大小
如果项目的 minSdkVersion 设置为 21 或更高值,只需在模块级 build.gradle 文件中将 multiDexEnabled 设置为 true,如此处所示:
android {
defaultConfig {
...
minSdkVersion 21
targetSdkVersion 26
multiDexEnabled true
}
...
}
此库可以为使用多个 Dalvik Executable (DEX) 文件开发应用提供支持。引用超过 65536 个方法的应用须使用 Dalvik 可执行文件分包配置。如需了解有关使用 Dalvik 可执行文件分包的详细信息。此库的 Gradle 构建脚本依赖关系标识符如下所示:
//在app的gradle脚本里写上:
com.android.support:multidex:1.0.0
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.myapp">
<application
android:name="android.support.multidex.MultiDexApplication" >
...
application>
manifest>
-public class MyApplication extends MultiDexApplication { ... }
public class MyApplication extends SomeOtherApplication {
@Override
protected void attachBaseContext(Context base) {
super.attachBaseContext(context);
Multidex.install(this);
}
}
构建应用后,Android 构建工具会根据需要构建主 DEX 文件 (classes.dex) 和辅助 DEX 文件(classes2.dex 和 classes3.dex 等)。然后,构建系统会将所有 DEX 文件打包到您的 APK 中。
运行时,Dalvik 可执行文件分包 API 使用特殊的类加载器来搜索适用于您的方法的所有 DEX 文件(而不是仅在主 classes.dex 文件中搜索
- Android5.0之前,使用 Dalvik 方式运行,先加载主分包,然后反射加载其余的包
- Android5.0之后,使用 ART 方式运行,ART预编译时,扫描主分包和子包,生成 .oat 文件用于用户运行
public class MultiDexApplication extends Application {
protected void attachBaseContext(final Context base) {
super.attachBaseContext(base);
MultiDex.install((Context)this);
}
}
public class MultiDex {
static {
//第二个Dex文件的文件夹名,实际地址是/date/date//code_cache/secondary-dexes
SECONDARY_FOLDER_NAME = "code_cache" + File.separator + "secondary-dexes";
installedApk = new HashSet();
IS_VM_MULTIDEX_CAPABLE = isVMMultidexCapable(System.getProperty("java.vm.version"));
}
public static void install(final Context context) {
//在使用ART虚拟机的设备上(部分4.4设备,5.0+以上都默认ART环境),已经原生支持多Dex,因此就不需要手动支持了
if (MultiDex.IS_VM_MULTIDEX_CAPABLE) {//针对ART
Log.i("MultiDex", "VM has multidex support, MultiDex support library is disabled.");
return;
}
if (Build.VERSION.SDK_INT < 4) {
throw new RuntimeException("Multi dex installation failed. SDK " + Build.VERSION.SDK_INT + " is unsupported. Min SDK version is " + 4 + ".");
}
try {
final ApplicationInfo applicationInfo = getApplicationInfo(context);
if (applicationInfo == null) {
return;
}
synchronized (MultiDex.installedApk) {
//installedApk的类型是:Set,如果apk文件已经被加载过了,就返回
final String apkPath = applicationInfo.sourceDir;
if (MultiDex.installedApk.contains(apkPath)) {
return;
}
MultiDex.installedApk.add(apkPath);
if (Build.VERSION.SDK_INT > 20) {
Log.w("MultiDex", "MultiDex is not guaranteed to work in SDK version " + Build.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") + "\"");
}
//类加载器,PathClassLoader
ClassLoader loader;
try {
loader = context.getClassLoader();
}
catch (RuntimeException e) {
Log.w("MultiDex", "Failure while trying to obtain Context class loader. Must be running in test mode. Skip patching.", (Throwable)e);
return;
}
if (loader == null) {
Log.e("MultiDex", "Context class loader is null. Must be running in test mode. Skip patching.");
return;
}
try {
//清除之前的Dex文件夹,之前的Dex放置在这个文件夹
//final File dexDir = new File(context.getFilesDir(), "secondary-dexes");
clearOldDexDir(context);
}
catch (Throwable t) {
Log.w("MultiDex", "Something went wrong when trying to clear old MultiDex extraction, continuing without cleaning.", t);
}
//dex将会输出到/data/data/{packagename}/code_cache/secondary-dexes目录。
final File dexDir = new File(applicationInfo.dataDir, MultiDex.SECONDARY_FOLDER_NAME);
//将Dex文件加载为File对象
List files = MultiDexExtractor.load(context, applicationInfo, dexDir, false);
//校验这些zip文件是否合法
if (checkValidZipFiles(files)) {
//正式安装其他Dex文件
installSecondaryDexes(loader, dexDir, files);
} else {//不合法的情况下强制load一遍
Log.w("MultiDex", "Files were not valid zip files. Forcing a reload.");
//最后一个参数是true,代表强制加载
files = MultiDexExtractor.load(context, applicationInfo, dexDir, true);
//还是不合法的就抛异常
if (!checkValidZipFiles(files)) {
throw new RuntimeException("Zip files were not valid.");
}
//终于合法了,安装zip文件
installSecondaryDexes(loader, dexDir, files);
}
}
}
catch (Exception e2) {
Log.e("MultiDex", "Multidex installation failure", (Throwable)e2);
throw new RuntimeException("Multi dex installation failed (" + e2.getMessage() + ").");
}
Log.i("MultiDex", "install done");
}
//从上面的过程来看,只是完成了加载包含着Dex文件的zip文件,具体的加载操作都在下面的方法中
//在这个方法里面进行将第二个 dex 的代码加载到程序中。
private static void installSecondaryDexes(final ClassLoader loader, final File dexDir, final List files) throws IllegalArgumentException, IllegalAccessException, NoSuchFieldException, InvocationTargetException, NoSuchMethodException, IOException {
if (!files.isEmpty()) {
if (Build.VERSION.SDK_INT >= 19) {
install(loader, files, dexDir);
}
else if (Build.VERSION.SDK_INT >= 14) {
install(loader, files, dexDir);
}
else {
install(loader, files);
}
}
}
//到这里为了完成不同版本的兼容,实际调用了不同类的方法,我们仅看一下>=14的版本,其他的类似
//在 Java 层的主要流程将第二个 dex 取出(现在只考虑两个 dex 的情况),整成 Zip 形式的,然后通过反射将 zip 的地址等参数封装起来再塞给 PathClassLoader 。为什么是 Zip ,因为在 BaseDexClassLoader 中 DexFile.loadDex() 只接受 jar 或者 zip。
private static final class V14
{
//optimizedDirectory地址是/data/data/{packagename}/code_cache/secondary-dexes
private static void install(final ClassLoader loader, final List additionalClassPathEntries, final File optimizedDirectory) throws IllegalArgumentException, IllegalAccessException, NoSuchFieldException, InvocationTargetException, NoSuchMethodException {
//通过反射获取loader的pathList字段,loader是由Application.getClassLoader()获取的,实际获取到的是PathClassLoader对象的pathList字段
final Field pathListField = findField(loader, "pathList");
final Object dexPathList = pathListField.get(loader);
//dexPathList是PathClassLoader的私有字段,里面保存的是Main Dex中的class
//dexElements是一个数组,里面的每一个item就是一个Dex文件
//makeDexElements()返回的是其他Dex文件中获取到的Elements[]对象,内部通过反射makeDexElements()获取
//expandFieldArray是为了把makeDexElements()返回的Elements[]对象添加到dexPathList字段的成员变量dexElements中
expandFieldArray(dexPathList, "dexElements", makeDexElements(dexPathList, new ArrayList(additionalClassPathEntries), optimizedDirectory));
}
private static Object[] makeDexElements(final Object dexPathList, final ArrayList files, final File optimizedDirectory) throws IllegalAccessException, InvocationTargetException, NoSuchMethodException {
//反射拿到dexPathList的方法makeDexElements,private static Element[] makeDexElements(ArrayList files, File optimizedDirectory, ArrayList suppressedExceptions)
final Method makeDexElements = findMethod(dexPathList, "makeDexElements", (Class>[])new Class[] { ArrayList.class, File.class });
//调用方法,该方法的作用是通过传入的files去加载jar或者zip,封装成DexFile,在封装成Element返回
return (Object[])makeDexElements.invoke(dexPathList, files, optimizedDirectory);
}
private static void expandFieldArray(Object instance, String fieldName, Object[] extraElements) throws NoSuchFieldException, IllegalArgumentException, IllegalAccessException {
//找到dexElements这个属性
Field jlrField = findField(instance, fieldName);
//classloader中原始的dexElements
Object[] original = (Object[])((Object[])jlrField.get(instance));
//new一个新的出来
Object[] combined = (Object[])((Object[])Array.newInstance(original.getClass().getComponentType(), original.length + extraElements.length));
//将原有的复制到新的里面去
System.arraycopy(original, 0, combined, 0, original.length);
//将第二个dex的复制到新的里面去
System.arraycopy(extraElements, 0, combined, original.length, extraElements.length);
//再塞回去
jlrField.set(instance, combined);
}
}
}
实际上寻找class时,会调用PathClassLoader#findClass()
,会在pathList中寻找.
因此通过反射手动添加其他Dex文件中的class到pathList字段中,就可以实现类的动态加载,这也是MutiDex方案的基本原理
通过查看MultiDex的源码,可以发现MultiDex在冷启动时,因为会同步的反射安装Dex文件,进行IO操作,容易导致ANR
为 Dalvik 可执行文件分包构建每个 DEX 文件时,构建工具会自动的执行复杂的决策制定来确定主要 DEX 文件中需要的类,以便应用能够成功启动。
如果启动期间需要的任何类未在主 DEX 文件中提供,那么您的应用将崩溃并出现错误 java.lang.NoClassDefFoundError。该情况不应出现在直接从应用代码访问的代码上,因为构建工具能识别这些代码路径,但可能在代码路径可见性较低(如使用的库具有复杂的依赖项)时出现。例如,如果代码使用自检机制或从原生代码调用 Java 方法,那么这些类可能不会被识别为主 DEX 文件中的必需项
因此,如果您收到 java.lang.NoClassDefFoundError,则必须使用构建类型中的 multiDexKeepFile 或 multiDexKeepProguard 属性声明它们,以手动将这些其他类指定为主 DEX 文件中的必需项。如果类在 multiDexKeepFile 或 multiDexKeepProguard 文件中匹配,则该类会添加至主 DEX 文件
com/example/MyClass.class
com/example/MyOtherClass.class
Gradle 会读取相对于 build.gradle 文件的路径,因此如果 multidex-config.txt 与 build.gradle 文件在同一目录中,以上示例将有效
android {
buildTypes {
release {
multiDexKeepFile file 'multidex-config.txt'
...
}
}
}
multiDexKeepProguard 文件使用与 Proguard 相同的格式,并且支持整个 Proguard 语法
-keep class com.example.MyClass
-keep class com.example.MyClassToo
//如果您想要指定包中的所有类,文件将如下所示:
-keep class com.example.** { *; } // All classes in the com.example package
android {
buildTypes {
release {
multiDexKeepProguard 'multidex-config.pro'
...
}
}
}
优化开
一、配置可以分包:
defaultConfig {
******
//分包1
multiDexEnabled true
}
二、指定maindex需要包含的类,在APP目录下的maindexlist.txt 里面,这里面有两种配置,一种是高版本的gradle的配置,一种是低版本的gradle配置。
//分包2(高版本的gradle)
dexOptions {
javaMaxHeapSize "4g"
preDexLibraries = false
additionalParameters = ['--multi-dex', '--main-dex-list='+ project.rootDir.absolutePath + '/app/maindexlist.txt', '--minimal-main-dex',
'--set-max-idx-number=1000']
}
//分包2(低版本的gradle配置)
//只在1.4以下管用,在1.4+版本的gradle中,app:dexXXX task 被隐藏了(更多信息请参考Gradle plugin的更新信息),jacoco, progard, multi-dex三个task被合并了。
afterEvaluate {
tasks.matching {
it.name.startsWith('dex')
}.each { dx ->
def listFile = project.rootDir.absolutePath+'/app/maindexlist.txt'
if (dx.additionalParameters == null) {
dx.additionalParameters = []
}
//表示当方法数越界时则生成多个dex文件(我的没有越界,貌似也生成了两个),代表采用多Dex分包
dx.additionalParameters += '--multi-dex'
//这个指定了listFile中的类(即maindexlist.txt中的类)会打包到主dex中,不过注意下一条。
dx.additionalParameters += '--main-dex-list=' +listFile
//表明只有-main-dex-list所指定的类(在我的配置中,就是app目录下的maindexlist.txt中包含的类)才能打包到主dex中,如果没有这个选项,上个选项就会失效
dx.additionalParameters += '--minimal-main-dex'
}
}
//或者 hook createDebugMainDexClassList task
tasks.whenTaskAdded {task ->
if (task.name.startsWith("create") && task.name.endsWith("MainDexClassList") {
task.doLast {
File tempFile
File keepFile
if (task.name.contains("Debug")) {
tempFile = new File("$project.rootDir/MyProject/keep_in_maindexlist_debug.txt")
keepFile = new File("${project.buildDir}/intermediates/debug/maindexlist.txt")
} else if (task.name.contains("Release")) {
// Release时类似处理
}
tempFile.eachLine("utf-8") { str, linenumber ->
keepFile.append(str + "\n")
}
}
}
}
三、添加分包插件依赖:
dependencies {
****
//分包3
compile 'com.android.support:multidex:1.0.2'
}
四、【官方辅助】按照上面三步配置完,打包再解包,如果你发现你自己指定的maindex并没有都在maindex里面(至少我的是这样),可以借助官方手段,就是第一步的时候设置可以分包并且指定miandexlist.txt,如下:
defaultConfig {
//分包1
multiDexEnabled true
multiDexKeepProguard file('multiDexKeep.pro') // keep specific classes using proguard syntax
multiDexKeepFile file('maindexlist.txt') // keep specific classes
}
maindexlist.txt.自己写还是相当麻烦的,可以借助脚本生成。
取巧的话,在\app\build\intermediates\multi-dex\debug目录下找到了一个maindexlist.txt,注意,这个你改了没用,一运行又恢复了,将这个复制到app目录下自己的maindexlist中,然后再加上自己的需要配置的类
美团有自己的脚本程序找启动依赖类,但人家没开!源!!啦!!!还好Google到了CDA(Class Dependency Analyzer),通过这个工具,基本能找到启动过程中所有Activity、Application等相关依赖类,通常会有一定偏差(会将某些系统方法也找出来了.但是要尤其注意 release中混淆的处理,引发的路径问题 参看Android MultiDex实践:如何绕过那些坑?
MultiDex会大幅增加构建处理时间,因为构建系统必须就哪些类必须包括在主 DEX 文件中以及哪些类可以包括在辅助 DEX 文件中作出复杂的决策。这意味着使用 Dalvik 可执行文件分包的增量式构建通常耗时更长,可能会拖慢开发进度
为了缩短耗时更长的 Dalvik 可执行文件分包输出构建时间,请利用 productFlavors(一个开发定制和一个发布定制,具有不同的 minSdkVersion 值)创建两个构建变型:
android {
defaultConfig {
...
multiDexEnabled true
}
productFlavors {
dev {
// Enable pre-dexing to produce an APK that can be tested on
// Android 5.0+ without the time-consuming DEX build processes.
minSdkVersion 21
}
prod {
// The actual minSdkVersion for the production version.
minSdkVersion 14
}
}
buildTypes {
release {
minifyEnabled true
proguardFiles getDefaultProguardFile('proguard-android.txt'),
'proguard-rules.pro'
}
}
}
dependencies {
compile 'com.android.support:multidex:1.0.1'
}
完成此配置变更后,可以为增量式构建使用应用的 devDebug 变体,后者集 dev 产品定制与 debug 构建类型的属性于一身。这将创建已启用 Dalvik 可执行文件分包且禁用 proguard 的可调试应用(因为 minifyEnabled 默认为 false)。这些设置会使适用于 Gradle 的 Android 插件执行以下操作:
您还可以构建其他变体,包括 prodDebug 变体构建,该变体虽然构建时间更长,但可用于开发以外的测试。在所示配置内,prodRelease 变体将是最终测试和发布版本。如需了解有关使用构建变体的详细信息,请参阅配置构建变体
官方文档中,已经承认这种分包方案具有一些已知的局限性:
由于存在 Dalvik linearAlloc 错误(问题 22586),使用 Dalvik 可执行文件分包的应用可能无法在运行的平台版本早于 Android 4.0(API 级别 14)的设备上启动。如果您的目标 API 级别低于 14,请务必针对这些版本的平台进行测试,因为您的应用可能会在启动时或加载特定类群时出现问题。代码压缩可以减少甚至有可能消除这些潜在问题。
由于存在 Dalvik linearAlloc 限制(问题 78035),因此,如果使用 Dalvik 可执行文件分包配置的应用发出非常庞大的内存分配请求,则可能会在运行期间发生崩溃。尽管 Android 4.0(API 级别 14)提高了分配限制,但在 Android 5.0(API 级别 21)之前的 Android 版本上,应用仍有可能遭遇这一限制
启动期间在设备数据分区中安装 DEX 文件的过程相当复杂,如果辅助 DEX 文件较大,可能会导致首次加载时会出现明显的黑屏,甚至应用无响应 (ANR) 错误。在此情况下,您应该通过 ProGuard 应用代码压缩以尽量减小 DEX 文件的大小,并移除未使用的那部分代码
分包数量过多引起安装失败,分到第6个以后容易出现,原理同上面一点
复杂的依赖的工程,分包后,不同依赖项目间的dex文件函数相互调用,报错找不到方法
带有混淆的工程,非常容易出现依赖沾粘(不同依赖项目间的dex文件同一个类定义树),安装时报告类定义安全检查异常
工程过大,且依赖管理混乱时,主分包因为必须加载,方法数还是超过了65536,导致主分包无法生成(后文会降到解决方案)
==在ART下MultiDex是不存在这些问题的==,这主要是因为ART下采用Ahead-of-time (AOT) compilation技术,系统在APK的==安装过程==中会使用自带的dex2oat工具对APK中可用的DEX文件进行编译并生成一个可在本地机器上运行的oat文件(5.0以下只能苦逼的启动时加载),这样能提高应用的启动速度,因为是在安装过程中进行了处理这样==会影响应用的安装速度==,对ART感兴趣的可以参考一下ART和Dalvik的区别
前面说的Issue 22586问题:部分低端2.3机型安装失败,INSTALL_FAILED_DEXOPT
apk是一个zip压缩包,dalvik每次加载apk都要从中解压出class.dex文件,加载过程还涉及到dex的classes需要的杂七杂八的依赖库的加载,这无疑是一个耗时操作。为了提高效率,Android进行了如下优化:
这样以空间换时间大大缩短读取/加载dex文件的过程.
期间,dexopt程序的dalvik分配一块内存来统计你的app的dex里面的classes的信息,Android 2.2和2.3的缓冲区只有5MB,Android 4.x提高到了8MB或16MB。当方法数量过多导致超出缓冲区大小时(不再是dex65536),就会导致dexopt failed
减小dex的大小
这个linearAlloc的限制不仅仅在安装时候的dexopt程序里 7,还在你的app的dalvik rumtime里
FB工程师Read The Fucking Source Code提出了一个hack方案:这个linearAlloc的size定义在c层而且是一个全局变量,他们通过对结构体的size的计算成功覆盖了该值的内容
这里要特别感谢C语言的指针和内存的设计,然而这个Hack方的实现是比较困难的,同时dvk虚拟机c层代码在2.x 4.x 版本里有变更,找到那个内存地址太难,未必成功
我们有偷懒的解决方案,为了避免2.3机型runtime 的linearAlloclimit ,最好保持每一个dex体积<4M ,刚才的的value<=48000
android.applicationVariants.all {
variant ->
dex.doFirst{
dex->
if (dex.additionalParameters == null) {
dex.additionalParameters = []
}
dex.additionalParameters += '--set-max-idx-number=48000'
}
}
–set-max-idx-number= 用于控制每一个dex的最大方法个数,写小一点可以产生好几个dex
出现这个错误时,解决办法是将异常中的这个类加至 mainDex 中。但是这个错误跟 NotClassFoundException的区别,可查阅 链接。
其出现这个问题的说法,简单理解为Multidex默认的分dex实现保证了应用内四大组件的class都在主dex中,但仍然会有NoClassXXX类型的crash出现。因为Android 加载Dex files采用的是Lazy Load,这会导致虚拟机中即使已经加载了某个class,但如果这个class不在主dex的class列表中,则主dex有可能引用不到这个class,从而导致NoClassDefFoundError
在 module 下创建 multidex.keep 文件,并在其中罗列出那些 class,以便让编译器知道在 main dex 文件中要保持哪些 class。
如果你在本地的测试机上没有遇到这个问题,并不代表你的 APP 没有问题,通过查看友盟的崩溃记录和使用一些真机测试平台来进行检查,通常情况下会有所发现
1. 使用下述任意方式配置完成后,clean 然后 rebuild 项目,完成之后在 module 下的build/intermediates/multi-dex/xxx里找到 maindexlist.txt 文件(如果找不到相关目录,可能需要你同步后 rebuild 项目才能生成),复制里面的内容到 module 根目录下 multidex.keep 文件中(没有则先创建此文件)。
- 然后,比较重要的一步就是:通过友盟、测试记录、Bug记录等获取到 NoClassDefFoundError 错误对应的类,按照 maindexlist.txt 文件的方式添加这些类到 multidex.keep 文件中就可解决了
如果觉得一个一个添加NoClassDefFoundError异常类麻烦,可以一次性的找出在应用启动后,虚拟机中已经加载但不在主dex中的class列表的所有class,记录到一个multidex.keep的文本文件中。该查找方法可以通过在应用启动后一个合适的时机调用MultiDexUtils的getLoadedExternalDexClasses方法来手动收集:
/**
* Get all loaded external classes name in "classes2.dex", "classes3.dex" ....
* @param context
* @return get all loaded external classes
*/
public List getLoadedExternalDexClasses(Context context) {
try {
final List externalDexClasses = getExternalDexClasses(context);
if (externalDexClasses != null && !externalDexClasses.isEmpty()) {
final ArrayList classList = new ArrayList();
final java.lang.reflect.Method m = ClassLoader.class.getDeclaredMethod("findLoadedClass", new Class[]{String.class});
m.setAccessible(true);
final ClassLoader cl = context.getClassLoader();
for (String clazz : externalDexClasses) {
if (m.invoke(cl, clazz) != null) {
classList.add(clazz.replaceAll("\\.", "/").replaceAll("$", ".class"));
}
}
return classList;
}
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
手动获取了multidex.keep文件之后,接下来需要修改Gradle 编译脚本:在Gradle打包生成Dex文件之前将multidex.keep合并到主Dex中,从而保证主Dex的加载不会发生NoClassDefFoundError
在这里,我们不再讲解语法相关的,而是讨论下如何让Gradle自动把multidex.keep 和 由Gradle生成的maindexlist.txt 结合在一起
apply plugin: 'com.android.application'
android {
...
}
dependencies {
...
}
//【1】Hook android gradle multidex list 相关 task:在createXXXMainDexClassList task之后插入一个自定义task
tasks.whenTaskAdded { task ->
android.applicationVariants.all { variant ->
if (task.name == "create${variant.name.capitalize()}MainDexClassList") {
task.finalizedBy "fix${variant.name.capitalize()}MainDexClassList"
}
}
}
//【2】在构建变种variant中加入该自定义task的声明
android.applicationVariants.all { variant ->
task "fix${variant.name.capitalize()}MainDexClassList" << {
logger.info "Fixing main dex keep file for $variant.name"
File keepFile = new File("$buildDir/intermediates/multi-dex/$variant.buildType.name/maindexlist.txt")
keepFile.withWriterAppend { w ->
// Get a reader for the input file
w.append('\n')
new File("${projectDir}/multidex.keep").withReader { r ->
// And write data from the input into the output
w << r << '\n'
}
logger.info "Updated main dex keep file for ${keepFile.getAbsolutePath()}\n$keepFile.text"
}
}
}
//【2】另一种写法
android.applicationVariants.all { variant ->
task "fix${variant.name.capitalize()}MainDexClassList" << {
println "Fixing main dex keep file for $variant.name, while the build type is release."
if (new File("${rootProject.projectDir}/buildsystem/multidex.keep").exists()
&& variant.buildType.name == 'release'
&& project.android.defaultConfig.multiDexEnabled) {
File keepFile = new File("$buildDir/intermediates/multi-dex/${variant.dirName}/maindexlist.txt")
// Step1 利用multidex.keep的列表找到混淆后的class name
// Read proguard mapping file to find real class name in dex file
def mappingList = ["key":"value"];
File mapping = new File("$buildDir/outputs/mapping/${variant.dirName}/mapping.txt")
if (mapping.exists()) {
mapping.eachLine { line ->
if (!line.startsWith(" ") && line.endsWith(":")) {
String key = line.split("->")[0].trim();
String value = line.split("->")[1].trim().split(":")[0].trim();
mappingList.put(key, value);
}
}
}
keepFile.withWriterAppend { w ->
// Get a reader for the input file
w.append('\n')
// Step2 将对应的class list插进入multidex的构建产物maindexlist.txt 。
new File("${rootProject.projectDir}/buildsystem/multidex.keep").withReader { r ->
boolean hasFindMapping = false
// And write data from the input into the output
mappingList.each {
if (it.key.equals(r)) {
r = it.value;
hasFindMapping = true
}
}
w << r << '\n'
w.flush()
}
println "Updated main dex keep file for ${keepFile.getAbsolutePath()}"
}
} else {
println 'There is no multidex.keep file in your project root dir or build type is debug or multidex not enabled.'
}
}
}
apply plugin: 'com.android.application'
android {
...
afterEvaluate {
tasks.matching {
it.name.startsWith('dex')
}.each { dx ->
if (dx.additionalParameters == null) {
dx.additionalParameters = []
}
//允许生成多个dex文件
dx.additionalParameters += '--multi-dex' // enable multidex
// optional
// 设置multidex.keep文件中class为第一个dex文件中包含的class,如果没有下一项设置此项无作用
dx.additionalParameters += "--main-dex-list=$projectDir/class-list.txt".toString() // enable the main-dex-list
//此项添加后第一个classes.dex文件只能包含-main-dex-list列表中class
dx.additionalParameters += '--minimal-main-dex'
}
}
}
dependencies {
...
}
这一步直接对应 dx 最终的调用,即修改我们上文所提到的参数值,将其替换我们手动填充的值,但是这一步的 multidex.keep 文件就需要我们折腾一二了
不过针对这个方案,笔者是一直没有找到在 Task 中相对应的以 dex 开头的任务,所以这个方案没有生效。
那为什么会有这种写法呢?笔者在 Project中的 Variant中相对应的 ApkVariant类中看到一点信息,此接口定义了 getDex()方法,对应实现在 ApkVariantImpl中如下:
@Nullable
@Override
public Object getDex() {
throw new RuntimeException("Access to the dex task is now impossible, starting with 1.4.0/n" + "1.4.0 introduces a new Transform API allowing manipulation of the .class files./n" + "See more information: http://tools.android.com/tech-docs/new-build-system/transform-api");
}
代码中返回的值就是这个方案中与 dx相对应的值。不过从异常信息中可以看到的是在 gradle plugin 1.4.0 的版本开始,此方法就已被废弃,而改为采用 transform 的实现
所以此方案只针对 gradle plugin 1.4.0 之前的版本
GitHub-https://github.com/casidiablo/multidex/issues/7
不仅仅是2.3 的机型,还有一些中档配置的4.x系统的机型,第一次安装后,点击图标,1s,2s,3s… 5s后,程序没有任何反应就好像你没点图标一样,再然后程序ANR
dexopt一般仅需要触发一次,生成odex后存放在系统文件路径下;非首次启动则直接从cache中读取已经执行过dexopt的ODEX文件,这个过程对启动并无太大影响
以上操作必须在5s内完成,否则导致UI线程阻塞,最终就导致ANR。一般情况下,往往是由于==第二个dex太大了导致MultiDex.install(Context context)的dexopt过程耗时过长==,可以简单将MultiDex.install理解为 “dexopt” + “加载odex”两个过程
我们来思考下解决方案:
主dex是无论如何都绕不过加载和dexopt的——如果主dex比较小的话可以节省时间—–但是主dex小就意味着后面的dex大,MultiDex.install是在主线程里做的,总时间又没有实质性改变—-不行
install能不能放到线程里做:开新线程加载,而主线程继续Application初始化—-如果异步化,multidex安装没有结束意味着dex还没加载进来,这时候如果进程需要seconday.dex里的classes信息不就悲剧了—-某些类强行使用就会报NoClassDefFoundError
还是看看大厂的吧:
精简主dex+异步加载secondary.dex 。对异步化执行速度的不确定性,他们的解决方案是重写Instrumentation execStartActivity 方法,hook跳转Activity的总入口做判断,如果当前secondary.dex 还没有加载完成,就弹一个loading Activity等待加载完成,如果已经加载完成那最好不过了
- 美团Android DEX自动拆包及动态加载简介
在21版本之前的Dalvik的VM版本中,MultiDex的安装大概分为几步:
这三步其实都比较耗时,由此我们可以有这样的思路:
- 对于ANR问题
- 为了解决ANR问题考虑是否可以把DEX的加载放到一个异步线程中,这样冷启动速度能提高不少,同时能够减少冷启动过程中的ANR
- 对于Dalvik linearAlloc的一个缺陷(Issue 22586)和限制(Issue 78035)
- 考虑是否可以人工对DEX的拆分进行干预,使每个DEX的大小在一定的合理范围内,这样就减少触发Dalvik linearAlloc的缺陷和限制
也就是说,
为了实现这2个目的,我们需要解决下面三个问题:
【1】我们首先来分析如何解决第一个问题,在使用MultiDex方案时,我们知道BuildTool会自动把代码进行拆成多个DEX包,并且可以通过配置文件来控制哪些代码放到第一个DEX包中.
为了实现产生多个DEX包,我们可以在生成DEX文件的这一步中, 在Ant或gradle中自定义一个Task来干预DEX产生的过程,从而产生多个DEX,下图是在ant和gradle中干预产生DEX的自定task的截图:
tasks.whenTaskAdded { task ->
if (task.name.startsWith('proguard') && (task.name.endsWith('Debug') || task.name.endsWith('Release'))) {
task.doLast {
makeDexFileAfterProguardJar();
}
task.doFirst {
delete "${project.buildDir}/intermediates/classes-proguard";
String flavor = task.name.substring('proguard'.length(), task.name.lastIndexOf(task.name.endsWith('Debug') ? "Debug" : "Release"));
generateMainIndexKeepList(flavor.toLowerCase());
}
} else if (task.name.startsWith('zipalign') && (task.name.endsWith('Debug') || task.name.endsWith('Release'))) {
task.doFirst {
ensureMultiDexInApk();
}
}
}
【2】上一步解决了如何打包出多个DEX的问题了,那我们该怎么该根据什么来决定哪些class放到Main DEX,哪些放到Secondary DEX呢(这里的Main DEX是指在2.1版本的Dalvik VM之前由android系统在启动apk时自己主动加载的Classes.dex,而Secondary DEX是指需要我们自己安装进去的DEX,例如:Classes2.dex, Classes3.dex等)
这个需要分析出放到Main DEX中的class依赖,需要确保把Main DEX中class所有的依赖都要放进来,否则在启动时会发生ClassNotFoundException, 这里美团的方案是
为了减少人工分析class的依赖所带了的不可维护性和高风险性,美团编写了一个能够自动分析Class依赖的脚本, 从而能够保证Main DEX包含class以及他们所依赖的所有class都在其内,这样这个脚本就会在打包之前自动分析出启动到Main DEX所涉及的所有代码,保证Main DEX运行正常
【3】如果我们在后台加载Secondary DEX过程中,用户点击界面将要跳转到使用了在Secondary DEX中class的界面, 那此时必然发生ClassNotFoundException, 那怎么解决这个问题呢,在所有的Activity跳转代码处添加判断Secondary DEX是否加载完成?这个方法可行,但工作量非常大; 那有没有更好的解决方案呢?
我们通过分析Activity的启动过程,发现Activity是由ActivityThread 通过Instrumentation来启动的,我们是否可以在Instrumentation中做一定的手脚呢?
通过分析代码ActivityThread和Instrumentation发现,Instrumentation有关Activity启动相关的方法大概有:execStartActivity、newActivity等等,这样我们就可以在这些方法中添加代码逻辑进行判断这个Class是否加载了:
这样在代码充分解耦合,以及每个业务代码能够做到颗粒化的前提下,我们就做到Secondary DEX的按需加载了, 下面是Instrumentation添加的部分关键代码:
public ActivityResult execStartActivity(Context who, IBinder contextThread, IBinder token, Activity target,
Intent intent, int requestCode) {
ActivityResult activityResult = null;
String className;
if (intent.getComponent() != null) {
className = intent.getComponent().getClassName();
} else {
ResolveInfo resolveActivity = who.getPackageManager().resolveActivity(intent, 0);
if (resolveActivity != null && resolveActivity.activityInfo != null) {
className = resolveActivity.activityInfo.name;
} else {
className = null;
}
}
if (!TextUtils.isEmpty(className)) {
boolean shouldInterrupted = !MeituanApplication.isDexAvailable();
if (MeituanApplication.sIsDexAvailable.get() || mByPassActivityClassNameList.contains(className)) {
shouldInterrupted = false;
}
if (shouldInterrupted) {
Intent interruptedIntent = new Intent(mContext, WaitingActivity.class);
activityResult = execStartActivity(who, contextThread, token, target, interruptedIntent, requestCode);
} else {
activityResult = execStartActivity(who, contextThread, token, target, intent, requestCode);
}
} else {
activityResult = execStartActivity(who, contextThread, token, target, intent, requestCode);
}
return activityResult;
}
public Activity newActivity(Class> clazz, Context context, IBinder token,
Application application, Intent intent, ActivityInfo info,
CharSequence title, Activity parent, String id, Object lastNonConfigurationInstance)
throws InstantiationException, IllegalAccessException {
String className = "";
Activity newActivity = null;
if (intent.getComponent() != null) {
className = intent.getComponent().getClassName();
}
boolean shouldInterrupted = !MeituanApplication.isDexAvailable();
if (MeituanApplication.sIsDexAvailable.get() || mByPassActivityClassNameList.contains(className)) {
shouldInterrupted = false;
}
if (shouldInterrupted) {
intent = new Intent(mContext, WaitingActivity.class);
newActivity = mBase.newActivity(clazz, context, token,
application, intent, info, title, parent, id,
lastNonConfigurationInstance);
} else {
newActivity = mBase.newActivity(clazz, context, token,
application, intent, info, title, parent, id,
lastNonConfigurationInstance);
}
return newActivity;
}
分析主dex需要的classes这个脚本比较难写。
由于历史原因,项目维护的App的manifest注册的组件的那些类,承载业务太多,依赖很多三方jar,导致直接依赖类非常多,而且短时间内无法梳理精简,没办法mini化主dex
Application的启动入口太多。Appication初始化未必是由launcher Activity的启动触发,还有可能是因为Service ,Receiver ,ContentProvider 的启动。 靠拦截重写Instrumentation execStartActivity 解决不了问题。要为 Service ,Receiver ,ContentProvider 分别写基类,然后在oncreate()里判断是否要异步加载secondary.dex。如果需要,弹出Loading Acitvity?用户看到这个会感觉比较怪异
结合自身App的实际情况来看美团的拆包方案虽然很美好然但是不能照搬啊
对于微信来说,一共有111052个方法。以线性内存3355444(限制5m,给系统预留部分)、方法数64K为限制,即当满足任意一个条件时,将拆分dex。由此微信将得到一个主dex,两个子dex,若微信采用Android原生的MultiDex方案,在首次启动时将长期无响应(没有出现黑屏时因为默认皮肤的原因)
微信与手Q的方案是类似的,将首次加载放于地球中,并用线程去加载(但是5.0之前加载dex时还是会挂起主线程)
Android拆分与加载Dex的多种方案对比
Dex形式
Dex类分包的规则
public MainDexListBuilder(String rootJar, String pathString) throws IOException {
path = new Path(pathString);
ClassReferenceListBuilder mainListBuilder=new ClassReferenceListBuilder(path);
}
(name md5 校验是否加载成功类)
secondary-1.dex.jar 63e5240eac9bdb5101fc35bd40a98679 secondary.dex01.Canary
secondary-2.dex.jar e7d2a4a181f579784a4286193feaf457 secondary.dex02.Canary
总的来说,这种方案用户体验较好,缺点在于太过复杂,每次都需重新扫描依赖集,而且使用的是比较大的间接依赖集(要真正运行到,直接依赖集是不行的)。当前微信必要的依赖集已经41306个方法,说不定哪一天就爆了
安装完成之后第一次启动时,是secondary.dex的dexopt花费了更多的时间,认识到这点非常重要,使得问题转化为:在不阻塞UI线程的前提下,完成dexopt,以后都不需要再次dexopt,所以可以在UI线程install dex了
我们现在想做到的是:既希望在Application的attachContext()方法里同步加载secondary.dex,又不希望卡住UI线程
FB的方案就是:
Facebook方案在于多起一个nodex进程作为 fake Main Process,在node进程中先显示一个简单页面并判断是否已经加载过dex形成odex,如果是则正常启动real Main界面并唤醒Real Main Process,如果不是则需要在Activity中开启一个异步任务开始加载合并Dex,加载完成后显式启动real Main界面并唤醒Real Main Process
<activity android:exported="false" android:process=":nodex"
android:name="com.facebook.nodex.startup.splashscreen.NodexSplashActivity">
这种方式好处在于依赖集非常简单,同时首次加载Dex时也不会卡死。
但是它的缺点也很明显,即每次启动主进程时,都需先启动nodex进程。尽管nodex进程逻辑非常简单,这也需100ms以上。
若对启动时间非常敏感,很难会去直接采用这个方案
Facebook的缺陷在于每次都要多起一个nodex进程,无论怎么规避,这个进程启动的耗时都无法避免,因此进一步的优化方案,只能是在主进程做到同样的效果
新的思路虽然也会唤起新进程,但是该进程只会触发一次,可以接受。现在转化为两个问题:
通过何种方式挂起主进程?
挂住主进程过程中,是否会产生ANR?
import android.app.ActivityManager;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.os.Build;
import android.support.multidex.MultiDex;
import com.xx.xx.common.BaseFunctionConfig;
import com.xx.xx.log.LogCore;
import com.xx.xx.login.activity.LoadResActivity;
import com.xx.xx.utils.StringUtils;
import java.util.Map;
import java.util.jar.Attributes;
import java.util.jar.JarFile;
/**
* 类描述:
*
* Created by yhf on 2018/1/19.
*/
public class BaseMultiDexApplication extends BaseMoaApplication {
public static final String TAG = "BaseMultiDexApplication";
public static final String KEY_DEX2_SHA1 = "dex2-SHA1-Digest";
@Override
protected void attachBaseContext(Context base) {
super .attachBaseContext(base);
LogCore.i( TAG, "App attachBaseContext ");
//是<5.0的系统 && 是主进程(不是loaddex进程) 则进入异步加载方案
if (!isLoadDexProcess() && Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
if (needWait(base)){//判断dexopt未执行过
waitForDexopt(base);
}
//主进程加载dex,
//此时odex已经产生(dexopt操作已经在 loaddexActivity中执行过了,或者不是第一次打开应用),所以不会有耗时问题
MultiDex.install (this );
} else {
//>=5.0的系统默认对dex进行oat优化,不需要MultiDex.install (this );
return;
}
}
@Override
public void onCreate() {
super .onCreate();
if (isLoadDexProcess()) {
return;
}
}
/*****************************判断是否是 loaddex进程********************************/
public boolean isLoadDexProcess() {
if (StringUtils.containsIgnoreCase( getCurProcessName(this), ":mini")) {
LogCore.i( TAG, ":mini start!");
return true;
}
return false ;
}
/*****************************判断dexopt是否已经执行过********************************/
/**
* 判断dexopt是否已经执行过
* 通过校验本地存储的md5记录和apk里的classes2.dex是否一致
* neead wait for dexopt ?
*/
private boolean needWait(Context context){
String flag = get2thDexSHA1(context);
LogCore.i( TAG, "dex2-sha1 "+flag);
SharedPreferences sp = context.getSharedPreferences(
getPackageInfo(context).versionName, MODE_MULTI_PROCESS);
String saveValue = sp.getString(KEY_DEX2_SHA1, "");
return !StringUtils.equals(flag,saveValue);
}
//Get classes.dex file signature
private String get2thDexSHA1(Context context) {
ApplicationInfo ai = context.getApplicationInfo();
String source = ai.sourceDir;
try {
JarFile jar = new JarFile(source);
java.util.jar.Manifest mf = jar.getManifest();
Map map = mf.getEntries();
Attributes a = map.get("classes2.dex");
return a.getValue("SHA1-Digest");
} catch (Exception e) {
e.printStackTrace();
}
return null ;
}
/*****************************阻塞等待********************************/
/**
* 1. 启动 异步dexopt的跨进程Activity
* 2. 阻塞当前主进程
* 3. 200ms间隔轮训dexopt是否完成,超时或者已完成,则唤醒主进程
* 3.1 在attachContext中的MultixDex.install,如果超时,则主进程自己再次同步执行dexopt,相当于恢复到官方默认方案;
* 3.2 在attachContext中的MultixDex.install,如果已完成,则主进程不再执行的dexopt,单纯加载odex,提升速度
*/
public void waitForDexopt(Context base) {
Intent intent = new Intent();
ComponentName componentName = new
ComponentName(BaseFunctionConfig.PACKAGE_NAME, LoadResActivity.class.getName());
intent.setComponent(componentName);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
base.startActivity(intent);
long startWait = System.currentTimeMillis ();
long waitTime = 10 * 1000 ;
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB_MR1 ) {
waitTime = 20 * 1000 ;//实测发现某些场景下有些2.3版本有可能10s都不能完成optdex
}
while (needWait(base)) {
//application启动了LoadDexActivity之后,自身不再是前台进程所以怎么hold 线程都不会ANR
try {
long nowWait = System.currentTimeMillis() - startWait;
LogCore.i( TAG, "wait ms :" + nowWait);
if (nowWait >= waitTime) {
return;
}
Thread.sleep(200 );
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
/*****************************utils********************************/
//LoadResActivity 中被调用
public void installFinish(Context context){
SharedPreferences sp = context.getSharedPreferences(
getPackageInfo(context).versionName, MODE_MULTI_PROCESS);
sp.edit().putString(KEY_DEX2_SHA1,get2thDexSHA1(context)).commit();
}
public static PackageInfo getPackageInfo(Context context){
PackageManager pm = context.getPackageManager();
try {
return pm.getPackageInfo(context.getPackageName(), 0);
} catch (PackageManager.NameNotFoundException e) {
LogCore.i(TAG, e.getLocalizedMessage());
}
return new PackageInfo();
}
public static String getCurProcessName(Context context) {
try {
int pid = android.os.Process.myPid();
ActivityManager mActivityManager = (ActivityManager) context
.getSystemService(Context. ACTIVITY_SERVICE);
for (ActivityManager.RunningAppProcessInfo appProcess : mActivityManager
.getRunningAppProcesses()) {
if (appProcess.pid == pid) {
return appProcess. processName;
}
}
} catch (Exception e) {
// ignore
}
return null ;
}
}
这里使用了原生MultiDex方案的classes(N).dex的方式保存了后面的dex而不是像微信目前的做法放到assest文件夹。前面有说到ART模式会将多个dex优化合并成oat文件。如果放置在asset里面就没有这个好处了
public class LoadResActivity extends Activity {
@Override
public void onCreate(Bundle savedInstanceState) {
requestWindowFeature(Window.FEATURE_NO_TITLE);
super .onCreate(savedInstanceState);
getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN , WindowManager.LayoutParams.FLAG_FULLSCREEN );
overridePendingTransition(R.anim.null_anim, R.anim.null_anim);
setContentView(R.layout.activity_verify);
new LoadDexTask().execute();
}
class LoadDexTask extends AsyncTask {
@Override
protected Object doInBackground(Object[] params) {
try {
MultiDex.install(getApplication());
LogUtils.d("loadDex" , "install finish" );
((App) getApplication()).installFinish(getApplication());
} catch (Exception e) {
LogUtils.e("loadDex" , e.getLocalizedMessage());
}
return null;
}
@Override
protected void onPostExecute(Object o) {
LogUtils.d( "loadDex", "get install finish");
finish();
System.exit( 0);
}
}
@Override
public void onBackPressed() {
//cannot backpress
}
name= "com.zongwu.LoadResActivity"
android:launchMode= "singleTask"
android:process= ":mini"
android:alwaysRetainTaskState= "false"
android:excludeFromRecents= "true"
android:screenOrientation= "portrait" />
name= "com.zongwu.WelcomeActivity"
android:launchMode= "singleTop"
android:screenOrientation= "portrait">
name="android.intent.action.MAIN"/>
name="android.intent.category.LAUNCHER"/>
-<set xmlns:android="http://schemas.android.com/apk/res/android">
<alpha
android:fromAlpha="1.0"
android:toAlpha="1.0"
android:duration="550"/>
set>
if (isLoadDexProcess()) {
return;
}
//app/build.gradle
android {
//...
defaultConfig {
//...
//定义main dex中必须保留的类
multiDexKeepProguard file('mainDexClasses.pro')
}
}
//app/mainDexClasses.pro
-keep class android.content.Intent { *; }
-keep interface android.content.SharedPreferences { *; }
//...自己看引用
这种方式好处在于依赖集非常简单,同时它的集成方式也是非常简单,我们无须去修改与加载无关的代码
但是:使用“使用MODE_MULTI_PROCESS标记使得SharedPreference得以进程间共享,主进程轮询sp文件”来判断是否dexopt完成的手段,在6.0上因为google废除了该标记导致行为准确性的难以保证
使用“使用MODE_MULTI_PROCESS标记使得SharedPreference得以进程间共享,主进程轮询sp文件”来判断是否dexopt完成的手段,在6.0上因为google废除了该标记导致行为准确性的难以保证
针对该环节进行优化,通过 跨进程通讯的Messenger来实现
public abstract class BaseApplication extends Application {
@SuppressWarnings("MismatchedReadAndWriteOfArray")
private static final byte[] lock = new byte[0];
@Override
protected void attachBaseContext(Context base) {
super.attachBaseContext(base);
//是<5.0的系统 && 是主进程(不是loaddex进程) 则进入异步加载方案
if (!isLoadDexProcess() && Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
//判断dexopt未执行过
if (needWait(base)) {
DexInstallDeamonThread thread = new DexInstallDeamonThread(this, base);
thread.start();
//阻塞等待:async_launch完成加载
synchronized (lock) {
try {
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
thread.exit();
Log.d("BaseApplication", "dexopt finished. alloc MultiDex.install()");
} else {
//主进程加载dex,
//此时odex已经产生(dexopt操作已经在 loaddexActivity中执行过了,或者不是第一次打开应用),所以不会有耗时问题
MultiDex.install(this);
}
}else{
//>=5.0的系统默认对dex进行oat优化,不需要MultiDex.install (this );
return;
}
}
/*****************************判断是否是 loaddex进程********************************/
public boolean isLoadDexProcess() {
String processName = getCurProcessName(this);
return processName != null && processName.contains(":mini");
}
/****************************判断dexopt是否已经执行过********************************/
/**
* 另一种手段判断是否dexopt,将其换转为 判断是否是首次启动
* 这个标记应当随着版本升级而重置
* @param context
* @return
*/
public final static String IS_FIRST_LAUNCH = "";
@SuppressWarnings("deprecation")
private boolean needWait(Context context) {
//这里实现不唯一,读取一个全局的标记,判断是否初次启动APP
SharedPreferences sp = context.getSharedPreferences(
getPackageInfo(context).versionName, MODE_MULTI_PROCESS);
return sp.getBoolean(IS_FIRST_LAUNCH, true);
}
/*****************************阻塞等待********************************/
/**
* 基于Messenger的跨进程通讯方式
* 1. 启动 异步dexopt 的跨进程Activity
* 2. 阻塞当前主进程
* 3. 锁机制等待dexopt是否完成
*/
private static class DexInstallDeamonThread extends Thread {
private Handler handler;
private Context application;
private Context base;
private Looper looper;
public DexInstallDeamonThread(Context application, Context base) {
this.application = application;
this.base = base;
}
@SuppressLint("HandlerLeak")
@Override
public void run() {
Looper.prepare();
looper = Looper.myLooper();
handler = new Handler() {
@SuppressWarnings("deprecation")
@Override
public void handleMessage(Message msg) {
synchronized (lock) {
lock.notify();
}
SPUtils
.getVersionSharedPreferences(application)
.edit()
.putBoolean(IS_FIRST_LAUNCH, false)
.apply();
}
};
Messenger messenger = new Messenger(handler);
Intent intent = new Intent(base, LoadResActivity.class);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
intent.putExtra("MESSENGER", messenger);
base.startActivity(intent);
Looper.loop();
}
public void exit() {
if (looper != null) looper.quit();
}
}
/*****************************utils********************************/
public static String getCurProcessName(Context context) {
try {
int pid = android.os.Process.myPid();
ActivityManager mActivityManager = (ActivityManager) context
.getSystemService(Context. ACTIVITY_SERVICE);
for (ActivityManager.RunningAppProcessInfo appProcess : mActivityManager
.getRunningAppProcesses()) {
if (appProcess.pid == pid) {
return appProcess. processName;
}
}
} catch (Exception e) {
// ignore
}
return null ;
}
public static PackageInfo getPackageInfo(Context context){
PackageManager pm = context.getPackageManager();
try {
return pm.getPackageInfo(context.getPackageName(), 0);
} catch (PackageManager.NameNotFoundException e) {
Log.i("getPackaigeInfo", e.getLocalizedMessage());
}
return new PackageInfo();
}
}
public class LoadResActivity extends AppCompatActivity {
private Messenger messenger;
@Override
public void onCreate(Bundle savedInstanceState) {
requestWindowFeature(Window.FEATURE_NO_TITLE);
super.onCreate(savedInstanceState);
getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN);
overridePendingTransition(R.anim.null_anim, R.anim.null_anim);
setContentView(R.layout.activity_load_res);
Log.d("LoadResActivity", "start install");
Intent from = getIntent();
messenger = from.getParcelableExtra("MESSENGER");
LoadDexTask dexTask = new LoadDexTask();
dexTask.execute();
}
class LoadDexTask extends AsyncTask {
@Override
protected Void doInBackground(Void... params) {
try {
MultiDex.install(getApplication());
Log.d("LoadResActivity", "finish install");
messenger.send(new Message());
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
@Override
protected void onPostExecute(Void o) {
finish();
System.exit(0);
}
}
@Override
public void onBackPressed() {
//无法退出
}
}
"com.synaric.dex.LoadResActivity"
android:launchMode= "singleTask"
android:alwaysRetainTaskState= "false"
android:excludeFromRecents= "true"
android:screenOrientation= "portrait"
android:process=":async_launch"/>
//app/build.gradle
android {
//...
defaultConfig {
//...
//定义main dex中必须保留的类
multiDexKeepProguard file('mainDexClasses.pro')
}
}
//app/mainDexClasses.pro
-keep public class * extends java.lang.Thread { *; }
-keep interface android.content.SharedPreferences { *; }
-keep class android.os.Handler { *; }
-keep class com.synaric.common.BaseSPKey { *; }
-keep class android.os.Messenger { *; }
-keep class android.content.Intent { *; }
//...自己看引用
如果你使用multidex,你需要意识到它对app启动性能有影响。我们通过跟踪app的启动时间发现了这个问题-用户点击app图标到所有图片都下载完并显示给用户的这段时间(假设说你的首屏需要下载图片并显示)。一旦multidex 启用,在所有运行Kitkat (4.4) 及以下的设备上我们的app启动时间就会大约增加15%。更多信息参考 Carlos Sessa的Lazy Loading Dex files 。
这是因为Android 5.0 以及更高版本使用了一个叫做ART的运行时,它天生就支持从应用的apk文件中加载multiple dex文件
之所以会出现启动耗时,不可否认是由于MultiDex.install引发的dexopt过程导致,但是dexopt的过程中,界面也会做一些处理,因此如果启动界面所涉及到的所有Class如果被放置到Main dex可以在一定程度上加快Ui显示过程。
现在的问题是,我们如何才能知道在app启动期间什么样的calss被加载了呢?
幸运的是,在 ClassLoader中我们有 findLoadedClass 方法。我们的办法就是在app启动结束的时候做一次运行时检查。如果第二个dex 文件中存有任何在app启动期间加载的class,那么就通过添加calss name 到multidex.keep文件中的方式来把它们移到main dex文件中
处理方式与《问题2:运行失败 NoClassDefFoundError》类似,在这里给出另外一种获得List的方法
在你认为app启动结束的地方运行下面util类中的getLoadedExternalDexClasses
把上面这个方法返回的列表添加到你的 multidex.keep 文件然后重新编译
把上面这个方法返回的列表添加到你的 multidex.keep 文件然后重新编译。
public class MultiDexUtils {
private static final String EXTRACTED_NAME_EXT = ".classes";
private static final String EXTRACTED_SUFFIX = ".zip";
private static final String SECONDARY_FOLDER_NAME = "code_cache" + File.separator +
"secondary-dexes";
private static final String PREFS_FILE = "multidex.version";
private static final String KEY_DEX_NUMBER = "dex.number";
private SharedPreferences getMultiDexPreferences(Context context) {
return context.getSharedPreferences(PREFS_FILE,
Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB
? Context.MODE_PRIVATE
: Context.MODE_PRIVATE | Context.MODE_MULTI_PROCESS);
}
/**
* get all the dex path
*
* @param context the application context
* @return all the dex path
* @throws PackageManager.NameNotFoundException
* @throws IOException
*/
public List getSourcePaths(Context context) throws PackageManager.NameNotFoundException, IOException {
final ApplicationInfo applicationInfo = context.getPackageManager().getApplicationInfo(context.getPackageName(), 0);
final File sourceApk = new File(applicationInfo.sourceDir);
final File dexDir = new File(applicationInfo.dataDir, SECONDARY_FOLDER_NAME);
final List sourcePaths = new ArrayList<>();
sourcePaths.add(applicationInfo.sourceDir); //add the default apk path
//the prefix of extracted file, ie: test.classes
final String extractedFilePrefix = sourceApk.getName() + EXTRACTED_NAME_EXT;
//the total dex numbers
final int totalDexNumber = getMultiDexPreferences(context).getInt(KEY_DEX_NUMBER, 1);
for (int secondaryNumber = 2; secondaryNumber <= totalDexNumber; secondaryNumber++) {
//for each dex file, ie: test.classes2.zip, test.classes3.zip...
final String fileName = extractedFilePrefix + secondaryNumber + EXTRACTED_SUFFIX;
final File extractedFile = new File(dexDir, fileName);
if (extractedFile.isFile()) {
sourcePaths.add(extractedFile.getAbsolutePath());
//we ignore the verify zip part
} else {
throw new IOException("Missing extracted secondary dex file '" +
extractedFile.getPath() + "'");
}
}
return sourcePaths;
}
/**
* get all the external classes name in "classes2.dex", "classes3.dex" ....
*
* @param context the application context
* @return all the classes name in the external dex
* @throws PackageManager.NameNotFoundException
* @throws IOException
*/
public List getExternalDexClasses(Context context) throws PackageManager.NameNotFoundException, IOException {
final List paths = getSourcePaths(context);
if(paths.size() <= 1) {
// no external dex
return null;
}
// the first element is the main dex, remove it.
paths.remove(0);
final List classNames = new ArrayList<>();
for (String path : paths) {
try {
DexFile dexfile = null;
if (path.endsWith(EXTRACTED_SUFFIX)) {
//NOT use new DexFile(path), because it will throw "permission error in /data/dalvik-cache"
dexfile = DexFile.loadDex(path, path + ".tmp", 0);
} else {
dexfile = new DexFile(path);
}
final Enumeration dexEntries = dexfile.entries();
while (dexEntries.hasMoreElements()) {
classNames.add(dexEntries.nextElement());
}
} catch (IOException e) {
throw new IOException("Error at loading dex file '" +
path + "'");
}
}
return classNames;
}
/**
* Get all loaded external classes name in "classes2.dex", "classes3.dex" ....
* @param context
* @return get all loaded external classes
*/
public List getLoadedExternalDexClasses(Context context) {
try {
final List externalDexClasses = getExternalDexClasses(context);
if (externalDexClasses != null && !externalDexClasses.isEmpty()) {
final ArrayList classList = new ArrayList<>();
final java.lang.reflect.Method m = ClassLoader.class.getDeclaredMethod("findLoadedClass", new Class[]{String.class});
m.setAccessible(true);
final ClassLoader cl = context.getClassLoader();
for (String clazz : externalDexClasses) {
if (m.invoke(cl, clazz) != null) {
classList.add(clazz.replaceAll("\\.", "/").replaceAll("$", ".class"));
}
}
return classList;
}
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
}
当运行时如果看到如下错误:
UNEXPECTED TOP-LEVEL ERROR:
java.lang.OutOfMemoryError: Java heap space
在dexOptions中有一个字段用来增加java堆内存大小:
android {
// ...
dexOptions {
javaMaxHeapSize "2g"
}
}
你可能会见到如下的错误:
Error:Execution failed for task ':app:dexDebug'.
> com.android.ide.common.internal.LoggedErrorException: Failed to run command:
$ANDROID_SDK/build-tools/android-4.4W/dx --dex --num-threads=4 --multi-dex
...
Error Code:
2
Output:
UNEXPECTED TOP-LEVEL EXCEPTION:
com.android.dex.DexException: Library dex files are not supported in multi-dex mode
at com.android.dx.command.dexer.Main.runMultiDex(Main.java:322)
at com.android.dx.command.dexer.Main.run(Main.java:228)
at com.android.dx.command.dexer.Main.main(Main.java:199)
at com.android.dx.command.Main.main(Main.java:103)
对于dex 的–multi-dex 选项设置与预编译的library工程有冲突,因此如果你的应用中包含引用的lirary工程,需要将预编译设置为false:
android {
// ...
dexOptions {
preDexLibraries = false
}
}
随着业务越来越庞大,早在两年前,项目已经遭遇了方法是超过65535的问题。 当时的解决方法是:采用google multidex方案解决;(那时项目还小,还未遭遇黑屏,启动速度optdex时间过长的问题), 一年后,黑屏,启动速度,ANR问题趋于明显;
然而好景不长,随着近两个版本大量SDK的接入,在接入multidex的情况下,成功的将主dex再次撑爆:编译时出现too manyclasses in –main-dex-list:
UNEXPECTED TOP-LEVEL EXCEPTION:com.android.dex.DexException: Too many classes in –main-dex-list,
main dex capacity exceeded at com.android.dx.command.dexer.Main.processAllFiles(Main.java:494)
at com.android.dx.command.dexer.Main.runMultiDex(Main.java:332)
at com.android.dx.command.dexer.Main.run(Main.java:243)
at com.android.dx.command.dexer.Main.main(Main.java:214)
at com.android.dx.command.Main.main(Main.java:106)
通过 sdk 的 mainDexClasses.rules
知道主 dex 里面会有 Application、Activity、Service、Receiver、Provider、Instrumentation、BackupAgent 和 Annotation。当这些类以及直接引用类比较多的时候,都要塞进主 dex
官方multidex分包方案,已经把原 Dex 分为 1 主 Dex 加多从 Dex。主 Dex 包含所有 4 大组件,Application,Annotation,multidex 等及其必要的直接依赖。too manyclasses in –main-dex-list 这个报错是因为主dex方法数超过65535的情况下,在编译时进行的报错
为了解决这个问题,当执行 Create{flavor}{buildType}ManifestKeepList task 之前将其中的 activity 去掉,之后会发现 /build/intermediates/multi_dex/{flavor}/{buildType}/manifest_keep.txt 文件中已经没有 Activity 相关的类了。
def patchKeepSpecs() {
def taskClass = "com.android.build.gradle.internal.tasks.multidex.CreateManifestKeepList";
def clazz = this.class.classLoader.loadClass(taskClass)
def keepSpecsField = clazz.getDeclaredField("KEEP_SPECS")
keepSpecsField.setAccessible(true)
def keepSpecsMap = (Map) keepSpecsField.get(null)
if (keepSpecsMap.remove("activity") != null) {
println "KEEP_SPECS patched: removed 'activity' root"
} else {
println "Failed to patch KEEP_SPECS: no 'activity' root found"
}
}
patchKeepSpecs()
详细可以看 CreateManifestKeepList 的源码:Github – CreateManifestKeepList
afterEvaluate {
project.tasks.each { task ->
if (task.name.startsWith('collect') && task.name.endsWith('MultiDexComponents')) {
println "main-dex-filter: found task $task.name"
task.filter { name, attrs ->
def componentName = attrs.get('android:name')
if ('activity'.equals(name)) {
println "main-dex-filter: skipping, detected activity [$componentName]"
return false
} else {
println "main-dex-filter: keeping, detected $name [$componentName]"
return true
}
}
}
}
}
这一步对应 gradle 执行过程中的 CreateManifestKeepList,利用其提供的 filter,进行一些过滤操作,其中 name参数表示为节点类型,例如 activity、service、receiver 等; attrs参数表示相应的节点信息,它是一个 Map 类型的参数,可表示的值形如 [‘android:name’:’com.example.ActivityClass’]
这一步可对 mainDex 中的组件信息做一些过滤,而不是添加所有的组件信息。像上述代码的处理就很残暴,把所有的 activity 都过滤掉。
PS: 需要注意的是,在源码中的 setFilter 已经被标为废弃,可能会在后续的版本被替换掉,所以用这种方案需要所使用的 gradle plugin 版本注意一二
参看 《4.2.4 完全控制主 DEX 文件的类》
afterEvaluate {
tasks.matching {
it.name.startsWith('dex')
}.each { dx ->
if (dx.additionalParameters == null) {
dx.additionalParameters = []
}
dx.additionalParameters += '--set-max-idx-number=48000'
}
}
//分包2(高版本的gradle)
dexOptions {
javaMaxHeapSize "4g"
preDexLibraries = false
additionalParameters = ['--multi-dex', '--main-dex-list='+ project.rootDir.absolutePath + '/app/maindexlist.txt', '--minimal-main-dex',
'--set-max-idx-number=1000']
}
参看 《4.2.4 完全控制主 DEX 文件的类》
百转千回的 too many classes in –main-dex-list
一个简单的将指定使用通配符包名分包到第二个dex中gradle插件。
同时支持 android gradle plugin 2.2.0 multidex. 可以解决 android studio 使用 multidex,但还会出现dex类太多的问题
项目地址:ceabie/DexKnifePlugin
项目地址:TangXiaoLv/Android-Easy-MultiDex
Dexopt
其实你不知道MultiDex到底有多坑
Android MultiDex初次启动APP优化