Android分包原理

如果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

本文只介绍最后一种方法。

multidex方案配置

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

使用步骤

使用步骤包括: 1.修改Gradle的配置,支持multidex 2.修改你的manifest。让其支持multidexapplication类

注意其中第二步其实还有另外两种替代方法,下面介绍。

修改Gradle的配置,支持multidex:

[代码]xml代码:

?
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
android {
     compileSdkVersion 21
     buildToolsVersion "21.1.0"
     defaultConfig {
         ...
         minSdkVersion 14
         targetSdkVersion 21
         ...
         // Enabling multidex support.
         multiDexEnabled true
     }
     ...
}
dependencies {
   compile 'com.android.support:multidex:1.0.0'
}


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

在manifest文件中,在application标签下添加MultidexApplication Class的引用,如下所示:

[代码]xml代码:

?
1
2
3
4
5
6
7
<!--?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。

如果之前已经继承了其他Application类,可以重写attachBaseContext()方法,并添加语句MultiDex.install(this);如下:

[代码]java代码:

?
1
2
3
4
5
6
7
public class MyApplication extends BaseApplication{
     @Override
     protected void attachBaseContext(Context base) {
         super .attachBaseContext(base);
         MultiDex.install( this );
     }
}


注意事项:Application 中的静态全局变量会比MutiDex的 instal()方法优先加载,所以建议避免在Application类中使用静态变量引用 main classes.dex文件以外dex文件中的类,可以根据如下所示的方式进行修改:

[代码]java代码:

?
01
02
03
04
05
06
07
08
09
10
11
12
13
@Override 
     public void onCreate() { 
         super .onCreate(); 
         final Context mContext = this
         new Runnable() { 
             @Override 
             public void run() { 
                 // put your logic here! 
                 // use the mContext instead of this here 
            
         }.run(); 
    
    


Multidex的局限性

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

1.如果第二个(或其他个)dex文件很大的话,安装.dex文件到data分区时可能会导致ANR(应用程序无响应),此时应该使用ProGuard减小DEX文件的大小。 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中.

[代码]java代码:

?
01
02
03
04
05
06
07
08
09
10
11
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()
     }
}</filename>


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

[代码]java代码:

?
01
02
03
04
05
06
07
08
09
10
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’。完整的配置如下:

[代码]java代码:

?
01
02
03
04
05
06
07
08
09
10
11
12
13
14
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最大的方法数,可以如下配置:

[代码]java代码:

?
01
02
03
04
05
06
07
08
09
10
afterEvaluate {
     tasks.matching {
         it.name.startsWith( 'dex' )
     }.each { dx ->
         if (dx.additionalParameters == null ) {
             dx.additionalParameters = []
         }
         dx.additionalParameters += '--set-max-idx-number=20000'
     }
}


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

更需要注意的是这个afterEvaluate的配置只在gradle 1.4以下可以使用,对于1.4+版本的gradle如何配置目前尚不清楚。

如果用使用其他Lib,要保证这些Lib没有被preDex,否则可能会抛出下面的异常:

[代码]java代码:

?
1
2
3
4
5
6
7
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

[代码]java代码:

?
1
2
3
4
5
6
7
android {
//  ...
     dexOptions {
         preDexLibraries = false
     }
}


如果每次都打开MultiDex编译版本的话,会比平常用更多的时间. Android的官方文档也给了我们一个小小的建议,利用Gradle建立两个Flavor.一个minSdkVersion设置成21,这是用了ART支持的Dex格式, 避免了MultiDex的开销.而另外一个Flavor就是原本支持的最小sdkVersion.平时开发时候调试程序,就用前者的Flavor, 发布版本打包就用后者的Flavor.

[代码]java代码:

?
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
android {
     productFlavors {
         // Define separate dev and prod product flavors.
         dev {
             // dev utilizes minSDKVersion = 21 to allow the Android gradle plugin
             // to pre-dex each module and produce an APK that can be tested on
             // Android Lollipop without time consuming dex merging processes.
             minSdkVersion 21
         }
         prod {
             // The actual minSdkVersion for the application.
             minSdkVersion 14
         }
     }
           ...
     buildTypes {
         release {
             runProguard true
             proguardFiles getDefaultProguardFile( 'proguard-android.txt' ),
                                                  'proguard-rules.pro'
         }
     }
}
dependencies {
   compile 'com.android.support:multidex:1.0.0'
}


MultiDex实现原理

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

1.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的官网执行步骤如下:

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

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

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

2.Dex加载

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

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

BaseDexClassLoader findClass的过程如下:

[代码]java代码:

?
01
02
03
04
05
06
07
08
09
10
11
12
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;
}</throwable></throwable>


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

[代码]java代码:

?
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
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) {
         }
     }</file></ioexception></ioexception></file>


理解官方的分包原理是掌握插件化技术和热修复技术的基础。例如:美团Android分包策略 就是在MultiDex的基础上进行了优化,可以在运行时自由的加载Secondary DEX,既能保证冷启动速度,又能减少运行时的内存占用。

携程Android App插件化技术参考这两篇文章:携程Android App插件化和动态加载实践 , 再议携程Android动态加载框架DynamicAPK 携程android中加载类使用的是PathClassLoader。

对于热修复很多第三方是使用PathClassLoader,PathClassLoader和DexClassLoader都继承自BaseDexClassLoader,Android 使用PathClassLoader作为其系统类和应用类的加载器。这个类只能去加载已经安装到Android系统中的apk文件, DexClassLoader可以用来从.jar和.apk类型的文件内部加载classes.dex文件。可以用来执行非安装的程序代码。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值