如何在2个月内实现Android启动速度翻倍

随着闲鱼App端更多新功能、新技术的加入,应用冷启动速度越来越慢,这也意味着用户看到有效内容的时间被拉长,对用户体验有着很大的伤害。目前,在内部测试版本中,我们已经将安卓在低端机上的冷启动时间从原来的10s降低到了5s内。

闲鱼是如何快速将启动时间减少一半的呢?分为 建立标准分析现状抓大放小三个步骤。

建立标准

做性能优化不是讨论哲学问题,建立合理的数据衡量标准非常重要。尽管已经有了很多关于如何卡口关键函数、如何判断页面第一帧渲染完成的讨论,但从代码层面进行判断始终与用户的感知无法100%匹配。如何迅速建立起启动时间的标准?我们借鉴了手淘的方式和标准,利用内部的魔镜平台,使用视频关键帧的方式记录下App图标被点下到首页第一屏渲染完成作为一整个应用冷启动的过程。这与用户看到的启动过程吻合。

对于设备的选择上,我们使用y67这样一台现在看起来相对性能较差的机型作为优化的目标机型。低端机存在CPU能力弱,IO速度慢等问题,而慢代码与IO恰恰是拖慢应用启动最大的原因。定位优化的目标机型可以更加快速解决common类型的启动问题。

闲鱼现状

我们先使用日志打点的方式来统计启动过程中耗时的大头,以便可以快速得将启动性能提高上去。可以看到图中,进入首页渲染前, commoninteractive两部分占去了大部分的时间,这是启动器在执行启动任务。而在进入首页后,页面的请求与view的排版占用了大部分的时间。

基于上面的分析,第一阶段我们将”启动任务治理“和”首页渲染加速“作为快速提升启动时间的重点来优化。

启动任务优化

闲鱼的 Android 端在 16 年的时候上线了一个基于 DAG(有向无环图) 的启动器,它将启动任务编排为一个 DAG,并使用多核多线程并发的执行任务。上面说到的 commoninteractive属于启动器执行任务的两个阶段,它们都会让主线程等待阶段中的任务全部执行完,所以这两个阶段的任务,我们叫它 阻塞型任务

目前为止,整个闲鱼 Android 在启动阶段有 77 个任务需要执行,其中阻塞型任务有 61 个,y67 上的总执行耗时在 8s 以上,并发后需要将近 2.5s 的时间。

对于启动阶段阻塞型任务,最快的优化方式有三点:

  • 部分任务延迟执行
  • 降低任务本身的耗时
  • 拆分大任务

任务拆分与延迟执行

减少阻塞型任务的数量,是加速启动最直接的手段。这里需要根据任务的 DAG 进行依赖分析,能够无痛被延迟执行的任务最明显的特征就是”没有其他任务依赖于它“。如果任务之间有依赖,则需要根据后续首页对于模块的使用情况来决定是否将整个依赖链上的任务全部延迟。

闲鱼的首页金刚位大部分是 weex、web 和小程序的入口,另外首页也会用到端智能相关的功能。然而这四个 sdk 的初始化,普遍都在 300-500ms 左右,属于比较”硬核“任务。在将这四个任务移动到异步非阻塞阶段后,整个启动降低了 500ms(当然要设置最高优先级以保证用户尽量少的等待时间)。

非阻塞任务的触发时机

任务启动的时机就像跟女生表白一样,不是你想启动,启动就启动的。错误的时机大概率造成灾难性的后果。

在我们将几个大任务移动到非阻塞阶段后发现,如果阶段启动的时候首页还没开始渲染或者没有渲染完成,整个首页的渲染会变得非常缓慢,图片的加载也随之变慢。总之就是谁碰到谁倒霉。实测中,非阻塞阶段启动的时机会对首页的渲染产生将近 1s 左右的波动,使得启动时间不断在危险的边缘疯狂试探。

这是由于非阻塞阶段会在进入首页后的第一个 queueIdle回调之后触发。而它的执行占用了多过的系统资源,造成 CPU 占用、网络请求排队、IO 密集等问题。最终导致主线程、渲染变慢的情况。

那么什么时间才是启动非阻塞任务的合适时机呢?既然我们选择首页渲染为最高优先级,非阻塞任务的启动就必须排在后面。于是一咬牙一跺脚——”砍“!

我们让首页在确认 view 都上屏后,发信号给启动器。启动器这个时候才开始注册 queueIdle回调,并启动一个延迟 6s 的 runnable 作为”备份“,防止 message queue 过忙长时间无法触发非阻塞阶段的任务。

