Android 性能优化入门(三)—— 启动速度优化
1、启动耗时统计
应用启动流程在前面 AMS 时已经讲过,回忆一下步骤:
①点击桌面App图标,Launcher进程采用Binder IPC向system_server进程发起startActivity请求;
②system_server进程接收到请求后,向zygote进程发送创建进程的请求;
③Zygote进程fork出新的子进程,即App进程;
④App进程,通过Binder IPC向sytem_server进程发起attachApplication请求;
⑤system_server进程在收到请求后,进行一系列准备工作后,再通过binder IPC向App进程发送scheduleLaunchActivity请求;
⑥App进程的binder线程(ApplicationThread)在收到请求后,通过handler向主线程发送LAUNCH_ACTIVITY消息;
⑦主线程在收到Message后,通过反射机制创建目标Activity,并回调Activity.onCreate()等方法。
⑧到此,App便正式启动,开始进入Activity生命周期,执行完onCreate/onStart/onResume方法,UI渲染结束后便可以看到App的主界面。
应用有三种启动状态,每种状态都会影响应用向用户显示所需的时间:冷启动、温启动与热启动:
- 冷启动:冷启动是指应用从头开始启动,系统进程在冷启动后才创建应用进程。发生冷启动的情况包括应用自设备启动后或系统终止应用后的首次启动
- 热启动:在热启动中,系统的所有工作就是将 Activity 带到前台。只要应用的所有 Activity 仍驻留在内存中,应用就不必重复执行对象初始化、布局加载和绘制
- 温启动:温启动包含了在冷启动期间发生的部分操作;同时,它的开销要比热启动高。有许多潜在状态可视为温启动。例如:
- 用户在退出应用后又重新启动应用。进程可能未被销毁,继续运行,但应用需要执行 onCreate() 从头开始重新创建 Activity
- 系统将应用从内存中释放,然后用户又重新启动它。进程和 Activity 需要重启,但传递到 onCreate() 的已保存的实例savedInstanceState 对于完成此任务有一定助益
- …
建议始终在假定冷启动的基础上进行优化。这样做也可以提升温启动和热启动的性能。
Android Virtal 计划提出,冷启动在 5s 以上,温启动在 2s 以上,热启动在 1.5s 以上会被视为启动时间过长。
性能优化的方法就这么多,都是死的,关键是动手实践。
至于应用启动的耗时统计,有两种方法可以查看:
-
系统的 ActivityManager 会输出从启动进程到在屏幕上完成对应 Activity 的绘制所用的时间,输入关键字 Displayed 即可看到:
I/ActivityManager: Displayed com.simple.leakcanarycode/.MainActivity: +725ms (total +15h26m26s801ms)
-
另一种方法是使用adb 命令 adb shell am start -S -W [packageName]/[activityName]:
F:>adb shell am start -S -W com.simple.leakcanarycode/.MainActivity Stopping: com.simple.leakcanarycode Starting: Intent { act=android.intent.action.MAIN cat=[android.intent.category.LAUNCHER] cmp=com.simple.leakcanarycode/.MainActivity } Status: ok Activity: com.simple.leakcanarycode/.MainActivity ThisTime: 5024 TotalTime: 5024 Complete
上面的各种时间一般有三种:
- WaitTime:包括前一个应用 Activity pause 的时间和新应用启动的时间;
- ThisTime:表示一连串启动 Activity 的最后一个 Activity 的启动耗时;
- TotalTime:表示新应用启动的耗时,包括新进程的启动和 Activity 的启动,但不包括前一个应用 Activity pause 的耗时。这个耗时应该是统计到 Activity 回调 onWindowFocusChanged()。
它们的示意图如下:
这个时间可以作为优化时间的前后对比。
那么,冷启动、热启动、温启动耗时多少才会认为是时间过长呢?Google 提出的 Android Vitals 计划中认为冷启动超过 5 秒、温启动超过 2 秒、热启动超过 1.5 秒就是启动耗时过长。
2、CPU Profile
点击运行模块 -> Edit Configurations -> Profiling -> 勾选 Start Recording CPU activity on startup -> 选择 Sample Java Methods(采样)/ Trace Java Methods(一直跟踪):
采样意味着在采样间隔调用的方法可能无法被监测到,因此选择一直跟踪好一点。
然后以 “profile app” 的方式运行 app。它可以呈现多种样式的数据,但其实数据内容都是一样的,基本都是方法的执行时间和调用关系,只不过展现方式不同:
后续具体内容,参考【启动优化.pdf】
Top Down 看起来更直观,使用方法调用链的形式展示方法的总时间、自己执行的时间和调用其他方法所用时间。
通过工具可以定位到耗时代码,然后查看是否可以进行优化。对于APP启动来说,启动耗时包括 Android 系统启动、APP 进程加上 APP 启动界面的耗时时长,我们可做的优化是 APP 启动界面的耗时,也就是说从 Application 的构建到主界面的 onWindowFocusChanged 的这一段时间。因此在这段时间内,我们的代码需要尽量避免耗时操作,检查的方向包括:主线程 IO;第三方库初始化或程序需要使用的数据等初始化改为异步加载/懒加载;减少布局复杂度与嵌套层级;Multidex(5.0以上无需考虑)等。
在观察 Top Down 时如果发现是在 setContentView() 中执行时间长,那么有可能是布局需要优化。这里稍微提一下布局优化的方法:
-
布局层级优化:层级越深,内容越复杂,LayoutInflater 解析时间越长,需要通过反射创建 View 对象的数量就越多
-
异步加载:如果 UI 界面在设计时就非常复杂,布局层级优化空间不大,可以考虑使用异步加载布局 —— AsyncLayoutInflater:
new AsyncLayoutInflater(this).inflate(R.layout.activity_main, null,new AsyncLayoutInflater.OnInflateFinishedListener() {@Overridepublic void onInflateFinished(@NonNull View view, int resid, @Nullable ViewGroup parent) {setContentView(view);}});
如果你使用的系统版本不能直接使用 CPU Profile(8.0 以下),那么可以在代码中通过 Debug.startMethodTracing(path) 和 Debug.stopMethodTracing() 来记录,path 是相对于 SD 卡目录的相对文件路径,在该目录下生成 .trace 文件,双击打开就可以通过 CPU Profile 打开了。
3、Strict Mode
StrictMode 是一个开发人员工具,它可以检测出我们可能无意中做的事情,并将它们提请我们注意,以便我们能够修复它们。
StrictMode 最常用于捕获应用程序主线程上的意外磁盘或网络访问。帮助我们让磁盘和网络操作远离主线程,可以使应用程序更加平滑、响应更快
Debug 模式下可以开启如下代码,在 Application 的 onCreate() 中:
此外,在使用 EventBus、ARouter 等三方库时,官方可能会提供针对这些框架启动速度优化的插件,需要注意,不要漏掉这些优化。
还有黑白屏的问题,实际上这是谷歌的一个对用户反馈。因为从点击应用图标的到入口 Activity 展示在屏幕上,系统为我们做了很多的工作:如为应用创建一个进程、初始化 Application、加载 Activity 等,这是需要一定时间的。如果在这段时间内只是默默的工作而不给用户反馈,用户可能会认为没有点击到应用图标,或者点击了但系统没有工作,这种体验就很不好。因此谷歌为了告诉用户系统正在启动应用,所以就会先展示一个空白的界面,这就是启动应用时黑白屏产生的原因。具体是黑屏还是白屏,取决于主题中 android:windowBackground 的设置。现在一般的解决方法就是自己找个图片设置给 android:windowBackground。
4、总结
1). 合理的使用异步初始化、延迟初始化、懒加载机制。
2). 启动过程避免耗时操作,如数据库 I/O 操作不要放在主线程执行。
3). 类加载优化:提前异步执行类加载。
4). 合理使用 IdleHandler 进行延迟初始化。
5). 简化布局
另外还有 IDLEHandler,参考文章:你知道android的MessageQueue.IdleHandler吗?
5、实战
用 MVVM 新闻客户端作为示例项目去优化。
5.1 启动时的背景图
在 Launcher 上点击应用图标到启动应用这个过程其实是需要一定时间的,Google 为了保证用户体验,会在应用图标被点击后立即开启应用,但是实际上由于应用的启动工作尚未完成,所以在这个空隙之中就会先展示一个黑/白屏,让用户知道点击动作生效,已经在打开应用了。但是这个黑/白屏看起来很不好看(是黑还是白取决于应用配置的 Theme),所以这个也算是优化的一个方向。
给黑/白屏添加一个过渡图片(当然其它效果也行,这里已添加图片为例),方法是给应用的入口 Activity 添加带有图片的 style:
<style name="AppTheme.Launcher"><item name="android:windowBackground">@drawable/old1</item><item name="android:windowFullscreen">true</item></style><activity android:name=".MainActivity"android:theme="@style/AppTheme.Launcher"><intent-filter><action android:name="android.intent.action.MAIN" /><category android:name="android.intent.category.LAUNCHER" /></intent-filter></activity>
在 Activity 的 onCreate() 中将主题设置回原本正常的主题:
@Overrideprotected void onCreate(Bundle savedInstanceState) {setTheme(R.style.AppTheme);super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);}
这样在启动应用时,白屏被替换成图片,用户体验会好一点:
5.2 启动时间检测
配置 app 运行参数,通过 Android Studio 的图形化工具 Start Recording CPU activity on startup,无论选择 Sample Java Methods 还是 Trace Java Methods 都会使得被检测的应用运行卡顿,只好转而使用代码的方式。在 Application 的构造方法中添加如下代码:
public class MyApplication extends Application {public MyApplication() {// 采样方式,采样数据大小上限就是8M,采样间隔为 1msDebug.startMethodTracingSampling(new File(Environment.getExternalStorageDirectory(),"sample").getAbsolutePath(), 8 * 1024 * 1024, 1_000);// 一直跟踪的方式,也会造成应用卡顿,所以没有使用这个方法
// Debug.startMethodTracing(new File(Environment.getExternalStorageDirectory(),
// "sample").getAbsolutePath(), 8 * 1024 * 1024);}
}
这样就在 SD 卡根目录下生成了一个 sample.trace 的文件,拖入 Android Studio 可以查看相关信息:
Top Down 查看调用链耗时,看到底是否有可以优化的地方。一些优化点可能会藏的比较深。比如页面获取缓存数据的位置如果耗费了很多时间,可以选择将该操作放在 IdleHandler 中:
// Handler 空闲时执行 IdleHandler 的任务Looper.myQueue().addIdleHandler(new MessageQueue.IdleHandler() {@Overridepublic boolean queueIdle() {// 获取缓存数据并加载的方法,原本放在 ViewModel 的构造方法中model.getCachedDataAndLoad();return false;}});
也可以选择延迟加载。
性能优化是一个很细致很耗精力的活儿,有很多可以优化的地方都很细小、很隐蔽,需要你对项目代码有一定的了解才能做好。
比如说 SharedPreferences 的使用。虽然我们读取 SP 文件时一定会将该操作放在子线程中,但是如果主线程要等待子线程的读取结果才能进行下一步操作的话,此时主线程也会对主线程产生影响。为了减少读取 SP 的耗时,可以考虑将 SP 拆分成多个文件,应用启动时必须要读取的属性,放到一个单独的 SP 文件中,这也算是启动优化的一个小细节。
5.3 布局优化
层级尽量少,且不要有过度渲染。
查看布局层级,使用 Android Studio 中的 Tools -> Layout Inspector。
ViewPager 换成 ViewPager2,后者的懒加载一行代码就搞定了。
RecyclerView 的 Item 布局层级优化。
使用 merge 标签时,在代码中调用 LayoutInflater.inflate() 时要注意 attachToParent 要传 true 同时把原本的 addView() 去掉
5.4 Systrace
Systrace 可以用来帮忙分析系统卡顿,一般是滑动的卡顿。新的 sdk 工具都么有这个了,用新的 Perfetto 替换。
5.5 BlockCanary 原理
利用 Handler 结束时间 - 开始时间的差值与预设值比较,超过预设值视为发生卡顿。
性能优化,细致,花时间,花精力!工具只能提供蛛丝马迹,最后发现问题点还是要靠自己经验。
笔记结合预习资料,基本上就是视频内容。