配色: 字号:
异步加载multidex
2017-01-04 | 阅:  转:  |  分享 
  
异步加载multidex



官方文档已经对这个做了比较详述的说明。

简单总结就是:早期dex执行文件的方法数限制在65536范围之内,如果超出这个限制,构建就会失败。



然而,为什么会构建失败,这个65536限制究竟是在哪里?既然dex文件构建失败,首先想到肯定就是去dx.jar找原因。

构建失败一般会有以下的日志:



UNEXPECTEDTOP-LEVELEXCEPTION:java.lang.IllegalArgumentException:methodIDnotin[0,0xffff]:65536

atcom.android.dx.merge.DexMerger$6.updateIndex(DexMerger.java:501)

atcom.android.dx.merge.DexMerger$IdMerger.mergeSorted(DexMerger.java:276)

atcom.android.dx.merge.DexMerger.mergeMethodIds(DexMerger.java:490)

atcom.android.dx.merge.DexMerger.mergeDexes(DexMerger.java:167)

atcom.android.dx.merge.DexMerger.merge(DexMerger.java:188)

atcom.android.dx.command.dexer.Main.mergeLibraryDexBuffers(Main.java:439)

atcom.android.dx.command.dexer.Main.runMonoDex(Main.java:287)

atcom.android.dx.command.dexer.Main.run(Main.java:230)

atcom.android.dx.command.dexer.Main.main(Main.java:199)

atcom.android.dx.command.Main.main(Main.java:103):Derp:dexDerpDebugFAILED



那么就去搜索“methodIDnotin”和根据错误信息提示的堆栈,果然在DexMerger的方法:

mergeMethodIds



中搜索到这段。然后顺藤摸瓜,找到调用栈:



调用栈



最后红框的就是dx.jar的入口main函数,而且也和错误日志是一致的。



另外,虽然从这个堆栈看,65536的问题找到了,而事实上,关于65535这个限制的地方不止这一处,上面的限制是在多个dexmerger的情况下发生的,然而,有些时候,即使只有一个dex文件,也同样出现这个问题,但是出错的日志并不相同,类似下面:



Dxtroublewritingoutput:Toomanymethodreferences:107085;maxis65536.

Youmaytryusing–multi-dexoption.

这种情况的出现,原因不详述,可以参考这篇文章,同样也是出现在dx.jar中。



针对这个,google官方推出了multidex的方案来解决这个构建的问题。



又爱又恨的Multidex



虽然google推出了multidex,然而,这个一个令人头疼的方案。对此,官方明确指出了方案的limitation:



1.Theinstallationof.dexfilesduringstartupontoadevice’sdatapartitioniscomplexandcanresultinApplicationNotResponding(ANR)errorsifthesecondarydexfilesarelarge.Inthiscase,youshouldapplycodeshrinkingtechniqueswithProGuardtominimizethesizeofdexfilesandremoveunusedportionsofcode.

2.ApplicationsthatusemultidexmaynotstartondevicesthatrunversionsoftheplatformearlierthanAndroid4.0(APIlevel14)duetoaDalviklinearAllocbug(Issue22586).IfyouaretargetingAPIlevelsearlierthan14,makesuretoperformtestingwiththeseversionsoftheplatformasyourapplicationcanhaveissuesatstartuporwhenparticulargroupsofclassesareloaded.Codeshrinkingcanreduceorpossiblyeliminatethesepotentialissues.

3.ApplicationsusingamultidexconfigurationthatmakeverylargememoryallocationrequestsmaycrashduringruntimeduetoaDalviklinearAlloclimit(Issue78035).TheallocationlimitwasincreasedinAndroid4.0(APIlevel14),butappsmaystillrunintothislimitonAndroidversionspriortoAndroid5.0(APIlevel21).

4.TherearecomplexrequirementsregardingwhatclassesareneededintheprimarydexfilewhenexecutingintheDalvikruntime.TheAndroidbuildtoolingupdateshandletheAndroidrequirements,butitispossiblethatotherincludedlibrarieshaveadditionaldependencyrequirementsincludingtheuseofintrospectionorinvocationofJavamethodsfromnativecode.Somelibrariesmaynotbeabletobeuseduntilthemultidexbuildtoolsareupdatedtoallowyoutospecifyclassesthatmustbeincludedintheprimarydexfile.