但这里有个矛盾点,首页上几大金刚位都是通往 weex、web 或者小程序的,如果用户点击这些页面比非阻塞阶段的触发更早,该怎么办呢?当然是原谅 触发 它啊!

这里我们采用的方式是,当这些功能被触发的时候,需要先去 check 需要的模块是否已经初始化完成。如果没有的话,check 非阻塞阶段是否已经启动。如果已启动,就进入等待,否则强制触发 (这个时候首页必然已经渲染完成了),并等待所需要的任务执行完成。

任务耗时治理

要快速治理,需要利用一些成熟的工具。可以先对任务中的每一行代码进行时间统计,筛选出执行时间较长的调用后,使用系统提供的 method trace进行更细粒度的分析。

既然是要快,那么一定是找通用类型的问题下手:

  • 对于 IO 出来的值,尽量做内存缓存,避免多次 IO
  • 避免产生大的 SharedPreference 文件,尽可能将对 commit 的调用换成 apply
  • 注意一些异步接口回调的线程,如果是主线程,也需要保证回调后的代码快速执行完

首页启动优化

优化前,闲鱼的首页需要先进行三个排队的网络请求,弹出广告页,接着进行动态模板的渲染与数据绑定,总消耗时间在 3.5s 以上,这里面还不包括图片上屏的时间。

闲鱼首页部分的启动优化,主要也从三个方面来做:

  • 广告页
  • 数据预加载
  • View 预创建

广告页优化

闲鱼之前的广告页的流程如下图:

先拉起启动页,然后启动页拉起首页,首页再拉起广告页,广告页起来先展示默认图,然后同时去做是否有广告的判断,然后再去做广告的展示,这个过程如果没有广告,也会让默认的广告页展示 3 秒钟再关闭。

这个过程显然是不合理的,广告有自己的疲劳期,那么在没有广告的时候,拉起广告页就是一个浪费。其次广告页作为一个 Activity 拉起,需要经历一些 IPC 的调用,整个操作也是比较重的。

基于这两点,我们在广告页这块,先在初始化的时候就做提前的资源拉取和预判,这样如果确实没有广告资源,那么广告页直接不做启动,节省启动资源。其次,我们将广告页由一个 activity,改造成一个全屏展示的 Dialog,进一步来节省广告拉起时资源消耗,让首页其他内容的加载有更充足的系统资源。

数据预加载

在性能优化中,空间换时间与提前预加载就像广为人知的”中间加一层“一样好用。

闲鱼首页必须的两个接口,冷启和热启接口耗时在 1 秒左右,而他们是在首页第一帧回调回来之后的时机才开始请求的。这里完全可以把请求的时机提前到初始化的过程中并行去做,从而为闲鱼启动 -1s。

于是我们设计了针对这种情况下的预取模块,在初始化的时候,就去做首页数据的预加载,整体的模块的时序如下:

这一步做完之后,本地机器测试结果大约节约了 950ms 的启动时间。

view 预创建

在解决完数据的问题之后,我们通过魔镜平台,会发现在 y67 上,首页展示之后,有大量的白屏的时间,view 的创建和渲染,在这里消耗了大量资源,并占用了很长的时间 (这里每一帧是 100ms),平均大概在 1400ms

于是我们自然而然的想到了在初始化的过程中去提前创建 view,但是如果是在初始化过程中的主线程去创建 view,那么势必会跟启动页和广告页等 ui 元素竞争主线程的使用,基本等于白干。

于是这里我们采用在子线程预先创建 view 并执行 mesure 与 layout 操作。等待首页渲染时,使用对应的 id 进行取出和使用。做完之后,会发现 view 的上屏时间,在 y67 上缩短到 600ms,减少了一倍的的时间:

总结与下一步优化

通过上面的方式,整个启动阶段的时间从 2.5s 降低到了 1.3s,降低了将近一倍的时间。另外启动任务所消耗的总时间从 8s 降低到了 3s。首页的渲染几乎达到了秒出。整体启动时间降低到了 4.5s 左右。

这个阶段主要是对启动过程中的任务与首页代码本身的优化。下一阶段,我们会对整个启动过程中的运行环境进行优化:

  • 对启动时候的资源消耗进行整理,减少不必要的网络请求与 IO 以及线程切换。
  • 对启动器中的线程负载进行优化,目前启动的任务分配方式距离理论上的最优值 (平均值) 大约还有 50% 的空间。
  • 使用 dex-relayout、PGO 加速启动
  • 发表于:
  • 本文为 InfoQ 中文站特供稿件
  • 首发地址https://www.infoq.cn/article/xBDxJ3QZ3eez0rTPHLTL
  • 如有侵权,请联系 yunjia_community@tencent.com 删除。

扫码关注云+社区

领取腾讯云代金券