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的配置,支持multidex2.修改你的manifest。让其支持multidexapplication类
注意其中第二步其实还有另外两种替代方法,下面介绍。
修改Gradle的配置,支持multidex:
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 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);如下:
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文件中的类,可以根据如下所示的方式进行修改:
@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中.
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,其中是一个文本文件的文件名,存放在和这个Gradle脚本同一级的文件目录下.而这个文本文件的内容如下.实际就是把需要放在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配置完成后运行。
如果用使用其他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编译版本的话,会比平常用更多的时间.Android的官方文档也给了我们一个小小的建议,利用Gradle建立两个Flavor.一个minSdkVersion设置成21,这是用了ART支持的Dex格式,避免了MultiDex的开销.而另外一个Flavor就是原本支持的最小sdkVersion.平时开发时候调试程序,就用前者的Flavor,发布版本打包就用后者的Flavor.
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的过程如下:
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) {
}
}
理解官方的分包原理是掌握插件化技术和热修复技术的基础。例如:美团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文件。可以用来执行非安装的程序代码。
Android分包原理的更多相关文章
- Android root 原理
Android root 原理 0x00 关于root linux和类Unix系统的最初设计都是针对多用户的操作系统,对于用户权限的管理很非常严格的,而root用户(超级用户)就是整个系统的唯一管理员 ...
- NFC(6)NFC编程的几个重要类,NFC硬件启动android应用原理
用于NFC编程的几个重要类 Tag NFC 标签 NfcAdapter Nfc 的适配类 NdefMessage 描述NDEF格式的信息 NdefRecord 描述NDEF信息的一个信息段,类似tab ...
- android的原理,为什么不需要手动关闭程序
转自android的原理,为什么不需要手动关闭程序 不用在意剩余内存的大小,其实很多人都是把使用其他系统的习惯带过来来了. Andoird大多应用没有退出的设计其实是有道理的,这和系统对进程的调度机制 ...
- 【转】Android Activity原理以及其子类描述,androidactivity
Android Activity原理以及其子类描述,androidactivity 简介 Activity是Android应用程序组件,实现一个用户交互窗口,我们可以实现布局填充屏幕,也可以实 ...
- 传智播客学习之Android运行原理 (转)
传智播客学习之Android运行原理 (2010-03-20 22:45:15) 转载▼ 今天终于忙里偷闲,和大家探讨一下android技术,第一次听到3G应该追溯到大学三年级的时候了,记得当时现代通 ...
- Android ADB原理及常用命令
Android调试桥(ADB, Android Debug Bridge)是一个Android命令行工具,包含在SDK 平台工具包中,adb可以用于连接Android设备,或者模拟器,实现对设备的控制 ...
- Android分包MultiDex原理详解
MultiDex的产生背景 当Android系统安装一个应用的时候,有一步是对Dex进行优化,这个过程有一个专门的工具来处理,叫DexOpt.DexOpt的执行过程是在第一次加载Dex文件的时候执行的 ...
- android分包方案
当一个app的功能越来越复杂,代码量越来越多,也许有一天便会突然遇到下列现象: 1. 生成的apk在2.3以前的机器无法安装,提示INSTALL_FAILED_DEXOPT 2. 方法数量过多,编译时 ...
- 用Ant手动打包android程序,android分包机制解决65536方法过多异常
Android利用ant手动打包 首先我们要给自己的IDE eclispe配置ant,默认的eclipse是集成了ant构建工具的,但是google提供的Android集成开发工具ADT,里面封装了E ...
随机推荐
- Debug 单步执行命令step into/step out/step over的区别
总结一下在debug中三种调试的区别: step into就是单步执行,遇到子函数就进入并且继续单步执行: step over是在单步执行时,在函数内遇到子函数时不会进入子函数内单步执行,而是将子函数 ...
- e868. 获取和设置本地外观
By default, Swing uses a cross-platform look and feel called Metal. In most cases, it is more desira ...
- 系统中hosts文件有哪些作用
hosts文件位于系统盘C:\Windows\System32\drivers\etc中,hosts是一个没有扩展名的系统文件,其基本作用就是将一些常用的网址域名与其对应的IP地址建立一个关联“数据库 ...
- Windows版Jenkins+SVN+Maven自动化部署环境搭建【转】
前言 因今年公司新产品线较多,为了降低耦合,达到业务分离.重用,提高内部开发效率的目的,采用了基于服务组件.前后端分离的架构体系.与之前传统单应用架构相比,系统部署.配置更加复杂,为了能够频繁地将软件 ...
- Java泛型概述
泛型是Java中一个非常重要的知识点,在Java集合类框架中泛型被广泛应用.本文我们将从零开始来看一下Java泛型的设计,将会涉及到通配符处理,以及让人苦恼的类型擦除. 泛型基础 泛型类 我们首先定义 ...
- 3.7 su命令 3.8 sudo命令 3.9 限制root远程登录
3.7 su命令 3.8 sudo命令 3.9 限制root远程登录 su命令 切换用户 [root@centos_1 ~]# su - xiaobo [root@centos_1 ~]# su - ...
- SQL2000系统表、存储过程、函数的功能介绍及应用
转自:http://blog.csdn.net/zlp321002/article/details/480925 ----系统表------------------------------------ ...
- js如何获取asp.net服务器端控件的值(label,textbox,dropdownlist,radiobuttonlist等)
js如何获取asp.net服务器端控件的值(label,textbox,dropdownlist,radiobuttonlist等) 欢迎访问原稿:http://hi.baidu.com/2wixia ...
- Publish/Subscribe Model——Notification chain——观察者模式
内核中用的很多,整理时间子系统的时候又遇到了notification mechanism,因此做次记录: 参考:1.http://msdn.microsoft.com/en-us/library/ff ...
- adb错误:Failed to execute android command 'adb devices'.
好吧,我是用的phonegap3.0开发的,很简单,安装的时候一句phonegap run android –device就可以了(-device参数非必要,我是为了不跑模拟器,加上此参数限制只跑到设 ...