Android MultiDex实践:如何绕过那些坑?

2018-12-13 22:48:48

划重点 抓自  Android MultiDex实践:如何绕过那些坑?

我的报错是找不到Application类,但这个问题是出现在4.4.4中,并且360打包后就只有一个MultiDex也不会出现这问题了,还是了解下吧。以后会用的找。




前言

Android应用65k方法数的限制一直为广大开发者所诟病,在应用功能越来越丰富、各种开源库越来越多的今天,65k方法数瓶颈俨然已是一大绊脚石。


关于65k方法数限制的更多细节可以看下冯建的这篇文章:

http://www.jayfeng.com/2016/03/10/由Android-65K方法数限制引发的思考/

至于怎么解决这个问题,业内有包括插件化在内的一些方案,我们今天的重点是Android官方给出的这个方案,MultiDex。

MultiDex, 顾名思义,是指多dex实现,大多数App,解压其apk后,一般只有一个classes.dex文件,采用MultiDex的App解压后可以看到有classes.dex,classes2.dex,… classes(N).dex,这样每个dex都可以最大承载65k个方法,很大限度地缓解了单dex方法数限制。

下文将详细介绍我在应用MultiDex方案,以及尝试解决由此带来的问题的实践过程。

MultiDex初步探索

先试试官方方案?

Android官方MultiDex方案使用比较简单:

http://developer.android.com/intl/zh-cn/tools/building/multidex.html


1. 在gradle中添加MultiDex支持

multiDexEnable true

2. 加载classes2.dex

AndroidManifest.xml的application中添加MultiDexApplication,或者如果已经重载了Application,则在attachBaseContext()中执行MultiDex.install()即可。

官方方案简单易用,但遗留问题也不少,江湖传言坑很多。

果然有坑!

最初对我们的App进行MultiDex实现时,App还只是超过65535大概上百个方法,就按Android官方方案对App进行多dex支持,启动发现无任何异常(包括启动速度、启动时应用不响应ANR、Crash等),classes2.dex也只有大概几十KB大小,暗自窃喜了一小阵子!


好景不长,随着一些SDK的引入,导致classes2.dex文件达到了200+KB,测试发现不少机型启动ANR、Crash,或者启动时间过长,回去看了下Android官方指出的MultiDex存在的问题,嗯,发现基本都是描述里提到的那些坑(启动时间过长、找不到类)!

第一个坑:启动时间过长

在解决这些坑之前,先来简要看看App启动流程:





不难发现,Application.attachBaseContext是我们能控制的最早执行的代码,在这个方法里面执行MultiDex.install()无疑是最佳时机。


还有一点我们需要了解,首次启动时Dalvik虚拟机会对classes.dex执行dexopt操作,生成ODEX文件,这个过程非常耗时,而执行MultiDex.install()必然会再次对classes2.dex执行dexopt等操作,所有这些操作必须在5秒内完成,否则就ANR给你看!


非首次启动则直接从cache中读取已经执行过dexopt的ODEX文件,这个过程对启动并无太大影响。


我们实际测试中发现首次启动classes2.dex加载需要1~2秒,非首次启动classes2.dex加载只需要几十毫秒左右。


这可能就是为什么classes2.dex不能太大的一个原因。


基于此,对attachBaseContext稍作改动:

  1. @Override
    protected void attachBaseContext(final Context base)
    {
       super.attachBaseContext(base);
       initBeforeDex2Installed();
       
       if (isFirstLaunch()) {
           // 首次启动
           new Thread(new Runnable() {
           
               @Override
               public void run() {
                   MultiDex.install(base);
                   initAfterDex2Installed();
               }
           }).start();
       } else {
           // 非首次启动
           MultiDex.install(base);

  2.        initAfterDex2Installed();
       }
    }

以上逻辑便是改动之后的初步实现。

首次启动开启一个线程来加载classes2.dex,防止阻塞UI线程,非首次启动则同步执行。

initAfterDex2Installed()方法是根据Classes2.dex中结果,将涉及到的相关初始化工作移到classes2.dex加载完之后执行,避免启动问题。

建议在classes2.dex加载完成前,设置一个启动等待界面,之后再进入主界面,确保用户体验。

第二个坑:ANR/Crash

