作者:Gracker
原文链接:https://www.androidperformance.com/2019/11/18/Android-App-Lunch-Optimize/
本文参考了目前大部分 Android 应用启动优化的方案,将大家的方案做一个汇总,如果你有这方面的需求,只需要对照这篇文章,看看其他人的方案,查漏补缺。很多方案是要根据具体的业务去做优化的,所以这里也没有对每一种方案进行详细的介绍,要用到哪一个方案的时候,可以具体去网上查找对应方案的具体实现方法,这里只是做一个汇总
另外我还加上了部分系统厂商所做的启动相关的优化,不过只写了一些我知道的,还有一些厂商有黑科技,就不在这里的讨论范围了。知道厂商做的事情,可能也会帮助到你,比如联系厂商做白名单、接入厂商 SDK 等
应用的启动,从桌面点击应用图标到主界面用户可操作,大致遵循下面的流程:
可以看到应用启动过程中,最重要的两个进程就是 SystemServer 和 App Process . 其职责划分如下:
这里还需要引入冷启动和热启动的概念,这也是我们经常会碰到的两个概念
各家应该都有自己的方案,关键在于如何定义启动结束的点,这个也是一直困扰我的一个地方,有的应用很好定义,有的应用则因为比较复杂,无法直接衡量启动速度。像 adb 这种方法自己玩玩可以,生产环境没啥用;录屏本身就有性能损耗…
这里我建议大家学习历时1年,上百万行代码!首次揭秘手淘全链路性能优化(上)中提到的测量方法:自动化、稳定、持续集成
通过OCR提取图片中的文字信息作为关键特征。该算法的优势:1. 在于应用页面上基本都是有文字的, OCR也可以识别到图片上的文字, 文字出现则图片加载完成, 和用户体感是一致的;2. 文字作为特征,过滤掉了很多图片特征可能带来的噪声, 减少了算法调试的工作量;另外阿里集团内有非常成熟和优秀的OCR服务——读光,文档识别率超过99.7%, 使用水滴平台封装的OCR服务,可以快速接入和使用。最终的识别方案就是基于OCR识别来进行的
启动窗口,也叫启动页、SplashWindow、StartingWindow 等,指的是应用启动时候的预览窗口。iOS App 强制有一个启动页,用户点击桌面 App 图标之后,系统会立即显示这个启动窗口,等 App 主页加载好之后再显示主页面。Android 也有类似的机制 (启动窗口这个是 Android 系统提供的),但是也提供了一个接口,让应用开发者设置是否显示这个启动窗口(默认是显示),部分开发者会把这个系统提供的启动窗口禁掉,启动自己的窗口。
但是启动自己的窗口需要的时间要比直接显示系统的启动窗口所花的时间要长,这就会导致用户在使用的时候,点击图标启动 App 的时候,有一定的延迟,表现在点击图标过了一段时间才进行窗口动画进入 App,我们要尽量避免这种情况
线程优化主要是减少 CPU 调度带来的波动,让启动时间更稳定。如果启动过程中有太多的线程一起启动,会给 CPU 带来非常大的压力,尤其是比较低端的机器。过多的线程同时跑会让主线程的 Sleep 和 Runnable 状态变多, 增加了应用的启动速度,优化的过程中要注意:
应用启动的时候,如果主线程的工作过多,也会造成主线程过于繁忙,下面几个系统调度相关的点需要注意:
启动过程中减少 GC 的次数
可以参考下面这篇文章 支付宝客户端架构解析:Android 客户端启动速度优化之「垃圾回收」)
启动过程中负载比较高,有许多系统 IO 都在此时发生,这时候 IO 的性能下降会比较快,此时 App 中的 IO 操作会比平时更慢一些,尤其是在性能比较差的机器上。
IO 分网络 IO 和磁盘 IO ,启动过程中不建议进行网络 IO ,对于磁盘 IO 则要细扣,邵文在高手课里面有讲到:
下面图中可以看到低内存的时候,启动应用主线程有较多的 IO 等待(UI Thread 这一栏,橘红色代表 IO 等待 )
利用 Linux 的 IO 读取策略,PageCache 和 ReadAhead 机制,按照读取顺序重新排列,减少磁盘 IO 次数 。具体操作可以参考支付宝 App 构建优化解析:通过安装包重排布优化 Android 端启动性能 这篇文章
Linux 底层文件系统中 VFS 上次 App 进程之间,存在一层 pagecache,pagecache 由内存中的物理 page 组成,其内容对应磁盘上的 block。Pagecache 的大小是动态变化的,可以扩大,也可以在内存不足时缩小。Cache 缓存的存储设备被称为后备存储(backing store),一个 page 通常包含多个 block,这些 block 不一定是连续的
利用文件重布局结合Pagecache 机制可以减少启动过程中的真正 IO 的次数,简单的说,通过文件重布局的目的,就是将启动阶段需要用到的文件在 APK 文件中排布在一起,尽可能的利用 pagecache 机制,用最少的磁盘 IO 次数,读取尽可能多的启动阶段需要的文件,减少 IO 开销,从而达到提升启动性能的目的
类重排的实现通过 ReDex 的 Interdex 调整类在 Dex 中的排列顺序。Interdex 优化不需要去分析类引用,它只需要调整 Dex 中类的顺序,把启动时需要加载的类按顺序放到主 dex 里,这个工作我们完全可以在编译过程中实现,而且这个优化可以提升启动速度,优化效果从 facebook 公布的数据来看也比较可观,性价比高。具体实现可以参考 Redex 初探与 Interdex:Andorid 冷启动优化
应用主界面布局优化是老生常谈了,综合起来无非就是下面两点,这个需要结合具体的界面布局去做优化,网上也有比较多的资料可以查阅
IdleHandler:当 Handler 空闲的时候才会被调用,如果返回 true, 则会一直执行,如果返回 false,执行完一次后就会被移除消息队列。比如,我们可以将从服务器获取推送 Token 的任务放在延迟 IdleHandler 中执行,或者把一些不重要的 View 的加载放到 IdleHandler 中执行
可以在 systrace 生成的文件中看到 verifyClass 过程,因为需要校验方法的每一个指令,所以是一个比较耗时的操作。
App 瘦身包括代码瘦身和资源瘦身,通常的做法如下:
启动优化整个流程的梳理,流程的梳理,我们这里引入了一个有向无环图的概念,我们会把整个的概念梳理成有向无环图的结构,然后会去挨个加载。右边的部分,可以看到我们其实在启动的时候,首先会去加载一些必要的启动项,必要的启动项是左边流程,会用一个多进程的方式加载,以来有向无环图进行控制,比如说我是在非必须的时候启动加载我可以放在后面再去加载。当然在整个有向无环图的顺序加载,其实还是会做一些进程的判断,要判断某些项目是不是要在主进程里加载,某些要在初始进程里面加载
从 Spark 的 DAGScheduler 中领悟到它的核心思想,面向阶段调度(Stage-Oriented Scheduler):把应用划分成一个个的阶段(Stage),再把任务(Task)安排到各个阶段中去,任务的编排则是通过构建 有向无环图(DAG),把任务依赖通过图的方式梳理得 井井有条。因为它分阶段执行,先集中资源把阶段一搞定,再齐心协力去执行阶段二,这样即能控制拥塞,又能保证时序,还能并发执行,让设备性能尽可能得到发挥
[图片上传失败…(image-6f2fdf-1574833214431)]
大家可以参考淘宝的全链路优化的案例:历时1年,上百万行代码!首次揭秘手淘全链路性能优化(上)
Activity 打开之前就预加载数据,在 Activity 的 UI 布局初始化完成后显示预加载的数据,大大缩短启动时间。 可以参考 :https://github.com/luckybilly/PreLoader/blob/master/README-zh-CN.md
保活,是各个应用开发者的噩梦,也是 Android 厂商关注和打击的重点。不过从启动的角度来看,如果应用进程不被杀,那么启动自然就快了,所以保活对应用启动速度也是有极大的帮助。
当然这里说的保活,并不是建议大家用各种黑科技、相互唤醒、通知轰炸这种保活手段,而是提供真正的功能,能让用户觉得你在后台是合理的、可以接收的。比如在后台的时候,资源能释放的都释放掉,不要一直在后台做耗电操作,该停的服务停掉,该关的动画关掉。
当然对于应用开发者来说,上面说的都太多理想化了,而且目前的手机厂商也会很暴力,应用到了后台就会处理掉,不过这毕竟是一个方向,Google 也在规范应用后台行为和规范厂商处理应用这两方面都在做努力,Android 系统的生态,还是需要应用开发者和 Android 厂商一起取改善。
当然保活还有一条路就是走跟厂商的合作,优化后台内存、去掉重复拉起、去掉流氓逻辑、积极响应低内存警告,做好这些话后可以跟系统厂商联系,谈放到查杀白名单和自启动白名单的可行性
这里涉及到具体的业务,每个 App 都不一样,但是所要做的事情都是一样的,下面是邵文在高手课里面提到的:
可以把具体的业务分为下面四个维度(此处图文来自https://juejin.im/post/5c21ea325188254eaa5c45b1#heading-5)
然后按需进行加载优化
具体的业务会有具体的优化场景,大家可以参考这篇文章中的优化流程和优化项(https://www.jianshu.com/p/f5514b1a826c)
- 数据库及IO操作都移到工作线程,并且设置线程优先级为THREAD_PRIORITY_BACKGROUND,这样工作线程最多能获取到10%的时间片,优先保证主线程执行
- 流程梳理,延后执行;实际上,这一步对项目启动加速最有效果。通过流程梳理发现部分流程调用时机偏失等, 例如
** 1. 更新等操作无需在首屏尚未展示就调用,造成资源竞争
** 2. 调用了IOS为了规避审核而做的开关,造成网络请求密集
** 3. 自有统计在Application的调用里创建数量固定为5的线程池,造成资源竞争
** 4. 修改广告闪屏逻辑为下次生效- 去掉用无但被执行的老代码
- 去掉开发阶段使用但线上被执行的代码
- 去掉重复逻辑执行代码
- 去掉调用三方SDK里或者Demo里的多余代码
- 信息缓存,常用信息只在第一次获取,之后从缓存中取
- 项目是多进程架构,只在主进程执行Application的onCreate()
StartingWindow 会在用户点击 App 后立即创建并显示(前提是 App 没有禁止 StartingWindow),在 AppWindow 创建好之后,StartingWindow 消失,AppWindow 显示
StartingWindow(SystemWindow)
->MainActivity(AppWindow)
StartingWindow(SystemWindow)
-> SplashActivity(AppWindow)
-> MainActivity(AppWindow)
###糟糕一点的启动流程是这样的
StartingWindow(SystemWindow)
-> MainActivity(AppWindow)
-> SplashActivity(AppWindow)
-> MainActivity(AppWindow)
SplashActivity(AppWindow)
-> MainActivity(AppWindow)
其实对用户来说,第一种启动流程是最好的,只涉及到一次窗口的切换;但是部分 App 由于广告页的需求,会使用第二种流程 ;但是尽量不要使用第三种和第四种启动流程,体验非常不好
除了 App 自身的优化之外,Android 框架对应用启动也是非常关注的,做了比较多的优化,下面简单说一下思路,各个厂商的实现也不太一样,但是基本上都会有,有些是硬核代码优化,有的是利用系统策略做优化。
厂商的策略各不相同,这里只是简单的提一下思路
App 启动的时候,系统会对要启动的应用做绝对的资源倾斜,比如 CPU、IO、GPU 等,这一点大家抓个 Systrace 看一下即可,不管是频率还是调度算法,正在启动的 App 绝对是当时的系统 VIP 客户
部分厂商也提供了资源调度的 SDK ,应用可以接入这些 SDK,在需要资源的时候直接调用 SDK 获取
Android Q 加入了 PreFork 机制,会先 fork 几个空进程,当 App 启动的时候,可以直接复用这几个空进程,而不用重新去 fork
2,348K: usap32 (pid 18731)
2,346K: usap32 (pid 18702)
2,343K: usap32 (pid 18707)
2,342K: usap32 (pid 18729)
2,341K: usap32 (pid 18711)
2,335K: usap32 (pid 20322)
2,335K: usap32 (pid 20325)
2,333K: usap32 (pid 20319)
2,333K: usap32 (pid 20320)
2,333K: usap32 (pid 20321)
1,509K: usap64 (pid 21169)
1,509K: usap64 (pid 21180)
1,507K: usap64 (pid 21171)
1,452K: usap64 (pid 21513)
1,450K: usap64 (pid 21506)
1,449K: usap64 (pid 21512)
1,447K: usap64 (pid 21511)
1,445K: usap64 (pid 21514)
启动的时候,对启动过程中的 Message 进行重新排列
部分厂家会对启动过程 App 的主线程和渲染线程做特殊对待,比如让他们直接跑到大核上,将其他不重要的线程移到小核
部分场景会针对用户的使用习惯进行学习,比如在什么时间、什么场合、什么交通工具打开手机,系统会预测你要启动的 App,并在后台进行启动,这样你点击这个 App 的时候,就已经是热启动了
系统也会对一些应用进行特殊处理,以提升用户体验:包括但不限于 进程\线程优先级调整、查杀白名单、用户常用应用记录等,进行适当的后台保活,下次启动的时候就是热启动了
系统会对一些应用进行特殊处理,比如这个 App 比较重要但是不能杀掉,那么有的厂商会在这种应用退到后台之后,进行无感重启:比如说某个应用内存超标或者持续 Crash ,后台重启可以很好地解决这个问题,这样重启后的 App 是用户点击启动的时候就是热启动
部分应用启动的时候,需要大量的内存,比如现在的相机启动,这时候如果没有足够的内存,那么系统必须要通过杀掉很多应用、释放 Cache 等操作来给这个 App 让路,这个过程会使得这些大内存的 App 在启动的时候频繁进行内存操作,导致启动速度变慢
部分厂商会在监测到这种大内存 App 启动的时候,提前做内存的回收操作,这样在启动的时候,就有了足够的内存给这个 App 使用
Android 系统更新也会对应用启动速度进行优化,比如上面提到的 Pre-Fork,又比如这里的简化 doFrame 个数
推荐阅读:2019最新整理阿里Android面试失败大全之源码篇
2019最新整理BATJAndroid 高级面试题及答案
转自“写给全国移动互联网工作者的一封公开信”