从上面提到的第一点可以知道,dex的install过程比较复杂,容易引起ANR的发生。ANR发生的原因很简单,莫非就是在UI线程作耗时操作。那为什么不在非UI线程做,如果在非UI线程进行dex的install过程,这个问题不就迎刃而解。理想很丰满,现实很骨感。



当尝试另起线程进行dex文件合并时,运行时如无意外就会发生ClassNotFoundExecption。原因很简单,当简单应用multidex方案的时候,dex文件只是进行简单拆分,不同classes会被分到不同的dex文件中。当你异步installdex文件时,应用初始只加载了第一个dex,也就是所谓的maindex,其他dex文件什么时候加载完成,不得而知,如果maindex中引用某个class,而这个class却在另一个没有install完成的dex文件中,自然就dalvik虚拟机中并不存在这个class。



那么该如何解决这个问题?业界自然会有方案。例如美团这个方案,例如腾讯的方案。可惜,方案都停留在这篇文章上,并没有开源出来分享。

但是,其实莫非就一个关键点:按需生成maindex。

如果能够按自己的要求,把特定的某些classes放到maindex中,例如程序启动部分代码类,一级页面类等,其他类可以放到另外dex,然后通过程序控制,保证在使用其他dex文件中的classes之前,dex的install过程完成,那么整个方案就pass了。



异步multidex实现(基于gradle构建)



首先,要深入认识apk打包的整个流程:



APK打包流程



既然,需要定制dex生成,就必须要搞清楚dx命令,因为它就是提供multidex支持的最根本的地方:



dx命令



从dx命令的图看出,multidex的支持就是dx提供的。仔细看参数:



--main-dex-list=



该参数实际意义就是,可以指定maindex的里面包含什么classes文件,那么它就是实现方案的基础。



既然dx支持按需生成maindex,那么,如何产生maindex的classes列表就是最最最核心的问题了。



这个问题困扰了很久,实现这个有两种办法:



1.每次打包,都运行一次,然后通过程序,找出程序启动后固定时间内(例如直到第一个界面resume)的所有classes。类似这里提到的[方案](https://medium.com/groupon-eng/android-s-multidex-slows-down-app-startup-d9f10b46770f#.ogmk4ytsu)。



2.编译过程,就通过程序或者脚本,分析出依赖来生成maindex。



第一个方案可行,但是很不理想,自动化太差。毕竟特别对于release版本牵涉到proguard的问题,要进行重新mapping的处理,才能找到所需class文件,而且需要二次打包,显得过于笨拙。



第二个方案,无疑更加优秀。那么,怎样通过程序或者脚本生成maindex文件,或者说,怎么生成maindexlistclases来产生maindex呢?



然后就想到,既然androidgraldeplugin是集成支持multidex的,那么它自然就有整个类似的过程。

由于gradle1.5以后采用了transfomapi,所以要分开1.5前和1.5后的来说这个构建过程。本文的采用的是1.3.1的和2.2.0的plugin。



gradle-plugin-1.3.1



先看gradle-plugin-1.3.1,不断反复查看构建的日志

构建日志,



根据字面意思,发现几个关键的task:



collectReleaseMultiDexComponents

packageAllReleaseClassesForMultiDex

shrinkReleaseMultiDexComponents

createReleaseMainDexClassList



然后一个个task来分析。



1.collectReleaseMultiDexComponents



不断搜索,看到这篇文章,然后清楚了该task对应的源码在这里



从源码确认,该task的任务是,根据manifest,keep住activity,service,broadcastreceiver,provider,instrumentation,application等,文件输出在:build\intermediates\multi-dex\release\manifest_keep.txt



2.packageAllReleaseClassesForMultiDex



这个task很简单,从日志看,就是打包所有的jar,输出在:

build\intermediates\multi-dex\release\allclasses.jar



这个task的源码在这里



3.shrinkReleaseMultiDexComponents



从日志看:

shrinkReleaseMultiDexComponents日志

该task实际执行的就是proguard的shrink的过程。



task的输出是build\intermediates\multi-dex\release\componentClasses.jar,这个jar的生成,是根据上面的task1生成的manifest_keep.txt和task2生成的classes.jar,可能还加上proguard文件,通过proguardshrink生成的。关于proguardshrink的详细内容就不展开叙述,可以参看相关资料,例如android官网、proguard。



4.createReleaseMainDexClassList



这个task的源码。

它的主要作用是,根据上面task3产生的componentsClasses.jar和task2产生的allclasses.jar,最终生成maindexlist,包含了所有maindex的class文件。输出在build\intermediates\multi-dex\release\maindexlist.txt



到此,就搞清楚了整个androidgradleplugin关于multidex的流程了。

那么问题来了,怎么能够按照自己的需要,产生自己的maindexlist呢?

其实产生maindexlist的关键在task3和task4,因为maindex中究竟keep住哪些类,是根据task3产生的componentClasses.jar来的,而componentClasses.jar的class是作为rootclass存在,然后在task4中找出这些rootclass相关联的classes。

那么这个分析过程是如何的呢?再看日志。



15:08:28.098[DEBUG][org.gradle.api.internal.tasks.execution.ExecuteAtMostOnceTaskExecuter]Startingtoexecutetask'':floor:createReleaseMainDexClassList''

15:08:28.099[DEBUG][org.gradle.api.internal.tasks.execution.SkipUpToDateTaskExecuter]Determiningiftask'':floor:createReleaseMainDexClassList''isup-to-date

15:08:28.100[INFO][org.gradle.api.internal.tasks.execution.SkipUpToDateTaskExecuter]Executingtask'':floor:createReleaseMainDexClassList''(up-to-datechecktook0.001secs)dueto:

Nohistoryisavailable.

15:08:28.100[DEBUG][org.gradle.api.internal.tasks.execution.ExecuteActionsTaskExecuter]Executingactionsfortask'':floor:createReleaseMainDexClassList''.

15:08:28.101[INFO][org.gradle.process.internal.DefaultExecHandle]Startingprocess''command''C:\ProgramFiles\Java\jdk1.7.0_79\bin\java.exe''''.Workingdirectory:D:\xxxx\xxxxx\floorCommand:C:\ProgramFiles\Java\jdk1.7.0_79\bin\java.exe-Dfile.encoding=UTF-8-Duser.country=CN-Duser.language=zh-Duser.variant-cpC:\Users\Administrator\AppData\Local\Android\android-sdkwww.baiyuewang.net\build-tools\23.0.1\lib\dx.jarcom.android.multidex.ClassReferenceListBuilderD:\xxxx\xxxx\floor\build\intermediates\multi-dex\release\componentClasses.jarD:\xxxx\xxxx\floor\build\intermediates\multi-dex\release\allclasses.jar



从日志得知,分析的过程是执行了dx.jar里面的ClassReferenceListBuilder。经过一番查看,看到下面这段关键代码:



/

@paramjarOfRootsArchivecontainingtheclassfilesresultingofthetracing,typically

thisistheresultofrunningProGuard.

/

publicvoidaddRoots(ZipFilejarOfRoots)throwsIOException{

//keeproots

for(Enumerationentries=jarOfRoots.entries();

entries.hasMoreElements();){

ZipEntryentry=entries.nextElement();

Stringname=entry.getName();

if(name.endsWith(CLASS_EXTENSION)){

classNames.add(name.substring(0,name.length()-CLASS_EXTENSION.length()));

}

}

//keepdirectreferencesofroots(+directreferenceshierarchy)

for(Enumerationentries=jarOfRoots.entries();

entries.hasMoreElements();){

ZipEntryentry=entries.nextElement();

Stringname=entry.getName();

if(name.endsWith(CLASS_EXTENSION)){

DirectClassFileclassFile;

try{

classFile=path.getClass(name);

}catch(FileNotFoundExceptione){

thrownewIOException("Class"+name+

"ismissingformoriginalclasspath"+path,e);

}

addDependencies(classFile);

}

}

}

SetgetClassNames(){

returnclassNames;

}

privatevoidaddDependencies(DirectClassFileclassFile){

for(Constantconstant:classFile.getConstantPool().getEntries()){

if(constantinstanceofCstType){

checkDescriptor(((CstType)constant).getClassType().getDescriptor());

}elseif(constantinstanceofCstFieldRef){

checkDescriptor(((CstFieldRef)constant).getType().getDescriptor());

}elseif(constantinstanceofCstBaswww.tt951.comeMethodRef){

checkPrototype(((CstBaseMethodRef)constant).getPrototype());

}

}

FieldListfields=classFile.getFields();

intnbField=fields.size();

for(inti=0;i
checkDescriptor(fields.get(i).getDescriptor().getString());

}

MethodListmethods=classFile.getMethods();

intnbMethods=methods.size();

for(inti=0;i
checkPrototype(Prototype.intern(methods.get(i).getDescriptor().getString()));

}

}



通过查看dx.jar源码,最后确定task4的依赖分析过程是,根据class字节码的constantpool,找出类依赖,这些依赖包括superclass,fields,methods,interfaces中出现的类依赖。而仔细看,这里的依赖分析仅仅是分析rootclasses的依赖,而rootclasses依赖的class的依赖是不包含在分析结果中,这就是我们异步加载multidex的时候出现ClassNotFounedException的主要原因。



那怎么解决呢,很简单,我们只需要循环执行依赖分析,那么这个问题就迎刃而解。循环执行就是说,把rootclasses的依赖classes又作为rootclasses去分析,如此循环,直到形成闭环。当然,这会存在极端情况就是,工程内所有classes都存在相互依赖,但这个不是坏处,而是说明程序写的太完美,一点多余的classes都不存在,而事实上,这种情况是基本不可能存在。



循着这个想法,就把dx.jar的相关源码拉下来,然后基于dx.jar实现自己的依赖分析。

而且,由于是循环依赖分析,所以输入根本不需要componentsClasses.jar作为输入,只需要指定几个入口类(写在配置文件中,作为依赖分析的inputfile),就可以完成整个分析过程,例如application,SplashActivity等入口类。然后把依赖分析出的依赖输出到maindexlist.txt中,也就是相当修改掉gradle-plugin的createReleaseMainDexClassList输出的内容,这样就可以保证maindex的keep住的类是我们期待的类。



通过这个简单方法,就可以实现multidex的异步install了,因为启动的时候需要加载的类都存在maindex中了,即使seconddex没有install,也不影响启动的过程。



我们也可以继续改进,参考美团的方案,我们可以把一级activity放到maindex,其他的activity放到其他dex中,我们只需要配置一个过滤列表,例如,非一级activity的activity类不能作为rootclasses去分析,然后代码上的activity跳转不要使用类似Intent(mContext,xxxx.class)的方法,而改为使用字符串。

或者,更直接的就是,可以在代码上做修改,直接不要引用二级activity。当然,这对现有工程会有一定量的修改工作。



依赖分析的dx.jar修改完成,然后就着手修改gradle的script。。。。

脚本修改完,就是程序代码修改。



代码修改主要关注下面的几个点:



1.异步installdex;



2.参考美团的实现,在二级页面没有加载完成之前,跳转一个中转的activity,直到二级页面加载完成;(这里使了点坏,替换了ActivityThread的mInstrument为自己的自定义对象,从而实现activity的跳转拦截)。但是这个方案相对会复杂点,要hack一些代码,还要对跳转二级页面的代码进行一定的修改工作,这对于已经存在的庞大工程来说,可能会需要多加小心,不然很容易出现classNotFind等情况出现。



3.不参考美团实现,像QQ(据了解,貌似新安装qq时,先跳转一个loading的界面,事实执行的就是multidex的install,待确认?)那样,新安装应用时,在splashactivity的时候显示进度条,等待install完成,这样我们仅仅需要修改初始化部分的代码。



至此,整个方案呼之欲出,基本实现了异步加载multidex的想法。



gradle-plugin-2.2.0



针对1.5plugin以后的情况,其实大致是一样的,只是在部分plugintask执行上有所区别的,但是原理基本和gradle-plugin-1.3.1是一致的,目标就只有一个:就是替换掉gradleplugin默认的task产生的maindexlist.txt内容。



本人的代码也是基于2.2.0版本去做的(1.3.1的太久了,找不到了,OMG),源码地址在:xuwakao的github。



结束语



下面还有些问题是需要注意的:

1.xml布局文件如果使用到某些自定义类,最好是在程序中引用一下,不然,无法分析出依赖,或者直接把该类添加到依赖分析的inputfile中;

2.一级页面中通过反射调用的类,也要添加到依赖分析的inputfile中;

3.manifest中的receiver最好都添加到依赖分析的,因为receiver有可能拉起App;

4.参考美团实现中,hackInstrumentation的过程中发现,可能存在兼容性问题(实际测试了十多款手机,只有在小米2s上出现问题)。例如,在小米系统(api=16,4.1.1)上,重载Instrucmentation的execStartActivity不被调用,发现Activity的mInstrucmentationfield的类根本就不是Instrucmentation,所以导致没调用,甚至于用instanceof判断该对象是不是Instrumenttation对象都是true,简直不忍直视。。。证据:

献花(0)
+1
(本文系thedust79首藏)