ANR!!Crash!!对,这就是MultiDex引起的另外一个坑!解决完启动时间过长的问题,测试不时就来找我:“这个机型ANR了”,“这个机型又ANR了”,“这个机型怎么又Crash了”,“这个手机启动不了”,简直令人抓狂!云测试通过率也没之前的高!

实际上所有这些都是同一个问题导致的:classes2.dex没加载完成之前,程序调用了classes2.dex中的类或者方法!adb logcat看下,基本也就是3类问题:

  • NoClassDefFoundError

  • Could not find class

  • Could not find method

个人建议在用MultiDex时,多次启动看logcat,重点关注以上3类信息!知道了是哪些类引起的错误之后,只需将这些类强制分到classes.dex中即可。

那么具体如何实现呢?还得先简单了解下MultiDex编译过程。

MultiDex编译过程

要想完全了解MultiDex编译过程,需要对gradle, groovy有些了解,限于篇幅这里不对它们作过多介绍,只介绍MultiDex编译过程中关键的几个gradle task。

task,顾名思义就是任务的意思,是gradle build的基本单位,一个project所有的build最终是由一个个task来完成,以下面一段简单的build日志为例,相信在build时有留意日志的同学不会陌生:

MyProject:generateDebugSources
MyProject:processDebugJavaRes
MyProject:compileDebugNdk UP-TO-DATE
MyProject:compileDebugSources
MyProject:collectDebugMultiDexComponents

日志中,generateDebugSources、processDebugJavaRes…都是build过程中依次执行的task任务,将上面的Debug替换为Release即为Release build时的task,这个好理解,下面主要介绍Debug的task。

这些task分别完成不同的功能,最终完成整个build,其中与MultiDex编译过程相关的task主要有3个:

1. collectDebugMultiDexComponents

先收集,这个task扫描AndroidManifest.xml中的application、activity、receiver、provider、service等相关类,并将这些类的信息写入到manifest_keep.txt文件中,该文件位于build/intermediates/multi-dex/debug目录下。

2. shrinkDebugMultiDexComponents

再压缩,这个task会根据proguard规则以及manifest_keep.txt文件来进一步优化manifest_keep.txt,将其中没有用到的类删除,最终生成componentClasses.jar文件,该文件同样位于build/intermediates/multi-dex/debug目录下。

3. createDebugMainDexClassList

最后创建,这个task会根据上步中生成的componentClasses.jar文件中的类,递归扫描这些类所有相关的依赖类,最终形成maindexlist.txt文件,该文件也位于build/intermediates/multi-dex/debug目录下,这个文件中的类最终会打包进classes.dex中。

需要注意的是,maindexlist.txt文件并没有完全列出有所的依赖类,如果发现要查找的那个class不在maindexlist中,也无需奇怪。如果一定要确保某个类分到主dex中,将该类的完整路径加入到maindexlist中即可,同时注意两点:

  • 如果加入的类并不在project中,则gradle构建会忽略这个类,

  • 如果加入了多个相同的类,则只取其中一个。

以上3个task在build日志中都能找到。

回到第二个坑:ANR/Crash如何解决?

回到前面的问题:如何将某个类强制打包到classes.dex中,以避免ANR/Crash?

上面的3个task已经给出了答案!对,只需将该类完整路径添加到maindexlist.txt中即可!

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

这里将需要强制分到classes.dex中的类放在keepin_maindexlist_debug.txt,这种实现方式基本能够解决眼前问题,但现在看来还是too simple sometimes navie!

主要问题是不可控,任何一次对代码的改动都有可能导致不同的分包结果,这就可能隐藏着不同的类导致首次启动失败,大量测试结果也证明了这种方法的不可控性。作为开发,代码不可控无疑无法忍受,如何改进这种方法使得MultiDex可控呢?与同事交流后间接找到了一种改进方案,下面讲讲这个方案。

MultiDex的一种改进实现

该如何让MultiDex分包可控呢?我的做法是:找出启动过程中所有类及依赖类,强制放入classes.dex中!

这么做要求启动相关的类不能太多(实际上大部分App从启动Application到进入MainActivity也就几个相关类),同时尽量让主界面和二级界面充分解耦。

如果不想对现有代码做太多改动,可以用反射方式调用二级界面中的Activity(反射可以避免依赖),不过调用时得要先判断classes2.dex是否加载完,以防某些二级界面相关代码在classes2.dex中而引起Crash,这么做虽然对功能实现并无影响,但可能导致代码可维护性降低。

