一、应用启动概述
应用启动的一般流程
应用的启动,从桌面点击应用图标到主界面用户可以操作,大致遵循以下流程:
可以看到在应用启动过程中,最重要的两个进程就是SystemServer和App Process。其职责划分如下:
- SystemServer负责应用的启动流程调度、进程的创建和管理、窗口的创建和管理(StaringWindow和AppWindow)等
- 应用进程被SystemServer创建后,进行一系列化的进程初始化、组件初始化、主页面的构建和内容填充等。
应用启动类型
- 冷启动:当启动应用时,后台没有该应用的进程,系统会重新创建一个新的进程分配给该应用,然后再根据启动的参数,启动对应的进程组件。
- 场景:开机后第一次启动应用 或 应用被杀死后再次启动
- 生命周期:Process.start->Application创建->attachBaseContext->onCreate->onStart->onResume->Activity生命周期
- 启动速度:在几中启动类型中最慢,也是我们优化启动速度最大的拦路虎。
- 温启动:当启动应用时,后台已有该应用的进程。
- 场景:应用已经启动,返回键退出
- 生命周期:onCreate->onStart->onResume->Activity生命周期
- 启动速度:较快
- 热启动:当启动应用时,后台已有该应用的进程??梢栽谌挝窳斜碇锌吹?。
- 场景:Home键最小化应用
- 生命周期:onResume->Activity生命周期
- 启动速度:快
冷启动的流程
- 系统需要做的事:
- 加载及启动app
- app启动后立即显示一个空白的预览窗口
- 创建app进程
- 系统完成app进程创建后,app进程的工作:
- 创建Application对象
- 创建并启动主线程ActivityThread
- 创建启动以一个Activity
- inflate view
- 布局屏幕
- 执行第一次绘制
一旦app进程完成了第一次绘制工作,系统进程就会用main activity替换前面显示的预览窗口,这时,用户就可以正式开始与app的交互了。
从冷启动的流程看,我们无法干预app进程创建等系统操作,我们能干预的有
- 启动窗口
- Application生命周期回调
- Activity生命周期回调
下面介绍一下一般App中可以从哪些方面进行优化:
启动窗口优化
启动窗口,也叫启动页、SplashWindow、StartingWindow等,指的是应用启动时候的预览窗口。iOS App 强制有一个启动页,用户点击桌面 App 图标之后,系统会立即显示这个启动窗口,等 App 主页加载好之后再显示主页面。Android 也有类似的机制 (启动窗口这个是 Android 系统提供的),但是也提供了一个接口,让应用开发者设置是否显示这个启动窗口(默认是显示),部分开发者会把这个系统提供的启动窗口禁掉,启动自己的窗口。
但是启动自己的窗口需要的时间要比直接显示系统的启动窗口所花的时间要长,这就会导致用户在使用的时候,点击图标启动 App 的时候,有一定的延迟,表现在点击图标过了一段时间才进行窗口动画进入 App,我们要尽量避免这种情况。
- 不要禁止系统默认的启动窗口:即不要在主题里面设置android:windowDisablePreview = true
- 自己定制启动窗口的内容,比如将启动页主题背景设置成闪屏图片,或尽量使闪屏页面的主题和主页一致??梢圆慰贾?、抖音的做法
- 合并闪屏和主页面的Activity:微信的做法,不过由于微信设置了 android:windowDisablePreview , 且他在各个厂商的白名单里面,一般不会被杀,冷启动的机会比较少。不过也是一个可以思考的方式。
线程优化
线程优化主要减少CPU调度带来的波动,让启动时间更稳定。如果启动过程中有太多的线程一起启动,会给CPU带来非常大的压力,尤其是比较低端的机器。过多的线程同时跑会让主线程的 Sleep 和 Runnable 状态变多, 增加了应用的启动速度,优化的过程中要注意:
- 控制线程数量,使用线程池
- 检查线程间的锁,防止依赖等待
- 使用合理的启动架构
- 微信内部使用的mmkernel
- 阿里Alpha
系统调度优化
应用启动的时候,如果主线程的工作过多,也会造成主线程过于繁忙,下面是几个系统调度相关的点需要注意:
- 启动过程中减少系统调用,避免与AMS、WMS竞争锁。 启动过程中本身 AMS 和 WMS 的工作就很多,且 AMS 和 WMS 很多操作都是带锁的,如果此时 App 再有过多的 Binder 调用与 AMS、WMS 通信,SystemServer 就会出现大量的锁等待,阻塞关键操作。
- 启动过程中不要启动子线程,如果好几个进程同时启动,系统负担则会加倍,SystemServer 也会更繁忙。
- 启动过程中除了Activity之外的组件启动要谨慎,因为四大组件的启动都是在主线程的,如果组件启动慢,占用了 Message 通道,也会影响应用的启动速度。
- Application和主Activity的onCreate中异步初始化某些代码
GC优化
启动过程中减少GC的次数
- 避免进行大量的字符串操作,特别是序列化和反序列化
- 频繁创建的对象考虑复用
- 转移到Native实现
IO优化
启动过程中不建议进行网络IO,对磁盘IO要细扣,要清楚启动过程中读了什么文件、多少个字节、 Buffer 是多大,使用了多长时间、在什么线程等一系列信息。
资源重排
利用Linux的IO读取策略,PageCache和ReadAhead机制,按照读取顺序重新排列,减少磁盘IO次数 。具体操作可以参考支付宝 App 构建优化解析:通过安装包重排布优化 Android 端启动性能这篇文章。
利用文件重布局结合Pagecache 机制可以减少启动过程中的真正 IO 的次数,简单的说,通过文件重布局的目的,就是将启动阶段需要用到的文件在 APK 文件中排布在一起,尽可能的利用 pagecache 机制,用最少的磁盘 IO 次数,读取尽可能多的启动阶段需要的文件,减少 IO 开销,从而达到提升启动性能的目的。
类重排
类重排的实现通过 ReDex 的 Interdex 调整类在 Dex 中的排列顺序。Interdex 优化不需要去分析类引用,它只需要调整 Dex 中类的顺序,把启动时需要加载的类按顺序放到主 dex 里,这个工作我们完全可以在编译过程中实现,而且这个优化可以提升启动速度,优化效果从 facebook 公布的数据来看也比较可观,性价比高。具体实现可以参考 Redex 初探与 Interdex:Andorid 冷启动优化
主页面布局优化
应用主界面布局优化是老生常谈了,综合起来无非就是下面两点,这个需要结合具体的界面布局去做优化,网上也有比较多的资料可以查阅。
- 通过减少冗余或者嵌套布局来降低视图层次结构
- 用 ViewStub 替代在启动过程中不需要显示的 UI 控件
- 使用自定义 View 替代复杂的 View 叠加
闲时调用
IdleHandler:当 Handler 空闲的时候才会被调用,如果返回 true, 则会一直执行,如果返回 false,执行完一次后就会被移除消息队列。比如,我们可以将从服务器获取推送 Token 的任务放在延迟 IdleHandler 中执行,或者把一些不重要的 View 的加载放到 IdleHandler 中执行
类加载优化
可以在 systrace 生成的文件中看到 verifyClass 过程,因为需要校验方法的每一个指令,所以是一个比较耗时的操作。
App瘦身
App 瘦身包括代码瘦身和资源瘦身,通常的做法如下:
- Inspect Code :Android Studio 提供的代码审查工具,实际上是内嵌了 Lint。
- 代码混淆
- 图片格式的选择:对图片的要求不高,可以换成RGB_565编码格式的图片,也可以对图片本身通过工具压缩后再引入app项目中
- 接入资源混淆
- 减少dex数量
选择合适的启动框架
启动优化整个流程的梳理,还要做一些进程的判断,要判断某些项目是不是要在主进程里加载,某些要在初始进程里面加载??梢圆慰继员Φ娜绰酚呕陌咐?a target="_blank">历时1年,上百万行代码!首次揭秘手淘全链路性能优化(上)
启动网络链路优化
问题和优化点
- 发送处理阶段:网络库bindService影响前x个请求,图片并发限制图片库线程排队
- 网络耗时:部分请求响应size大,包括 SO文件,Cache资源,图片原图大尺寸等
- 返回处理:个别数据网关请求json串复杂解析严重耗时(3s),且历史线程排队设计不合适
- 上屏阻塞:回调UI线程被阻,反映主线程卡顿严重。高端机达1s,低端机恶化达3s以上
- 回调阻塞:部分业务回调执行耗时,阻塞主线程或回调线程。
优化
- 多次重复的请求,业务方务必收敛请求次数,减少非必须请求。
- 数据大的请求如资源文件、so文件,非启动必须统一延后或取消。
- 业务方回调执行阻塞主线程耗时过长整改。我们知道,肉眼可见流畅运行,需要运行60帧/秒, 意味着每帧的处理时间不超过16ms。针对主线程执行回调超过16ms的业务方,推动主线程执行优化。
- 协议json串过于复杂导致解析耗时严重,网络并发线程数有限,解析耗时过长意味着请求长时间占用MTOP线程影响其他关键请求执行。推动业务方handler注入使用自己的线程解析或简化json串。
预加载
Activity 打开之前就预加载数据,在 Activity 的 UI 布局初始化完成后显示预加载的数据,大大缩短启动时间。 可以参考这个示例:预加载:页面启动速度优化利器
?;?/h4>
?;?,是应用开发者的噩梦,也是Android厂商关注和大吉的重点。不过从启动的角度来看,如果应用进程不被杀,那么启动自然就快了,所以保活对应用启动速度也是有极大的帮助。
当然这里说的?;?,并不是建议大家用各种黑科技、相互唤醒、通知轰炸这种?;钍侄危翘峁┱嬲墓δ?,能让用户觉得你在后台是合理的、可以接收的。比如在后台的时候,资源能释放的都释放掉,不要一直在后台做耗电操作,该停的服务停掉,该关的动画关掉。
当然对于应用开发者来说,上面说的都太多理想化了,而且目前的手机厂商也会很暴力,应用到了后台就会处理掉,不过这毕竟是一个方向,Google 也在规范应用后台行为和规范厂商处理应用这两方面都在做努力,Android 系统的生态,还是需要应用开发者和 Android 厂商一起取改善。
当然?;罨褂幸惶趼肪褪亲吒痰暮献?,优化后台内存、去掉重复拉起、去掉流氓逻辑、积极响应低内存警告,做好这些话后可以跟系统厂商联系,谈放到查杀白名单和自启动白名单的可行性
业务梳理
这里涉及到具体的业务,每个 App 都不一样,但是所要做的事情都是一样的,下面是邵文在高手课里面提到的:
- 梳理清楚启动过程中的每一个??椋男┦且欢ㄐ枰?,那些是可以砍掉,那些是可以懒加载的。
- 根据不同的业务场景决定不同的启动模式。
- 懒加载防止集中化
可以把具体的业务分为下面四个维度:
- 必要且耗时:启动初始化,考虑用线程来初始化
- 必要不耗时:首页绘制
- 非必要但耗时:数据上报、插件初始化
- 非必要不耗时:不用想,这块直接去掉,在需要用的时再加载
业务优化
- 优化业务中的代码效率,抓大放小,先从比较明显的瓶颈处下手,逐步进行优化;
- 历史债务要偿还,历史的代码要重构,不能一直拖着
具体的业务会有具体的优化场景,比如:
- 数据库及IO操作都移到工作线程,并且设置线程优先级为THREAD_PRIORITY_BACKGROUND,这样工作线程最多能获取到10%的时间片,优先保证主线程执行
- 流程梳理,延后执行;实际上,这一步对项目启动加速最有效果。通过流程梳理发现部分流程调用时机偏失等, 例如
- 更新等操作无需在首屏尚未展示就调用,造成资源竞争
- 调用了IOS为了规避审核而做的开关,造成网络请求密集
- 自有统计在Application的调用里创建数量固定为5的线程池,造成资源竞争
- 修改广告闪屏逻辑为下次生效
- 去掉无效或未被执行的代码
- 去掉调用三方SDK里或者Demo里的多余代码
- 信息缓存,常用信息只在第一次获取,之后从缓存中取
- 项目是多进程架构,只在主进程执行Application的onCreate()
减少Activity的跳转层次
StartingWindow 会在用户点击 App 后立即创建并显示(前提是 App 没有禁止 StartingWindow),在 AppWindow 创建好之后,StartingWindow 消失,AppWindow 显示
- 默认 App 的启动窗口流程:StartingWindow(SystemWindow)——>MainActivity(AppWindow)
- 大部分三方App的启动流程:StartingWindow(SystemWindow) ——> SplashActivity(AppWindow)——> MainActivity(AppWindow)
- 糟糕一点的启动流程:StartingWindow(SystemWindow) ——> MainActivity(AppWindow) ——> SplashActivity(AppWindow)——> MainActivity(AppWindow)
- 更糟糕的启动流程(去掉了 StartingWindow):SplashActivity(AppWindow)——> MainActivity(AppWindow)
其实对用户来说,第一种启动流程是最好的,只涉及到一次窗口的切换;但是部分 App 由于广告页的需求,会使用第二种流程 ;但是尽量不要使用第三种和第四种启动流程,体验非常不好。
厂商优化
除了 App 自身的优化之外,Android 框架对应用启动也是非常关注的,做了比较多的优化,下面简单说一下思路,各个厂商的实现也不太一样,但是基本上都会有,有些是硬核代码优化,有的是利用系统策略做优化。厂商的策略各不相同,这里只是简单的提一下思路。
PreFork
Android Q 加入了 PreFork 机制,会先 fork 几个空进程,当 App 启动的时候,可以直接复用这几个空进程,而不用重新去 fork