前言
本章主要围绕 App 的启动流程如何优化进行讲解;
将启动优化,首先要了解的就是 app 的启动流程,只有清晰并完善的了解了 启动流程 才能更好的进行优化;
App 启动流程
在将 AMS 的时候,其实已经讲解了 App 的启动流程,感兴趣的可以翻看下我之前的文章;
这里我们贴一张启动流程图;
整体流程就是当我们点击桌面图标启动某一个应用层的时候,首先会在 Launcher 进程中通过 ActivityManagerProxy 跨进程通信发送 startActivtity 并代理到 system_server(AMS) 进程, AMS 发现这个 Activity 所在的进程已经存在,则直接启动这个 Activity,(这就是所谓的热启动),如果不存在,则通知 Zygote 进程 fork 出一个进程给目标 App 使用,并通知 AMS,由 AMS 来启动目标 Activity;而启动目标 Activity 之前,先启动 Application,在 Application 启动之后才会启动 Activity;
整体可以分为三个大的阶段
- 点击桌面 Launcher 的应用图标,通过与 AMS 通信,启动应用的过程;
- 应用 Application 执行过程;
- 启动 MainActivity 执行过程;
所以这里的启动其实是有三种状态的:冷启动、温启动、热启动
启动方式
冷启动
冷启动是指应用从头开始启动:系统进程在冷启动后才创建应用进程。发生冷启动的情况包括应用自设备启动后或系统终止应用后首次启动
热启动
在热启动中,系统的所有工作就是将 Activity 带到前台。只要应用的所有 Activity 仍驻留在内存
中,应用就不必重复执行对象初始化、布局加载和绘制
温启动
包含了在冷启动期间发生的部分操作;同时,它的开销要比热启动高。有许多潜在状态可视为温启动。例如:
用户在退出应用后又重新启动应用。进程可能未被销毁,继续运行,但应用需要执行
onCreate() 从头开始重新创建 Activity;
系统将应用从内存中释放,然后用户又重新启动它。进程和 Activity 需要重启,但传递到 onCreate() 的已保存的实例 state bundle 对于完成此任务有一定助益;
启动时长统计
Displayed
app启动完成之后,ActivityManager 会打印一个 Displayed 展示启动时间;
adb 命令
adb shell am start -s -w 「packageName/ .activityName」
- waitTime 总的耗时,包括前一个应用Activity pause的时间和新应用启动的时间
- thisTime 表示一连串启动Activity的最后一个Activity的启动耗时
- totalTime 表示新应用启动的耗时,包括新进程的启动和Activity的启动,但不包括前一个应用Activity pause的耗时;
插桩 + systrace
通过插桩,我们可以看到应用主线程和其他线程的函数调用流程;
CPU Profile
AS 之后,我们一般都是通过 CPU Profile 对 App 的启动耗时进行采样优化;
当我们需要对一个 app 进行采样的时候,我们需要在 Edit Configuration 中进行相关配置信息的打开
选择 Method Trace 之后,使用 profile 的方式启动 App
开启 trace 之后,采集一段时间(跳转到第一个页面之后)可以点击 stop 停止采集;
之后生成对应的 trace 信息;
其中橙色表示系统方法执行时间,绿色表示 app 方法执行时间,蓝色表示三方 sdk 执行时间;
每一个方法,x 轴越大,表示花费的时间越多,通过放大,可以看到我们每一个方法的执行时间,包括 Application 类加载等的创建时间
重点关注绿色区域,逐个的分支查看 app 中耗时的方法;
右侧区域分为四种分析方式:Summary、Top Down、Flame Chat、Bottom Up
Summary 并不是很方便的查看方法的细节;
我们切换到 Flame Chat(火焰图) 来看下:
这个就是和 Summary 反向的分析图,从下往上分析;
我们切换到 Top Down(主要分析耗时) 来看下:
Top Down 比较直观的看到每个方法的执行耗时,以及内部方法的执行耗时;
App 启动优化方式
根据启动流程的三大阶段,对应的三个阶段的优化方式;
第一阶段的优化 主要是桌面 Launcher 应用与 AMS 的交互,以及 AMS 启动应用的过程,这个阶段主要是系统 Framework 层在做,优化空间基本没有;
第二阶段的优化,对于 Application 的优化,主要包括三部分的优化,一是 attachBaseContext 的优化,二是 onCreate 回调方法的优化,三是应用执行到 MainActivity 之前的白屏处理;
第三阶段的优化,主要是第一个 Activity 运行的阶段,直到 Activity 执行完成 onResume 函数,对应的就是 onCreate、onStart、onResume 的优化;
应用执行到 MainActivity 之前的白屏处理
Activity 真正展示的是 Window,对应的唯一实例就是 PhoneWindow,也就是到 PhoneWindow 展示之后,用户才能看到实际的内容;
这个流程其实是:点击 Launcher 桌面图图标到第一个 Activity 的 PhoneWindow 展示之前,这个期间内应该展示什么来规避黑屏或者白屏的 case;
产生这个黑白屏的 case 其实跟我们设置的启动的第一个 Activity 的主题有关系,也就是 windowBackground 依赖这个 theme,如果 theme 设置的是白色主题,那么 windowBackground 默认就是白色,启动就会白屏;
根据流程图,我们进入 PhoneWindowManager 的 addSplashScreen 方法看下,黑屏开始的地方在哪里?
public StartingSurface addSplashScreen(IBinder appToken, int userId, String packageName,
int theme, CompatibilityInfo compatInfo, CharSequence nonLocalizedLabel, int labelRes,
int icon, int logo, int windowFlags, Configuration overrideConfig, int displayId) {
// 省略部分代码
// 黑白屏开始的地方,就是 addView 添加要显示的 View 的时候;
wm.addView(view, params);
return view.getParent() != null ? new SplashScreenSurface(view, appToken) : null;
}
那么,怎么优化这个黑白屏的问题,我们可以通过 PhoneWindowManager 的源码来看下:
private void addSplashscreenContent(PhoneWindow win, Context ctx) {
final TypedArray a = ctx.obtainStyledAttributes(R.styleable.Window);
final int resId = a.getResourceId(R.styleable.Window_windowSplashscreenContent, 0);
a.recycle();
if (resId == 0) {
return;
}
final Drawable drawable = ctx.getDrawable(resId);
if (drawable == null) {
return;
}
// We wrap this into a view so the system insets get applied to the drawable.
final View v = new View(ctx);
v.setBackground(drawable);
win.setContentView(v);
}
可以看到,PhoneWindowManager 通过获取 theme 中定义的 windowSplashscreenContent 来获取一个 drawable 设置给 PhoneWindow;
PS:这个 API 需要 API >= 26,如果最低版本小于 26 则还是通过 windowBackground 来设置;
那么,可能会有人有疑问了,这个 windowSplashscreenContent 属性比 windowBackground 强大在了哪些地方呢?
windowBackground 只能设置一张图片,而 windowSplashscreenContent 借助 Jetpack 的 SplashScreen 可以展示一个开屏动画;
attachBaseContext 优化
可以参考字节的 MutilDex 优化启动速度,核心是:去掉了 dex 转 zip 的操作,优化了启动速度,而不是多进程;
onCreate 优化
onCreate 方法中,如果使用 setContentView 方式,目前只能通过减少 xml 层级的方式来降低启动耗时,因为 setContentView 中充斥着大量的反射逻辑来创建 View;
所以,如果 xml 的绘制比较简单,建议使用 new View 的方式,通过 addView 来实现 View 的创建和绘制;
onResume 优化
如果页面布局是 ViewPager + Fragment 的方式,通常采用懒加载的方式,来进行页面渲染的优化;
布局层级的优化
使用 约束布局 替换普通的布局,优化渲染层级,减少绘制时长;
使用布局的异步加载 AsynLayoutInflater
AsyncLayoutInflater(this).inflate(R.layout.activity_debug, null, object : OnInflateFinishedListener{
override fun onInflateFinished(view: View, p1: Int, p2: ViewGroup?) {
setContentView(view)
}
})
但是 AsyncLayoutInflater 的使用也是有一些限制的,我们可以先尝试下能不能在实际的项目中使用它;
延迟任务优化
通过 handler 的 addIdleHandler 方法添加延迟任务,会在主线程空闲的时候执行;
线程优化
线程的优化主要在于减少 CPU 调度带来的波动,让应用的启动时间更加稳定;
- 控制线程数量;线程的优化一方面是控制线程数量,线程数量太多会相互竞争 CPU 资源,因此要有统一的线程池,并且根据机器性能来控制数量;
- 检查线程间的锁;
- 防止主线程因为其他线程的锁而等待空转
- 为各个任务建立依赖关系,最终构成一个有向无环图。对于可以并发的任务,会通过线程池最大程度提升启动速度,通过启动框架进行优化,例如:Android StartUp、Aplha、mmkernel
GC 优化
- 在启动过程,要尽量减少 GC 的次数,避免造成主线程长时间的卡顿;
- 通过 systrace 单独查看整个启动过程 GC 的时间;
- 启动过程避免进行大量的字符串操作,特别是序列化跟反序列化过程。一些频繁创建的对象,例如网络库和图片库中的 Byte 数组、Buffer 可以复用。如果一些模块实在需要频繁创建对象,可以考虑移到 Native 实现;
- 监控启动过程总 GC 的耗时情况,特别是阻塞式同步 GC 的总次数和耗时
// GC使用的总耗时,单位是毫秒
Debug.getRuntimeStat("art.gc.gc-time");
// 阻塞式GC的总耗时
Debug.getRuntimeStat("art.gc.blocking-gc-time");
系统调优优化
在启动过程,我们尽量不要做系统调用,例如 PackageManagerService 操作、Binder 调用等待;
- 在启动过程也不要过早地拉起应用的其他进程,System Server 和新的进程都会竞争 CPU 资源。特别是系统内存不足的时候,当我们拉起一个新的进程,可能会成为“压死骆驼的最后一根稻草”。它可能会触发系统的 low memory killer 机制,导致系统杀死和拉起(保活)大量的进程,从而影响前台进程的 CPU;
I/O 优化
- 启动过程不建议出现网络 I/O;
- 启动过程尽可能的减少磁盘 I/O,只解析启动过程用到的数据项则会很大程度减少解析时间,启动过程适合使用随机读写的数据结构
- 可以将 ArrayMap 改造成支持随机读写、延时解析的数据存储方式
数据重排
原理:Dex 文件用的到的类和安装包 APK 里面各种资源文件一般都比较小,但是读取非常频繁。我们可以利用系统这个机制将它们按照读取顺序重新排列,减少真实的磁盘 I/O 次数;
- 类重排
- 第一步、启动过程类加载顺序可以通过复写 ClassLoader 得到
- 第二步、然后通过 ReDex(facebook开源) 的Interdex调整类在 Dex 中的排列顺序;
- 资源文件重排
- 通过修改 Kernel 源码,单独编译了一个特殊的 ROM;
- 支付宝 App 构建优化解析:通过安装包重排布优化 Android 端启动性能
类加载优化
通过 Hook 的方式 去掉 类加载的过程中 verify class 的步骤;
下一张预告
Android StartUp 原理解析
欢迎三连
来都来了,点个关注,点个赞吧,你的支持是我最大的动力~