另外,我们可以控制哪些类在classes.dex中,但无法控制哪些类分到classes2.dex中,以反射方式调用二级界面activity可以增大二级界面相关类分到classes2.dex中的概率。

寻找启动类

如何找出App启动到主界面显示这个过程中的所有类?

网上能够找得到的方法比较少,美团有自己的脚本程序找启动依赖类,但人家没开!源!!啦!!!还好Google到了CDA(Class Dependency Analyzer),通过这个工具,基本能找到启动过程中所有Activity、Application等相关依赖类,通常会有一定偏差(会将某些系统方法也找出来了)。

这时还需结合App的所有类来作进一步优化(获取App所有类只需反编译dex文件形成jar,解压jar包,再用shell相关工具处理即可得到),取两者的交集基本就能找出所有启动依赖类了。这里有一点需注意:必须以debug版本的App来分析,下面会讲到为什么。

Release版本寻找启动类

为什么要将Release版本单独拿出来说呢?

对,就是因为混淆!

混淆可能会导致每次编译形成的class文件名不同,代码的增加或减少也会对混淆结果产生影响,这可能导致每次编译所需的启动类名都不一样,而Debug版本往往不会做代码混淆,因此启动过程中的类名基本变化不大。

那么问题来了,如何确定Release版本启动依赖类呢?

build日志!!

通过build日志,我们发现,proguardRelease这个task在createReleaseMainDexClassList这个task之前执行,这意味着,在形成maindexlist之前,我们能够确切的知道哪些类进行了混淆以及混淆之后的类名!如何获知?proguard的产物给出了答案:

build/outputs/mapping/release/目录下的4个txt文件就是proguard的产物:

dump.txt:所有class文件的内部结构
mapping.txt:源码与混淆之后的类、方法、属性名字之间的一一映射关系
seeds.txt:未被混淆的类和属性
usage.txt:从Apk中剥离的代码

这里mapping.txt文件正是我们需要的,至于另外的3个文件有兴趣的可以研究下。我们简单了解下mapping.txt中文本的结构:

android.support.ActivityManagerCompat -> android.support.a:   48:52:int getLargeMemoryClass() -> a   62:83:boolean isHighEndGfx(android.content.Context) -> a

从上述信息中,我们知道经过代码混淆,android.support.ActivityManagerCompat在release版中最终打包为android.support.a类,并且对其中的方法、属性也进行了混淆。

并且注意到,文本中对类混淆的行以”:”结尾。

这下问题就有解了:


1. 根据startup_keep_list_debug.txt文件中的每一行,在mapping.txt中寻找其是否被混淆。

2. 如果被混淆了,则读取经过混淆的类。

3. 如果没有被混淆,则直接获取该类。

通过以上几个步骤,即可形成最终Release版本的启动依赖类。

至此,寻找启动类工作基本完成,但不难发现一个问题,那就是build release版本是将会更加耗时,因为要从mapping.txt中查找混淆类,涉及两层循环,mapping.txt文件通常有上万行,这也是这种方法最大的缺陷之一。

构建得到APK之后,点击icon,貌似一切正常work!

但,但,但,重要的事说三遍,至此并非所有事情都做完了,仍然可能会遗留一些问题!

通过以上方法找到的启动依赖类并非100%正确,几千上万个类中遗漏几个毕竟不是小概率事件,解决方法还是得多次启动,通过adb logcat获取启动日志,在日志中查找NoClassDefFoundError、Could not find class、Could not find method等warning。

有必要的话仍需将这些形成warning的类添加到startup_keep_list_debug.txt文件中,多次启动,直到没有相关的warning,这么做是为了减小未知风险。

至此,这种MultiDex实现方法基本也就完成了,后续会寻求其他更好的解决方案,比如动态加载dex方式等等。

MultiDex使用小结

以上基本就是我实现MultiDex的整个过程,中间有多少坑只有实现了才知道!个人认为无必要和绝对把握还是远离它比较好,特别是针对用户量大的App,任何线上ANR/Crash的影响范围可想而知。

  • 提高设计与代码质量应该可以不必被方法数限制困扰,据我观察微信目前最新的v6.3版本就没有超出这个限制,避免不必要的功能与代码非常重要。

  • 多试验首次启动App,以观察启动log是必须的,除了测试MultiDex是否会对首次启动时间产生明显影响,最重要的还是查看启动过程中是否有找不到的类。

  • 通常多次云测也是必须的,毕竟测试能覆盖到的机型有限,云测也节省了测试工作量。

我目前的方案,不尽完美但却能够解决当下问题,也仍然在寻求最优的解决方案.






产生65535问题的原因


单个Dex文件中,method个数采用使用原生类型short来索引,即4个字节最多65536个method,field、class的个数也均有此限制,关于如何解决由于引用过多基础依赖项目,造成field超过65535问题,请参考@寒江不钓的这篇文章『当Field邂逅65535』

对于Dex文件,则是将工程所需全部class文件合并且压缩到一个DEX文件期间,也就是使用Dex工具将class文件转化为Dex文件的过程中, 单个Dex文件可被引用的方法总数(自己开发的代码以及所引用的Android框架、类库的代码)被限制为65536。

这就是65535问题的根本来源。

LinearAlloc问题的原因

这个问题多发生在2.x版本的设备上,安装时会提示INSTALL_FAILED_DEXOPT,这个问题发生在安装期间,在使用Dalvik虚拟机的设备上安装APK时,会通过DexOpt工具将Dex文件优化为ODex文件,即Optimised Dex,这样可以提高执行效率。

在Android版本不同分别经历了4M/5M/8M/16M限制,目前主流4.2.x系统上可能都已到16M, 在Gingerbread或以下系统LinearAllocHdr分配空间只有5M大小的, 高于Gingerbread的系统提升到了8M。Dalvik linearAlloc是一个固定大小的缓冲区。dexopt使用LinearAlloc来存储应用的方法信息。Android 2.2和2.3的缓冲区只有5MB,Android 4.x提高到了8MB或16MB。当应用的方法信息过多导致超出缓冲区大小时,会造成dexopt崩溃,造成INSTALL_FAILED_DEXOPT错误。

Google提出的MultiDex方案

当App不断迭代的时候,总有一天会遇到这个问题,为此Google也给出了解决方案,具体的操作步骤我就不多说了,无非就是配置Application和Gradle文件,下面我们简单看一下这个方案的实现原理。

MultiDex实现原理

实际起作用的是下面这个jar包

~/sdk/extras/android/support/multidex/library/libs/android-support-multidex.jar

不管是继承自MultiDexApplication还是重写attachBaseContext(),实际都是调用下面的方法

public class MultiDexApplication extends Application {protected void attachBaseContext(final Context base) {super.attachBaseContext(base);MultiDex.install((Context)this);}}

下面重点看下MutiDex.install(Context)的实现,代码很容易理解,重点的地方都有注释

static {    //第二个Dex文件的文件夹名,实际地址是/date/date/<package_name>/code_cache/secondary-dexes        SECONDARY_FOLDER_NAME = "code_cache" + File.separator + "secondary-dexes";        installedApk = new HashSet<String>();        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) {            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) {                  //如果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") + "\"");                }                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);                }                final File dexDir = new File(applicationInfo.dataDir, MultiDex.SECONDARY_FOLDER_NAME);                //将Dex文件加载为File对象                List<File> files = MultiDexExtractor.load(context, applicationInfo, dexDir, false);                //检测是否是zip文件                if (checkValidZipFiles(files)) {                    //正式安装其他Dex文件                    installSecondaryDexes(loader, dexDir, files);                }                else {                    Log.w("MultiDex", "Files were not valid zip files.  Forcing a reload.");                    files = MultiDexExtractor.load(context, applicationInfo, dexDir, true);                    if (!checkValidZipFiles(files)) {                        throw new RuntimeException("Zip files were not valid.");                    }                    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文件,具体的加载操作都在下面的方法中

installSecondaryDexes(loader, dexDir, files);

下面重点看下

private static void installSecondaryDexes(final ClassLoader loader, final File dexDir, final List<File> 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的版本,其他的类似

private static final class V14    {        private static void install(final ClassLoader loader, final List<File> 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<File>(additionalClassPathEntries), optimizedDirectory));        }         private static Object[] makeDexElements(final Object dexPathList, final ArrayList<File> files, final File optimizedDirectory) throws IllegalAccessException, InvocationTargetException, NoSuchMethodException {            final Method makeDexElements = findMethod(dexPathList, "makeDexElements", (Class<?>[])new Class[] { ArrayList.class, File.class });            return (Object[])makeDexElements.invoke(dexPathList, files, optimizedDirectory);        }    }

