App的启动实际上设计到体验性问题,特别在一些用户常用的手机,如微信、支付宝等使用评率频繁,如果应用不存在后台,也是重新启动,既冷启动的情况下,启动速度如果很慢,那对用户来说是无法忍受的,因此对App的启动就显得有必要。
Multidex多dex加载优化
Android4.4及以下使用的是Dalvik虚拟机,应用在安装的时候已经对主dex进行dex opt优化了,dexopt的作用主要对dex文件进行verification和optimization,对dex文件的字节码进行优化。而之所以加入Multidex,主要是在进行dex文件方法数的限制,一个dex文件的方法数定义的类型为short类型,最大只能支持到65535,因此,若一个dex文件超出改方法数就会导致方法找不到,不在范围内。而Multidex既是谷歌用于解决方法数超出的额问题。
MultiDex做了多dex处理,在首次启动app的时候,才对除了主dex之外的dex进行dexopt处理。对其他的子dex进行dexopt处理是在application初始化的时候执行的,而既对应的application是能被dexclassloader加载的,因此在应用安装的时候,相关的application类是放进主dex中并进行了dexopt优化的。查找类的过程涉及的类如下图,因此,在dexopt过程中,主要涉及的类就是DexPathList这个类中,通过循环遍历数组dexElments,从DexFile的loadClassByBinartName方法中加载相关的类,接下来我们分析MultiDex是怎么处理的时候就可以看到。
MultiDex处理多dexopt的过程如下:
这个过程是在主线程进行的,而耗时操作分别有提取dex、压缩dex为zip文件以及dexopt操作。而其中提取dex的过程中,是使用循环遍历除main dex之外的其他dex,以及在opt dex,既makeDexElements方法中,也是循环遍历文件进行dex opt的操作。因此,主要的优化点就在这几步中。一般的,若不支持到2.3的话,程序没必要经过压缩为zip文件,压缩为zip文件主要是因为向下兼容2.3的。
至于单向的循环遍历操作,可以并发解压抽取apk中dex,使用FutureTask,可以等到所有的dex解压抽取完成,这里dex需要归并成满足CPU个数的线程个数,充分利用资源的情况下,保证dex解压后是不变的。
而在makeDexElements中,同样也是单向的循环对提取出来的dex文件进行opt操作的,opt dex是在进程中进行的,同样的,仍然充分利用资源,并发济宁dex opt操作。
经过这些优化之后,App 多dex的加载从400ms降到200ms左右。
App启动过程相关初始化优化之线程治理
在App的启动的时候,为了避免在主线程中做太多影响主线程运行的操作,我们会把一些逻辑放到子线程中进行,一般情况下可能会存在开启多个子线程,然后线程之间有先后的顺序,这在应用程序启动的时候是经常见到的。如下图,任务从开始Task开始,TaskC在执行之前,需要依赖TaskA和TaskB执行完成,同样,TaskD也需要TaskC和TaskD完成,Project在继承关系上集成Task的,其实也是一种Task,只不过一个Project会包含多个Task,其实就是嵌套的关系。这种关系实际上就类似PERT模型,是一个有向的多线图,一个Task得等前面多个线程完成才能执行。若当前Task后面没有紧跟具体任务Task,那么就直接运行结束Task。数据结构其实相对简单,使用双向的链表,用于记录前向所有的Task以及后向紧跟的Task。结合链表的添加和移除来判定是否执行当前Task,如前向Task列表为空,则立即执行当前Task,如后向Task列表为空,则立即执行结束(Finish)Task。
在我们App启动阶段,我们可能需要读取各种SharedPreference文件、可能需要初始化一些第三方apk,可能希望能提前请求加载首页的数据,可能希望读取服务配置,用于更新业务需要的配置,如此种种,我们可以根据具体需要在应用启动的时候,可以在子线程进行。而主线程会通过发送消息的方式等待子线程初始化完成,最后归拢到主线程进行页面刷新等。
通过启动时,把一些操作交给子线程处理,从而能高效启动App,而这一部的优化而使得App的启动时间从5369ms减少到3739ms。
减少GC
在我们启动过程中,我们需要查看下这个过程是否发生了GC,因为GC过程中,会存在短时的除了GC回收线程之外的其他线程进入睡眠,另外还存在锁堆的情况,这些都能造成性能变慢的原因。GC的判断是需要分配的内存和已分配的内存达到一定的范围。和Java堆相关的三个参数分别为堆最小空闲值、堆最大空限值以及堆目标利用率。通过这三个值来使得GC的次数达到最小。因此,在App初始化的时候,尽量减少大堆对象的使用,减少不必要的局部对象。
此外,在启动的时候,我们要充分保证主线程能尽量占有CPU,从而让主线程能正常执行页面加载刷新操作。因此,有必要注意IO相关的操作,尽量用空间换时间、减少SP线程和数据库线程操作占用的CPU时间、资源文件的读写、数据读写,以及优化类的加载过程等来提升性能。
减少IO操作
启动过程避免不了涉及IO的操作,资源的加载、类的加载、数据库操作、文件加载等等。IO的优化对启动的影响至关重要。尽量减少IO的重复读取磁盘,充分利用pagecache缓存、减少pagecache污染,用最少的磁盘IO,读取尽可能多的文件,因为磁盘IO读取以block为单位,一般block的大小为4KB、也就是说一次磁盘IO读取至少4KB,因此要充分利用4kB的空间大小,而我们知道,类加载是我们经常需要用到的,那么在启动阶段如果对经常用到的类进行重排,而类的大小一般又比较小,那么一来可以在没有page cache命中鹅情况下,减少磁盘IO次数,二来为更多的类加载提供命中page cache的可能。使用的方式就是在findclass中使用如下的方式:
然后我们利用Facebook的Redex添加如上的配置的文件,如下:
同样的,我们也需要对文件进行重排,从而减少磁盘IO,那么在启动阶段主要也是在研发阶段统计存在资源(如dtawable、layout、assets等)的调用顺序,形成一个调用顺序文件,然后度量相关的文件哪些是命中了page cache,哪些是真是磁盘IO的,这样我们就可以结合Redex工具来进行重排列,当然,每次要手动去处理哪些需要重排序会有点低效,最好做成自动化,自动形成重排序文件。
优化类加载过程
类的加载机制包括加载、验证、准备、解析、初始化,其中验证阶段需要校验方法的每个指令、是一个比较耗时的操作,通过Hook去掉验证环节。
总之,在启动的时候,我们要充分保证主线程能尽量占有CPU,从而让主线程能正常执行页面加载刷新操作。因此,有必要注意IO相关的操作,尽量用空间换时间、减少SP线程和数据库线程操作占用的CPU时间、资源文件的读写、数据读写,以及优化类的加载过程等来提升性能。