Android之Multidex分包配置以及原理分析

引言

  • 如果App引用的库太多,方法数超过65536后无法编译。这是因为单个dex里面不能有超过65536个方法。为什么有最大的限制呢, 因为android会把每一个类的方法id检索起来,存在一个链表结构里面。但是这个链表的长度是用一个short类型来保存的, short占两个字节(保存-2的15次方到2的15次方-1,即-32768~32767),最大保存的数量就是65536。新版本的Android系统中修复了这个问题, 但是我们仍然需要对低版本的Android系统做兼容。

  • 解决方法有如下几个: 1.精简方法数量,删除没用到的类、方法、第三方库。 2.使用ProGuard去掉一些未使用的代码 3.复杂模块采用jni的方式实现,也可以对边缘模块采用本地插件化的方式。 4.分割Dex。本文介绍最后一种方法。

配置操作

dex文件拆成两个或多个,为此谷歌官方推出了multidex兼容包,配合AndroidStudio实现了一个APK包含多个dex的功能。 Android 的 Gradle插件在 Android Build Tool 21.1开始就支持使用multidex了。

  1. 修改Gradle的配置,支持multidex:
android {
    compileSdkVersion 22
    buildToolsVersion "25.0.0"
    defaultConfig {
        ...
        minSdkVersion 14
        targetSdkVersion 22
        ...
        // Enabling multidex support.
        multiDexEnabled true  //这里
    }
    ...
}
dependencies {
  compile 'com.android.support:multidex:1.0.1' // 还有这里
}

你可以在Gradle配置文件中的defaultConfig、 buildType、productFlavor中设置 multiDexEnabled true。

  1. 第二步分三种情况:
  • 如果没有重写Application,在manifest文件中,在application标签下添加MultidexApplication Class的引用,看配置:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.android.multidex.myapplication">
    <application
        ...
        android:name="android.support.multidex.MultiDexApplication">
        ...
    </application>
</manifest>
  • 如果重写了Application,可以让自定义Applicationd继承android.support.multidex.MultiDexApplication。
图.png

看看里MultiDexApplication里有什么:

MultiDexApplication.png
  • 如果重写了Application并且已经继承了其他Application类,可以重写attachBaseContext()方法,并添加语句MultiDex.install(this);看代码:
public class MyApplication extends BaseApplication{
    @Override
    protected void attachBaseContext(Context base) {
        super.attachBaseContext(base);
        MultiDex.install(this);
    }
}

没什么特别问题,到这里就可以了,你的分包就完成了。
注意事项:Application 中的静态全局变量会比MutiDex的 install()方法优先加载,所以建议避免在Application类中使用静态变量引用 main classes.dex文件以外dex文件中的类。

Multidex的局限

官方文档中提到了Multidex有局限性:

  1. 如果第二个(或其他个)dex文件很大的话,安装.dex文件到data分区时可能会导致ANR(应用程序无响应)。
  2. 由于Dalvik linearAlloc的bug的关系,使用了multidex的应用可能无法在Android 4.0 (API level 14)或之前版本的设备上运行。
  3. 由于Dalvik linearAlloc的限制,使用了multidex的应用会请求非常大的内存分配,从而导致程序奔溃。Dalvik linearAlloc是一个固定大小的缓冲区。 在应用的安装过程中,系统会运行一个名为dexopt的程序为该应用在当前机型中运行做准备。dexopt使用LinearAlloc来存储应用的方法信息。 Android 2.2和2.3的缓冲区只有5MB,Android 4.x提高到了8MB或16MB。当方法数量过多导致超出缓冲区大小时,会造成dexopt崩溃。
  4. 在Dalvik运行时中,某些类的方法必须要放在主dex中,Android构建工具可能无法确保所有有此要求的类被编译进主dex中。

下面的问题也非常值得我们关注:

  • 一些在二级Dex加载之前,可能会被调用到的类(比如静态变量的类),需要放在主Dex中.否则会ClassNotFoundError. 通过修改Gradle,可以显式的把一些类放在Main Dex中。
afterEvaluate {
    tasks.matching {
        it.name.startsWith('dex')
    }.each { dx ->
        if (dx.additionalParameters == null) {
            dx.additionalParameters = []
        }
        dx.additionalParameters += '--multi-dex'
        dx.additionalParameters += "--main-dex-list=$projectDir/<filename>".toString()
    }
}

注意上面是修改后的Gradle,其中是一个文本文件的文件名,存放在和这个build.gradle脚本同一级的文件目录下,而不是 项目根目录。可以把这个文本文件起名为multidex.keep,内容如下.实际就是把需要放在Main Dex的类罗列出来。

android/support/multidex/BuildConfig/class
android/support/multidex/MultiDex$V14/class
android/support/multidex/MultiDex$V19/class
android/support/multidex/MultiDex$V4/class
android/support/multidex/MultiDex/class
android/support/multidex/MultiDexApplication/class
android/support/multidex/MultiDexExtractor$1/class
android/support/multidex/MultiDexExtractor/class
android/support/multidex/ZipUtil$CentralDirectory/class
android/support/multidex/ZipUtil/class

project.afterEvaluate标签在特定的project配置完成后运行,而gradle.projectsEvaluated在所有projects配置完成后运行。 注意afterEvaluate需要放在android{}里,不可放外面。

  • 这样做了之后并不一定解压apk之后会出现多个dex文件,可能仍然只有一个dex。因为只有必须分包的时候才会分,如果不需要就不会。 如果要强制分dex,还需要加上dx.additionalParameters += ‘–minimal-main-dex’。完整的配置如下:
 afterEvaluate {
        tasks.matching {
            it.name.startsWith('dex')
        }.each { dx ->
            if (dx.additionalParameters == null) {
                dx.additionalParameters = []
            }
            dx.additionalParameters += '--multi-dex'

            // 设置multidex.keep文件中class为第一个dex文件中包含的class,如果没有下一项设置此项无作用
            dx.additionalParameters += "--main-dex-list=$projectDir/multidex.keep".toString()

            //此项添加后第一个classes.dex文件只能包含-main-dex-list列表中class  
            dx.additionalParameters += '--minimal-main-dex'
        }
    }

这样配置了之后就按照multidex.keep里面的内容拆分出了第一个dex文件。其他内容在第二个里面。 那么如何把需要的类放在multidex.keep文件里呢?其实不用手动一个类一个类写,我们进入这个文件: 项目\build\intermediates\multi-dex\release(或debug)\maindexlist.txt。 将maindexlist.txt中没有在application中初始化的类删除一部分之后,剩余的复制到multidex.keep文件中就可以了。 当然也可以自行增加没有被包含进去的类,因为不直接引用的类都不在maindexlist.txt中。 注意,如果需要混淆的话需要写混淆之后的 class 。

  • 如果需要配置每一个dex最大的方法数,可以如下配置:
afterEvaluate {
        tasks.matching {
            it.name.startsWith('dex')
        }.each { dx ->
            if (dx.additionalParameters == null) {
                dx.additionalParameters = []
            }
            dx.additionalParameters += '--set-max-idx-number=48000'
        }
    }

通过上面的number参数代表每个Dex文件中的最大id数,默认是65535,通过修改这个值可以减少Main Dex文件的大小和个数。 这样可以拆分出多个dex。但是这个number不可设置的太小,因为主dex需要加载足够app启动 需要的类,太小则无法加载完,直接报错。

  • 如果用使用其他Lib,要保证这些Lib没有被preDex,否则可能会抛出下面的异常:
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:337)
        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)

遇到这个异常,需要在Gradle中修改,让它不要对Lib做preDexing

android {
    //  ...
        dexOptions {
            preDexLibraries = false
        }
    }

MultiDex实现原理

下面从DEX自动拆包和动态加载两方面来分析。

  • Dex 拆分

dex拆分步骤分为:

  1. 自动扫描整个工程代码得到main-dex-list;
  2. 根据main-dex-list对整个工程编译后的所有class进行拆分,将主、从dex的class文件分开;
  3. 用dx工具对主、从dex的class文件分别打包成 .dex文件,并放在apk的合适目录。

怎么自动生成 main-dex-list? Android SDK 从 build tools 21 开始提供了 mainDexClasses 脚本来生成主 dex 的文件列表。查看这个脚本的源码,可以看到它主要做了下面两件事情:

1)调用 proguard 的 shrink 操作来生成一个临时 jar 包; 2)将生成的临时 jar 包和输入的文件集合作为参数,然后调用com.android.multidex.MainDexListBuilder 来生成主 dex 文件列表。

Proguard的官网执行步骤如下:

img158.jpg

在 shrink 这一步,proguard 会根据 keep 规则保留需要的类和类成员,并丢弃不需要的类和类成员。也就是说,上面 shrink 步骤生成的临时 jar 包里面保留了符合 keep 规则的类,这些类是需要放在主 dex 中的入口类。

但是仅有这些入口类放在主 dex 还不够,还要找出入口类引用的其他类,不然仍然会在启动时出现 NoClassDefFoundError。而找出这些引用类,就是调用的 com.android.multidex.MainDexListBuilder,它的部分核心代码如下:

img159.jpg

在调用 com.android.multidex.MainDexListBuilder 之后,符合 keep 规则的主 dex 文件列表就生成了。

  • Dex加载
    因为Android系统在启动应用时只加载了主dex(Classes.dex),其他的 dex 需要我们在应用启动后进行动态加载安装。 Google 官方方案是如何加载的呢,Google官方支持Multidex 的 jar 包是 android-support-multidex.jar,该 jar 包从 build tools 21.1 开始支持。这个 jar 加载 apk 中的从 dex 流程如下:
img160.jpg

此处主要的工作就是从 apk 中提取出所有的从 dex(classes2.dex,classes3.dex,…),然后通过反射依次安装加载从dex,并合并到放在BaseDexClassLoader的DexPathList的 Element数组。

BaseDexClassLoader findClass的过程如下:

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

下面代码说明了怎么通过DexFile来加载Secondary DEX并放到BaseDexClassLoader的DexPathList中:

    private static void install(ClassLoader loader, List<File> additionalClassPathEntries,
                                File optimizedDirectory)
            throws IllegalArgumentException, IllegalAccessException,
            NoSuchFieldException, InvocationTargetException, NoSuchMethodException {
        /* The patched class loader is expected to be a descendant of
         * dalvik.system.BaseDexClassLoader. We modify its
         * dalvik.system.DexPathList pathList field to append additional DEX
         * file entries.
         */
        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));
        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) {
        }
    }
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,456评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,370评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,337评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,583评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,596评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,572评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,936评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,595评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,850评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,601评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,685评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,371评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,951评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,934评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,167评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,636评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,411评论 2 342

推荐阅读更多精彩内容