PathClassLoader.java

public class PathClassLoader extends BaseDexClassLoader {public PathClassLoader(String dexPath, ClassLoader parent) {super(dexPath, null, null, parent);}public PathClassLoader(String dexPath, String libraryPath,ClassLoader parent) {super(dexPath, null, libraryPath, parent);}}

BaseDexClassLoader的代码如下,实际上寻找class时,会调用findClass(),会在pathList中寻找,因此通过反射手动添加其他Dex文件中的class到pathList字段中,就可以实现类的动态加载,这也是MutiDex方案的基本原理。

public class BaseDexClassLoader extends ClassLoader {private final DexPathList pathList;public BaseDexClassLoader(String dexPath, File optimizedDirectory,String libraryPath, ClassLoader parent) {super(parent);this.pathList = new DexPathList(this, dexPath, libraryPath, optimizedDirectory);}@Overrideprotected 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;}}

缺点

通过查看MultiDex的源码,可以发现MultiDex在冷启动时,因为会同步的反射安装Dex文件,进行IO操作,容易导致ANR

  1. 在冷启动时因为需要安装Dex文件,如果Dex文件过大时,处理时间过长,很容易引发ANR

  2. 采用MultiDex方案的应用因为linearAlloc的BUG,可能不能在2.x设备上启动

美团的多Dex分包、动态异步加载方案

首先我们要明白,美团的这个动态异步加载方案,和插件化的动态加载方案要解决的问题不一样,我们这里讨论的只是单纯的为了解决65535问题,并且想办法解决Google的MutiDex方案的弊端。

多Dex分包

首先,采用Google的方案我们不需要关心Dex分包,开发工具会自动的分析依赖关系,把需要的class文件及其依赖class文件放在Main Dex中,因此如果产生了多个Dex文件,那么classes.dex内的方法数一般都接近65535这个极限,剩下的class才会被放到Other Dex中。如果我们可以减小Main Dex中的class数量,是可以加快冷启动速度的。

美团给出了Gradle的配置,但是由于没有具体的实现,所以这块还需要研究。

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

实现Dex自定义分包的关键是分析出class之间的依赖关系,并且干涉Dex文件的生成过程。

Dex也是一个工具,通过设置参数可以实现哪一些class文件在Main Dex中。

afterEvaluate {tasks.matching {it.name.startsWith('dex')}.each { dx ->if (dx.additionalParameters == null) {dx.additionalParameters = []}dx.additionalParameters += '--multi-dex'dx.additionalParameters += '--set-max-idx-number=30000'println("dx param = "+dx.additionalParameters)dx.additionalParameters += "--main-dex-list=$projectDir/multidex.keep".toString()}}
  • --multi-dex 代表采用多Dex分包

  • --set-max-idx-number=30000 代表每个Dex文件中的最大id数,默认是65535,通过修改这个值可以减少Main Dex文件的大小和个数。比如一个App混淆后方法数为48000,即使开启MultiDex,也不会产生多个Dex,如果设置为30000,则就产生两个Dex文件

  • --main-dex-list= 代表在Main Dex中的class文件

需要注意的是,上面我给出的gredle task,只在1.4以下管用,在1.4+版本的gradle中,app:dexXXX task 被隐藏了(更多信息请参考Gradle plugin的更新信息),jacoco, progard, multi-dex三个task被合并了。

The Dex task is not available through the variant API anymore….The goal of this API is to simplify injecting custom class manipulations without having to deal with tasks, and to offer more flexibility on what is manipulated. The internal code processing (jacoco, progard, multi-dex) have all moved to this new mechanism already in 1.5.0-beta1.

所以通过上面的方法无法对Dex过程进行劫持。这也是我现在还没有解决的问题,有解决方案的朋友可以指点一下!

异步加载方案

其实前面的操作都是为了这一步操作的,无论将Dex分成什么样,如果不能异步加载,就解决不了ANR和加载白屏的问题,所以异步加载是一个重点。

异步加载主要问题就是:如何避免在其他Dex文件未加载完成时,造成的ClassNotFoundException问题?

美团给出的解决方案是替换Instrumentation,但是博客中未给出具体实现,我对这个技术点进行了简单的实现,Demo在这里MultiDexAsyncLoad,对ActivityThread的反射用的是携程的解决方案。

首先继承自Instrumentation,因为这一块需要涉及到Activity的启动过程,所以对这个过程不了解的朋友请看我的这篇文章【凯子哥带你学Framework】Activity启动过程全解析

