App启动优化

321 阅读6分钟

启动分析

启动过程分析

image.png

  • T1预览窗口显示。系统在拉起微信进程之前,会先根据微信的Theme属性创建预览窗口。当然如果我们禁用预览窗口或者将预览窗口指定为透明,用户在这段时间依然看到的是桌面。

  • T2闪屏显示。在微信进程和闪屏窗口页面创建完毕,并且完成一系列inflate view、onmeasure、onlayout等准备工作后,用户终于可以看到熟悉的“小地球”。

  • T3主页显示。在完成主窗口创建和页面显示的准备工作后,用户可以看到微信的主界面。

  • T4界面可操作。在启动完成后,微信会有比较多的工作需要继续执行,例如聊天和朋友圈界面的预加载、小程序框架和进程的准备等。在这些工作完成后,用户才可以真正开始愉快地聊天。

启动问题分析

  • 问题1:点击图标很久都不响应

如果我们禁用了预览窗口或者指定了透明的皮肤,那用户点击了图标之后,过了几秒还是停留在桌面,看起来就像没有点击成功,这在中低端机中更加明显。

  • 问题2:首页显示太慢

现在应用启动流程越来越复杂,所有准备工作都需要集中在启动阶段完成。

  • 问题3:首页显示后无法操作。

启动优化

优化工具

Traceview性能损耗太大,得出的结果并不真实;Nanoscope非常真实,不过暂时只支持Nexus 6P和x86模拟器,无法针对中低端机做测试;Simpleperf的火焰图并不适合做启动流程分析;systrace可以很方便地追踪关键系统调用的耗时情况,但是不支持应用程序代码的耗时分析。

在卡顿优化中提到“systrace + 函数插桩”似乎是比较理想的方案,而且它还可以看到系统的一些关键事件,例如GC、System Server、CPU调度等。

优化方式

分为闪屏优化、业务梳理、业务优化、线程优化、GC优化和系统调用优化。

  • 闪屏优化

今日头条把预览窗口实现成闪屏的效果,这样用户只需要很短的时间就可以看到“预览闪屏”。这种完全“跟手”的感觉在高端机上体验非常好,但对于中低端机,会把总的的闪屏时间变得更长。

微信做的另外一个优化是合并闪屏和主页面的Activity,减少一个Activity会给线上带来100毫秒左右的优化。但是如果这样做的话,管理时会非常复杂,特别是有很多例如PWA、扫一扫这样的第三方启动流程的时候。

  • 业务梳理

通过梳理之后,剩下的都是启动过程一定要用的模块。这个时候,我们只能硬着头皮去做进一步的优化。优化前期需要“抓大放小”,先看看主线程究竟慢在哪里。最理想是通过算法进行优化,例如一个数据解密操作需要1秒,通过算法优化之后变成10毫秒。退而求其次,我们要考虑这些任务是不是可以通过异步线程预加载实现,但需要注意的是过多的线程预加载会让我们的逻辑变得更加复杂。

业务优化做到后面,会发现一些架构和历史包袱会拖累我们前进的步伐。比较常见的是一些事件会被各个业务模块监听,大量的回调导致很多工作集中执行,部分框架初始化“太厚”,例如一些插件化框架,启动过程各种反射、各种Hook,整个耗时至少几百毫秒。

  • 线程优化

当然我们也希望每个线程都开足马力向前跑,而不是作为接力棒。所以线程的优化主要在于减少CPU调度带来的波动,让应用的启动时间更加稳定。

从具体的做法来看,线程的优化一方面是控制线程数量,线程数量太多会相互竞争CPU资源,因此要有统一的线程池,并且根据机器性能来控制数量。

线程切换的数据我们可以通过卡顿优化中学到的sched文件查看,这里特别需要注意nr_involuntary_switches被动切换的次数。

proc/[pid]/sched:
  nr_voluntary_switches:     
  主动上下文切换次数,因为线程无法获取所需资源导致上下文切换,最普遍的是IO。    
  nr_involuntary_switches:   
  被动上下文切换次数,线程被系统强制调度导致上下文切换,例如大量线程在抢占CPU

image.png

  • GC优化

我们可以通过systrace单独查看整个启动过程GC的时间。

python systrace.py dalvik -b 90960 -a com.sample.gc

不知道你是否还记得我在“内存优化”中提到Debug.startAllocCounting,我们也可以使用它来监控启动过程总GC的耗时情况,特别是阻塞式同步GC的总次数和耗时。

// GC使用的总耗时,单位是毫秒
Debug.getRuntimeStat("art.gc.gc-time");
// 阻塞式GC的总耗时
Debug.getRuntimeStat("art.gc.blocking-gc-time");

如果我们发现主线程出现比较多的GC同步等待,那就需要通过Allocation工具做进一步的分析。启动过程避免进行大量的字符串操作,特别是序列化跟反序列化过程。一些频繁创建的对象,例如网络库和图片库中的Byte数组、Buffer可以复用。如果一些模块实在需要频繁创建对象,可以考虑移到Native实现。

Java对象的逃逸也很容易引起GC问题,我们在写代码的时候比较容易忽略这个点。我们应该保证对象生命周期尽量的短,在栈上就进行销毁。

  • 系统调用优化

通过systrace的System Service类型,我们可以看到启动过程System Server的CPU工作情况。在启动过程,我们尽量不要做系统调用,例如PackageManagerService操作、Binder调用等待。

在启动过程也不要过早地拉起应用的其他进程,System Server和新的进程都会竞争CPU资源。特别是系统内存不足的时候,当我们拉起一个新的进程,它可能会触发系统的low memory killer机制,导致系统杀死和拉起(保活)大量的进程,从而影响前台进程的CPU。

参考: