MasterofProject

An introduction to the DEX Android automatic package and dynamic loading of the US group

label MultiDex
6634 people read comment(7) Collection report
Classification:

Recently also encountered a number of methods to solve the problem of more than 65K, and ultimately through the MultiDex method to solve the feeling is still relatively simple, that is, to change the gradle file, do not know why the U.S. mission to get such a complex program




Original http://tech.meituan.com/mt-android-auto-split-dex.html


summary

As an Android Developer, in application development, with the scale of business development to a certain extent, continue to join new functions, add a new library, code in the rapid expansion, corresponding APK size has increased dramatically, someday, you will have the misfortune to encounter this error:

  1. The generated APK cannot be installed on the Android 2.3 or before the machine, prompted by INSTALL_FAILED_DEXOPT
  2. Method is too many, compile time error, prompt:
    To Dalvik format failed:Unable Conversion to execute dex: method ID not in [0 0xffff]:: 65536

The specific reasons for the problems are as follows:

  1. Unable to install (Android 2.3 install failed INSTALL_FAILED_DEXOPT) problem, is caused by dexopt LinearAlloc. In different versions of Android respectively, through the 4M/5M/8M/16M limit, the mainstream 4.2.x system may have to 16m. Only the size of 5m Gingerbread or following system LinearAllocHdr allocate space for the above system the gingerbread upgrade to 8m. LinearAlloc Dalvik is a fixed size buffer. In the application of the installation process, the system will run a program called dexopt for the application in the current model to run the preparation. Dexopt uses LinearAlloc to store information about the method of application. Android 2.2 and 2.3 of the buffer is only Android, 4.x 5MB to the 8MB or 16MB. When the method is too large to exceed the buffer size, it will cause a dexopt crash.

  2. Exceed the maximum number of restrictions on the problem is due to limit the DEX file format, a DEX file method by using native type short document indexing method is 4 bytes totaling up to express 65536 method, the number of field/class also has the limit. The DEX file is project will be required for all class files are merged and compressed to a DEX file during, is packaged Android DEX process, a single DEX file can be referenced method Total (to develop their own code and referenced by the Android framework, class library code) is limited to 65536;

Plug in? MultiDex?

To solve this problem, generally have the following scheme, a scheme is increased Proguard, efforts to reduce the DEX's size and the number of methods, but this is palliatives scheme, with the addition of business code, number method will eventually this limit is reached, a popular scheme is the plug-in program, another is provided by Google MultiDex and Google in the launch of MultiDex before Android developers blog introduced byCustom class loading process, and then is the introduction of Facebook for Android application developmentDalvik patch, but Facebook blog writing is not very detailed; we in the plug-in program also made to explore and try, find the plug-in program deployment, first of all need to comb and modify the code of each line of business, the decoupling, relatively large changes to the surface and volume, through a discussion and analysis, we believe that for us at present the MultiDex more reliable number so that we can fast and simple to code split, at the same time code changes also in the acceptable range; so we adopt the Google MultiDex were developed.

Plug in the program in the industry has a different implementation of the principle, here is no longer one one lists, here only to list the next Google to build more than 65K number of applications to provide the official support of the program:MultiDex.

首先使用Android SDK经理升级到最新的Android SDK构建工具和Android支持库。然后进行以下两步操作:

1。修改Gradle配置文件,启用multidex并包含multidex支持:

安卓{
compilesdkversion 21 buildtoolsversion”21.1.0”

defaultconfig {
…
例子14
targetSdkVersion 21
…

/ /使multidex支持。
multidexenabled真实
}
…
}
依赖{编译的COM。Android支持:multidex:1.0.0”。
}

2。让应用支持多文件在官方文档中描述了三种可选方法地塞米松:

在AndroidManifest. xml的应用中声明android.support.multidex.multidexapplication;
如果你已经有自己的应用类,让其继承multidexapplication;
如果你的应用类已经继承自其它类,你不想/能修改它,那么可以重写attachbasecontext()方法:

“重写”
protected void attachbasecontext(碱基){
超级attachbasecontext(基地);
multidex安装(这);
}

并在体现中添加以下声明:

