精细优化APP启动
App启动是产品第一印象,但是基本所有业务线都需要在启动过程中掺和,加上三方库粗暴初始化,APP体积不断爆肥,主界面不断复杂,给APP启动性能持续加压。优化APP启动往往吃力不讨好,各业务线都不肯放弃“更早完成初始化”的机会。但从整个产品来说,让用户持续感觉到启动很慢(越来越慢),或者启动卡得要死,远比起启动之后APP的一些性能瑕疵更来得要命。优化APP启动需要数据,也需要抽丝剥茧。
总的来说APP有三个启动场景。1. 首次安装后启动; 2. 每次App进程如果不在内存中,用户打开App经历的冷启动; 3. 用户在某个界面切出,之后切回时APP进程还在内存,发生界面的热启动。这三个场景,热启动往往非常快,所以一般没有太大优化必要;冷启动经常发生,是重点;首次安装是面子工程,求精可做,而且有了冷启动优化的方案后,首装也比较容易。我们先来关注冷启动。
数据采集
目前,很多App普遍采取启动后首先进入一个欢迎界面(SplashActivity
)等待短暂时间后再进入主界面(一般3秒,期间展示推广内容,可掩盖加载)。此外,有些APP首装后有Guide或者登录,大致流程是 启动 -> Application -> SplashActivity -> LoginActivity/GuideActivity -> MainActivity
,而冷启动一般为 启动 -> Application -> SplashActivity -> MainActivity
。在这每个部件(Application/Activity)生命周期函数上都需要进行不同的初始化,比如Application阶段常见的multidex或者热更新库,各类功能三方SDK、控件等,下图展示了完整的启动过程:
要优化,只有一个单调的启动时间是远远不够的,业界还有录屏逐帧拆解时间的方法,用处有限,因为拆解无非让时间测量粒度变细,能够比较精确地获取每个界面上停留时间,但是换个手机结果完全不同,而且两次启动也可能完全不同。此外即使时间测准了,还是不能给开发排查出代码里面耗时的地方。优化,更需的是详细的数据指出启动过程中耗时的操作。此处可用插桩APK的方式接入Appetizer的数据收集功能,可以全面收集APP启动过程中的各种耗时问题,包括网络请求(http/https)、主线程卡顿、主线程耗时操作等。有些人说简单onXXX打些点,okhttp加个intercetpor输出logcat就够了,这些方法(还不考虑维护成本、发版去掉和二次数据分析等)主要针对的是垂直业务;而插桩的话是水平一刀切,对三方SDK和所有业务一视同仁,便于排查不同业务线以及三方库交错的各种异步以及同步请求。首次使用插桩参考:
在Appetizer中提供的APP测试方法中,有"APP启动时间测试"专项,选好设备和插桩过的APK就可以开始,按照步骤来就好。注意测量冷启动的时候APP要安装好,然后过教程并且登录好,确保下次启动能直接进入主界面,然后彻底关掉APP。测试后会出一个报告,在“数据分析报告”里面查看,与启动有关的测试产生的报告会包括统计信息、最慢的HTTP请求和详细加载过程等。
案例分析
本次以一个电商类APP为案例,分析了冷启动和首次安装启动两个场景,探讨如何分析问题与优化:
用Appetizer插桩后,测试了首装和冷启动两项,稳定的WiFi,中高端的手机上,先来看看冷启动的统计信息:
高亮的几个点,冷启动需要6.7秒,略长;32次网络请求不少,938ms最慢的网络请求,待会需要详细排查下;此外,主线程卡顿表现尚可,启动场景主要是网络请求方面耗时。来看一下详细的加载过程:
创建进程和Application上花了3.8秒,此处被Splash掩盖,3秒倒数,时间上尚可。看了一下HTTP请求,标出四个,为某网络性能监控库发出,作为一个辅助性的库,过于主动;此处出现了本次启动里面最慢的请求,就是938ms的png图片请求,排查后发现是广告大图,1.6MB PNG,优化可以考虑让client端请求不同分辨率的PNG,可以节省不少。接着是主界面:
在不到3秒的时间内,洋洋洒洒17个请求,却只有4次是业务请求,紫色方块(4次)标示了APP自己的打点,红色(4次)为一个用户行为分析的库的打点;蓝色(4次)为请求同一个静态图片;紫色的4次打点(3秒内如此密集。。。),完全可以写入内存缓存,然后postRunnable一个5秒之后,把5秒内所有打点内容一起推送服务端,代码改动不过十几行;而红色的4次,会怼三方库;蓝色的四次完全一样的静态图片请求,显然是没有开启client端HTTP缓存,打开即可;这些简单的优化下,立即能够减少6次不必要的网络请求;再来看一下首装启动:
请求数比冷启动的33次多了11次达到了44次!数据量惊人57.5MB!而最慢的一个HTTP请求居然达到了6秒。。。此外,在首装启动过程中发现一次主线程卡顿,仔细来看一下:
这两个标记为红色的HTTP请求的罪魁祸首是首屏广告用的是腾讯TBS (Tencent Browsing Service,就是腾讯自己的WebView)。TBS会自动更新,在APP第一次安装的时候,检测到了更新,下载了两个(不知道为啥是两次,貌似是同一个文件)20多M的tbs更新文件,下图的流量分析中也指出问题在协议流量上这两次巨量请求。
从TBS角度出发,APP启动时自动检查更新并自动更新并没有错,但是从APP的角度就意味着每一个新安装的用户都会被迫在第一次启动的时候更新一波TBS,为的是显示一下APP首屏几百K的广告,实在不值得,我会直接用原生的WebView做就好了,让自动更新来得更晚一点。最后排查一下这个主线程卡顿:
从下往上看,可以得知是在HttpRequest.onPostExecute()
时候,也就是某次协议请求完成后,setHomeTabs
设置主界面的Fragments,然后创建 HomeFragment.onCreateView
的时候构造View耗时,相应的解决方法是去给HomeFragment的Layout文件进行一定的瘦身。
总结:
- 启动过程有不必要的网络请求,client-side cache和合并打点可以减少30%的不必要请求
- 启动过程辅助性的SDK太积极使用网络
- 首次安装启动会更新TBS,不值得,拖慢网速影响整个APP首次启动
- HomeFragment常见的Layout过胖问题
常见误区:
- HTTP请求都是在其他线程上跑的,主线程没卡啊
- 虽然是异步,但是界面是等待业务的HTTP请求完成才能完成加载展示的
- 我看到很多HTTP请求挺快的,好像没必要去掉
- 最头疼的就是小而多的请求,因为请求虽然是异步的,但是从APP到系统层有各种排队限制导致同时能做的网络请求数量有限(dns, tcp连接数等等),所以异步!=并行,异步!=快