/** * Created by zhaokaiqiang on 15/12/18. */public class MeituanInstrumentation extends Instrumentation {private List<String> mByPassActivityClassNameList;public MeituanInstrumentation() {mByPassActivityClassNameList = new ArrayList<>();}@Overridepublic Activity newActivity(ClassLoader cl, String className, Intent intent) throws InstantiationException, IllegalAccessException, ClassNotFoundException {if (intent.getComponent() != null) {className = intent.getComponent().getClassName();}boolean shouldInterrupted = !MeituanApplication.isDexAvailable();if (mByPassActivityClassNameList.contains(className)) {shouldInterrupted = false;}if (shouldInterrupted) {className = WaitingActivity.class.getName();} else {mByPassActivityClassNameList.add(className);}return super.newActivity(cl, className, intent);}}

至于为什么重写了newActivity(),是因为在启动Activity的时候,会经过这个方法,所以我们在这里可以进行劫持,如果其他Dex文件还未异步加载完,就跳转到Main Dex中的一个等待Activity——WaitingActivity。

private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) {ActivityInfo aInfo = r.activityInfo;if (r.packageInfo == null) {r.packageInfo = getPackageInfo(aInfo.applicationInfo, r.compatInfo,Context.CONTEXT_INCLUDE_CODE);}Activity activity = null;try {java.lang.ClassLoader cl = r.packageInfo.getClassLoader();activity = mInstrumentation.newActivity(cl, component.getClassName(), r.intent);} catch (Exception e) {}}

在WaitingActivity中可以一直轮训,等待异步加载完成,然后跳转至目标Activity。

public class WaitingActivity extends BaseActivity {     private Timer timer;     @Override    protected void onCreate(Bundle savedInstanceState) {        super.onCreate(savedInstanceState);        setContentView(R.layout.activity_wait);        waitForDexAvailable();    }     private void waitForDexAvailable() {         final Intent intent = getIntent();        final String className = intent.getStringExtra(TAG_TARGET);         timer = new Timer();        timer.schedule(new TimerTask() {            @Override            public void run() {                while (!MeituanApplication.isDexAvailable()) {                    try {                        Thread.sleep(100);                    } catch (InterruptedException e) {                        e.printStackTrace();                    }                    Log.d("TAG", "waiting");                }                intent.setClassName(getPackageName(), className);                startActivity(intent);                finish();            }        }, 0);    }     @Override    protected void onDestroy() {        super.onDestroy();        if (timer != null) {            timer.cancel();        }    }}

异步加载Dex文件放在什么时候合适呢?

我放在了Application.onCreate()中

public class MeituanApplication extends Application {     private static final String TAG = "MeituanApplication";    private static boolean isDexAvailable = false;     @Override    public void onCreate() {        super.onCreate();        loadOtherDexFile();    }     private void loadOtherDexFile() {        new Thread(new Runnable() {            @Override            public void run() {                MultiDex.install(MeituanApplication.this);                isDexAvailable = true;            }        }).start();    }     public static boolean isDexAvailable() {        return isDexAvailable;    }}

那么替换系统默认的Instrumentation在什么时候呢?

当SplashActivity跳转到MainActivity之后,再进行替换比较合适,于是

public class MainActivity extends BaseActivity {@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);MeituanApplication.attachInstrumentation();}}

MeituanApplication.attachInstrumentation()实际就是通过反射替换默认的Instrumentation

public class MeituanApplication extends Application {public static void attachInstrumentation() {try {SysHacks.defineAndVerify();MeituanInstrumentation meiTuanInstrumentation = new MeituanInstrumentation();Object activityThread = AndroidHack.getActivityThread();Field mInstrumentation = activityThread.getClass().getDeclaredField("mInstrumentation");mInstrumentation.setAccessible(true);mInstrumentation.set(activityThread, meiTuanInstrumentation);} catch (Exception e) {e.printStackTrace();}}}

至此,异步加载Dex方案的一个基本思路就通了,剩下的就是完善和版本兼容了。


  • 2021-11-03 19:59:58

    Android 11 adb无线调试使用方法

    ​Android 11无线调试不需要再像以前一样,先插上usb线,输入命令来启用无线调试,再进行无线连接了。Android 11系统设置开发者选项中自带了无线调试,今天亲自测试了