<?xml version=“1”encoding=“utf-8”?>
<体现xmlns:Android =“http://schemas.android.com/apk/res/android包=“COM。例子。Android。multidex。MyApplication“>
<应用程序
…
安卓:名称=“Android。支持。multidex。multidexapplication”>
…
< /应用程序>
< /清单>

如果已经有自己的应用,则让其继承multidexapplication即可。

DEX自动拆包及动态加载

multidex带来的问题

在第一版本采用multidex方案上线后,在Dalvik下multidex带来了下列几个问题:

  1. 在冷启动时因为需要安装DEX文件,如果DEX文件过大时,处理时间过长,很容易引发ANR(应用程序无响应);
  2. 采用multidex方案的应用可能不能在低于Android 4(API Level 14)机器上启动,这个主要是因为Dalvik linearalloc的一个bug(发行22586);
  3. 采用multidex方案的应用因为需要申请一个很大的内存,在运行时可能导致程序的崩溃,这个主要是因为Dalvik linearalloc的一个限制(发行78035)。这个限制在Android 4(API Level 14)已经增加了,应用也有可能在低于Android 5(API Level 21)版本的机器上触发这个限制;

而在艺术下multidex是不存在这个问题的,这主要是因为艺术下采用提前(AOT)编译技术,系统在APK的安装过程中会使用自带的dex2oat工具对APK中可用的DEX文件进行编译并生成一个可在本地机器上运行的文件,这样能提高应用的启动速度,因为是在安装过程中进行了处理这样会影响应用的安装速度,对艺术感兴趣的可以参考一下艺术和Dalvik的区别

multidex的基本原理是把通过dexfile来加载二DEX,并存放在basedexclassloader的dexpathlist中。

下面代码片段是basedexclassloader findClass的过程:

保护级<?> findClass(String name)抛出ClassNotFoundException {
<错误>列表suppressedexceptions =新的ArrayList < Throwable >();
C类= pathlist findClass(名称、suppressedexceptions);
如果(=空){
cnfe =新时抛出ClassNotFoundException(“没有找到类\”+姓名+“\”的路径:“+ pathlist);
对于(Throwable T:suppressedexceptions){
cnfe addsuppressed(T);
}
把cnfe;
}
返回;
}

下面代码片段为怎么通过dexfile来加载二DEX并放到basedexclassloader的dexpathlist中:

私有静态无效安装(类装载器装载机,单additionalclasspathentries <文件>,
文件optimizeddirectory)
非法存取异常抛出时,
nosuchfieldexception,invocationtargetexception,调用不存在方法异常{
被修补的类装入程序将是一个后代
* dalvik.system.basedexclassloader。我们修改它
* dalvik.system.dexpathlist pathlist场追加DEX
*文件项。
*
场pathlistfield = findfield(装载机,“pathlist”);
对象dexpathlist = pathlistfield得到(装载机);
ArrayList < IOException > suppressedexceptions =新的ArrayList < IOException >();
expandfieldarray(dexpathlist,“dexelements”,makedexelements(dexpathlist,
新的ArrayList <文件>(additionalclasspathentries),optimizeddirectory,
suppressedexceptions));
尝试{
如果(suppressedexceptions。size() > 0){
对于(IOException e:suppressedexceptions){
/ /日志。W(标签,“例外makedexelement”,E);
}
场suppressedexceptionsfield =
findfield(装载机,“dexelementssuppressedexceptions”);
IOException [ ] dexelementssuppressedexceptions =
(IOException [ ])suppressedexceptionsfield得到(装载机);

如果(dexelementssuppressedexceptions = = null){
dexelementssuppressedexceptions =
suppressedexceptions toArray(。
新方法中的[ suppressedexceptions size() ]);
{ }
IOException [ ]结合=
新size() + [ suppressedexceptions IOException。
dexelementssuppressedexceptions长];
suppressedexceptions toArray(组合);
系统。arraycopy(dexelementssuppressedexceptions,0,联合,
suppressedexceptions。size(),dexelementssuppressedexceptions长度);
dexelementssuppressedexceptions =组合;
}

suppressedexceptionsfield集(装载机,dexelementssuppressedexceptions);
}
(例外情况){
}
}

DEX自动拆包及动态加载方案简介

通过查看multidex的源码,我们发现multidex在冷启动时容易导致ANR的瓶颈在Dalvik VM,2.1版本之前的的版本中,multidex的安装大概分为几步,第一步打开APK这个拉链包,第二步把multidex的地塞米松(DEX解压出来除去类。之外的其他DEX,例如:classes2.dex,课。DEX等等),因为Android系统在启动APP时只加载了第一个类。DEX,其他的DEX需要我们人工进行安装,第三步通过反射进行安装,这三步其实都比较耗时,为了解决这个问题我们考虑是否可以把DEX的加载放到一个异步线程中,这样冷启动速度能提高不少,同时能够减少冷启动过程中的ANR,对于Dalvik linearalloc的一个缺陷(发行22586和限制()发行78035),我们考虑是否可以人工对DEX的拆分进行干预,使每个DEX的大小在一定的合理范围内,这样就减少触发Dalvik linearalloc的缺陷和限制;为了实现这几个目的,我们需要解决下面三个问题:

  1. 在打包过程中如何产生多个的DEX包?
  2. 如果做到动态加载,怎么决定哪些DEX动态加载呢?
  3. 如果启动后在工作线程中做动态加载,如果没有加载完而用户进行页面操作需要使用到动态加载DEX中的类怎么办?

我们首先来分析如何解决第一个问题,在使用multidex方案时,我们知道buildtool会自动把代码进行拆成多个DEX包,并且可以通过配置文件来控制哪些代码放到第一个DEX包中,下图是Android的打包流程示意图:

为了实现产生多个DEX包,我们可以在生成DEX文件的这一步中,在蚂蚁或Gradle中自定义一个任务来干预DEX产生的过程,从而产生多个DEX,下图是在蚂蚁和Gradle中干预产生DEX的自定任务的截图:

tasks.whentaskadded {任务>
如果(任务。名字。从('proguard ')&(任务。名字。EndsWith('debug”)| |任务。名字。EndsWith('release '))){
task.dolast {
makedexfileafterproguardjar();
}
task.dofirst {
删除“$ {项目。builddir } /中间体/类混淆器”;

串味=任务。名字。substring('proguard’。length(),任务。名称字符串(任务。名字。EndsWith('debug ')?”调试“:”“释放”);
generatemainindexkeeplist(风味。tolowercase());
}
} else if(任务。名字。从('zipalign ')&(任务。名字。EndsWith('debug”)| |任务。名字。EndsWith('release '))){
task.dofirst {
ensuremultidexinapk();
}
}
}

上一步解决了如何打包出多个DEX的问题了,那我们该怎么该根据什么来决定哪些类放到主要DEX,哪些放到二DEX呢(这里的主要DEX是指在2.1版本的Dalvik虚拟机之前由Android APK时自己主动加载的系统在启动类。DEX,而二DEX是指需要我们自己安装进去的DEX,例如:classes2.dex,课。DEX等),这个需要分析出放到主要DEX中的类依赖,需要确保把主要DEX中类所有的依赖都要放进来,否则在启动时会发生ClassNotFoundException服务提供商,这里我们的方案是把、接收机、涉及到的代码都放到主要DEX中,而把活动涉及到的代码进行了一定的拆分,把首页活动活动、、火箭炮欢迎页的活动、城市列表页活动等所依赖的类放到了主要DEX中,把二级、三级页面的活动以及业务频道的代码放到了二DEX中,为了减少人工分析类的依赖所带了的不可维护性和高风险性,我们编写了一个能够自动分析类依赖的脚本,从而能够保证主要DEX包含类以及他们所依赖的所有类都在其内,这样这个脚本就会在打包之前自动分析出启动到主要DEX所涉及的所有代码,保证主要DEX运行正常。

随着第二个问题的迎刃而解,我们来到了比较棘手的第三问题,如果我们在后台加载二DEX过程中,用户点击界面将要跳转到使用了在二DEX中类的界面,那此时必然发生ClassNotFoundException,那怎么解决这个问题呢,在所有的活动跳转代码处添加判断二DEX是否加载完成?这个方法可行,但工作量非常大;那有没有更好的解决方案呢?我们通过分析活动的启动过程,发现活动是由activitythread通过仪器仪表中做一定的手脚呢通过分析代码来启动的,我们是否可以在?activitythread仪器仪表发现,仪表有关活动启动相关的方法大概有:execstartactivity、newactivity等等,这样我们就可以在这些方法中添加代码逻辑进行判断这个类是否加载了,如果加载则直接启动这个活动,如果没有加载完成则启动一个等待的活动显示给用户,然后在这个活动中等待后台二DEX加载完成,完成后自动跳转到用户实际要跳转的活动;这样在代码充分解耦合,以及每个业务代码能够做到颗粒化的前提下,我们就做到二DEX的按需加载了,下面是仪表添加的部分关键代码:

公共activityresult execstartactivity(语境,IBinder contextthread,IBinder令牌,活动目标,
意图,意向,int requestCode){
activityresult activityresult = null;
String className;
如果getcomponent()(意图!=空){
类名=意图。getcomponent()。getclassname();
{ }
resolveinfo resolveactivity =谁。getpackagemanager() resolveactivity(意图,0);

如果(resolveactivity!= null && resolveactivity.activityinfo!=空){
resolveactivity.activityinfo.name classname =;
{ }
类名= null;
}
}

如果(!textutils。isempty(类名)){
布尔shouldinterrupted =!meituanapplication。isdexavailable();
如果(meituanapplication。sisdexavailable。get() | | mbypassactivityclassnamelist。包含(类名)){
shouldinterrupted = false;
}
如果(shouldinterrupted){
目的interruptedintent =新的意图(mcontext,waitingactivity。类);

activityresult = execstartactivity(谁,contextthread,令牌,目标,interruptedintent,requestCode);
{ }
activityresult = execstartactivity(谁,contextthread,令牌,目标,意图,requestCode);
}
{ }
activityresult = execstartactivity(谁,contextthread,令牌,目标,意图,requestCode);
}

返回activityresult;
}

公共活动newactivity(类<?>类、语境、IBinder令牌,
申请,意图意图,activityinfo信息,
CharSequence称号,活动的家长,字符串ID、对象lastnonconfigurationinstance)
把instantiationexception,非法存取异常{

String className =“”;
活动newactivity = null;
如果getcomponent()(意图!=空){
类名=意图。getcomponent()。getclassname();
}

布尔shouldinterrupted =!meituanapplication。isdexavailable();
如果(meituanapplication。sisdexavailable。get() | | mbypassactivityclassnamelist。包含(类名)){
shouldinterrupted = false;
}
如果(shouldinterrupted){
意图=新的意图(mcontext,waitingactivity。类);
newactivity = MBASE。newactivity(类、语境、令牌,
应用,意图,信息,标题,家长,身份证,
lastnonconfigurationinstance);
{ }
newactivity = MBASE。newactivity(类、语境、令牌,
应用,意图,信息,标题,家长,身份证,
lastnonconfigurationinstance);
}
返回newactivity;
}

实际应用中我们还遇到另外一个比较棘手的问题,就是场的过多的问题,场过多是由我们目前采用的代码组织结构引入的,我们为了方便多业务线、多团队并发协作的情况下开发,我们采用的AAR的方式进行开发,并同时在AAR依赖链的最底层引入了一个通用业务AAR,而这个通用业务AAR中包含了很多资源,而ADT14资源处理时以及更高的版本中对图书馆,图书馆的R资源不再是静态的了,详情请查看谷歌官方说明图书馆,这样在最终打包时中的R没法做到内联,这样带来了R场过多的情况,导致需要拆分多个二DEX,为了解决这个问题我们采用的是在打包过程中利用脚本把图书馆中R场(例如ID、布局、冲等)的引用替换成常量,然后删去图书馆中r.class中的相应场。

总结

上面就是我们在使用multidex过程中进化而来的DEX自动化拆包的方案,这样我们就可以通过脚本控制来进行自动化的拆分DEX,然后在运行时自由的加载二DEX,既能保证冷启动速度,又能减少运行时的内存占用。

猜你在找
查看评论
*以上用户言论只代表其个人观点,不代表CSDN网站的观点或立场
    个人资料
    • 访问:601015次
    • 积分:八千三百四十
    • 等级:
    • 排名:1251名第
    • 原创:185篇
    • 转载:240篇
    • 译文:10篇
    • 评论:237条
    博客专栏
    文章分类
    Latest comments