请保持淡定,分析代码,记住:性能很重要。
毫无疑问,应用的启动速度越快越好。
本文可以帮助你优化应用的启动时间:首先解释启动过程内部机制;然后讨论如何分析启动性能;最后,描述了一些常见的影响启动时间的问题,并就如何解决这些问题给出一些提示。
应用的启动有三种状态,不同状态的启动时长是不一样的。三种状态分别为:冷启动(cold start),热启动(warm start),温启动(lukewarm start)。冷启动即应用从零开始加载运行,而其它则是应用从后台运行回到前台运行。建议您始终基于冷启动的假设进行优化,因为这样做同样提升了另两种启动状态的表现。
要使得应用能快速启动,需要先理解应用以不同状态启动时,系统和应用内发生了什么,以及它们如何相互作用。
冷启动指系统没有该应用的进程,直到开启应用才创建出应用程序的进程。冷启动一般指的就是应用在开机后或者系统停止应用后的第一次启动。因为系统和应用程序在该状态下启动需要做更多的工作,所以减少它的启动时间的挑战是最大的。
冷启动初始时,系统完成三个任务:
一旦系统创建了应用的专属进程,该进程开始创建应用:
一旦应用完成了第一次绘制,系统进程就把当前显示的启动窗口切换为应用的界面,这时用户就可以开始使用应用了。
下图展示了系统和应用的启动时相互之间的关系:
以上流程中的大部分由系统来控制,需要关注性能问题的地方往往出现在 Application 和 Activity 的创建(onCreate)过程中。
当你的应用启动,屏幕立即出现的空白屏幕,将在应用完成首屏的绘制时,切换为应用首屏,然后允许用户开始与应用进行交互。
如果你在应用中重载了 Application.oncreate(),系统将先调用应用的 onCreate()方法。大型的 App 通常会在这里做大量的通用组件、SDK 的初始化操作。
然后应用程序会生成主线程,也被称为 UI 线程,并开始创建 Main Activity。
在这之后,系统和应用按各自的生命周期运行着。
应用创建 Activity:
通常情况下,onCreate() 方法对加载时间的影响最大,因为它执行了开销最重的工作:加载、渲染,以及初始化 Activity 所需要的对象,如果布局过于复杂很可能导致严重的启动性能问题。
应用程序的热启动比冷启动开销低。在热启动中,系统只是需要把 Activity 切换到前台运行。如果应用的该 Activity 之前驻留在内存中,那么应用程序就不用重新初始化对象和渲染布局。
但是,如果由于响应了低内存事件,例如在 onTrimMemory() 方法中清除了资源对象,那么这些对象就需要在热启动时重新创建。
热启动与冷启动的显示情况是一致的:系统进程显示空白屏幕,直到应用程序已经完成 Activity 的渲染。即如果从内存中直接切换,则不会显示空白屏幕,如果内存内容被清除,将显示空白屏幕等待渲染完成。
温启动为冷启动的过程操作的子集:这代表开销比热启动稍大。以下这些情况可以认为是温启动:
用户退出应用,但随后重新启动它。应用的进程还在运行,但应用必须从新从 onCreate()开始创建 Activity。
系统从内存中清除了应用(非用户主动),然后用户重新启动它。进程和 Activity 需要重新启动,但 onCreate()将接收到保存状态的 Bundle。事实上,savedInstanceState 的保存在用户未主动销毁 Activity 时系统就会调用。
为了正确评估启动时的表现,你需要跟踪应用启动到显示需要多长时间。下图展示了应用初始显示的时间和完全显示的时间的定义。
从 Android 4.4(API 19) 开始,logcat 的输出包括了一行 Displayed 的值。这个值表示了应用启动进程到 Activity 完成屏幕绘制经过的时间。经过的时间包括以下事件,按顺序为:
报告的日志行看起来类似于下面的例子:
I/ActivityManager: Displayed com.android.contacts/.activities.PeopleActivity: +612ms
如果您在终端使用 logcat,可以直接找到这一行,当然,为了方便需要使用 grep 进行查找。而如果使用 Android Studio 查看,你必须在你的 logcat 视图中禁用过滤器,因为这是系统打的日志而不是应用本身。一旦您完成了过滤器设置,就可以轻松地搜索到该行查看时间。下图 展示了如何禁用过滤器,及 logcat 窗口显示 Displayed 时间的例子。
Displayed 时间显示的是到第一次绘制的时候,它并不包括不被布局文件及初始化对象所引用的资源的加载时间,因为这个加载是一个内部过程,不阻塞应用初始内容的显示。
你也可以使用 ADB Shell Activity Manager 测量启动到显示的时间。下面是一个例子:
adb shell am start -S -W com.android.contacts/.activities.PeopleActivity -c android.intent.category.LAUNCHER -a android.intent.action.MAIN
你的终端窗口就像显示 Displayed 一样地显示如下内容:
Starting: Intent { act=android.intent.action.MAIN cat=[android.intent.category.LAUNCHER] cmp=com.android.contacts/.activities.PeopleActivity } Status: ok Activity: com.android.contacts/.activities.PeopleActivity ThisTime: 701 TotalTime: 701 WaitTime: 718 Complete
通过可选参数-c 和-a 可以指定 Intent 的 <category> 和 <action>。
你可以使用reportFullyDrawn()方法来测量应用启动到所有资源和视图层次结构的完整显示之间所经过的时间,这在应用使用延迟加载的情况下是很有用的。
在延迟加载时,应用在初始的绘图之后,异步加载资源,然后更新视图。如果由于延迟加载,应用的初始显示并不包括所有的资源,你可能会考虑将所有的资源和视图的完全加载和显示作为一个单独的指标。例如,您的用户界面可能已经完成了文本的加载,但又必须从网络获取图像。
为了解决这个问题,你可以手动调用reportFullyDrawn(),让系统知道你的 Activity 完成了它的延迟加载。当您使用此方法,logcat 将显示出从创建应用对象到调用 reportFullyDrawn()方法的时间。下面是 logcat 的输出的例子:
system_process I/ActivityManager: Fully drawn {package}/.MainActivity: +1s54ms
还有一种测量启动时间的方法值得一提,因为这种方法虽然繁琐但可以很直观查看起止位置的时间,那就是通过screenrecord命令。该命令可以直接录制屏幕,通过以下命令启动:
adb shell screenrecord --bugreport /sdcard/launch.mp4
在手机上操作,点击 App,等待其显示,必要时可以多等待一会儿,然后使用Ctrl + c停止命令,就得到了想要的视频了。使用命令导出视频:
adb pull /sdcard/launch.mp4
接着就可以使用一个能逐帧查看的视频播放器——例如 QuickTime 播放器来查看视频,一般地,认为 App 的图标高亮时为计时的起点,记录此刻到你想要的停止的位置之间的时间就可以了。简单来说,就是录制一个视频,使用逐帧查看的视频播放器方便地记录下你想查看的任意起止时刻。
如果你发现启动时间比预期要慢,你可以尝试着找出启动过程中的瓶颈。
有两个很好的方法可以用来来定位问题:Method Tracer 工具和 Systrace 工具。
在 Android Studio 的 CPU Monitor 栏中,提供了 Method Tracer 工具。
首先需要启动要监控的应用,在 Android Studio 下方的 Android Monitor 中选择该应用的进程(图中长方框位置),就可以看到 Memory Monitor / CPU Monitor / Network Monitor 都开始工作起来。
如果要使用 Method Trace 功能,只需要点击 Start Method Tracing(图中小方框),在手机上进行操作之后,再次点击它停止 Method Trace,稍等片刻就能在工程的 captures 文件夹中找到 .trace 文件了。
由以上流程可以知道对于冷启动而言是无法在正确的时间启动该工具以获得日志信息的。这种情况下可以在代码中合适的位置,例如onCreate()和onResume()中,添加android.os.Debug.startMethodTracing()和android.os.Debug.stopMethodTracing()方法来生成 trace 文件,该文件生成在 sdcard 根目录下或者应用的读写目录中。
Note: 运行 Method Trace 将明显地影响运行应用的效果。 Method Trace 应该用来了解程序的流程及方法的运行时间比例,其计时时间不可直接作为应用性能的表现。
使用 Android Studio 打开 trace 文件,如果是 CPU Monitor 中生成了 trace 文件,Android Studio 会自动打开它,你将得到如下形式的图片:
列名 | 具体含义 |
---|---|
Name | 方法名 |
Invocation Count | 方法调用次数 |
Inclusive Time (microseconds) | 该方法及其调用的子方法的耗时 |
Exclusive Time (microseconds) | 该方法(不包含调用的子方法)的耗时 |
图表的 x 坐标可以选择Wall Clock Time或者Thread Time,其中前者表示方法调用到返回结果真实的 CPU 时间,后者表示线程调度的时间,如果线程不连续执行,那么被中断的时间将被排除,所以将小于前者的统计。
也可以使用 DDMS 打开 trace 文件,其展示的视图如下所示:
各列名称及其含义与 Android Studio 的图示基本类似。
还可以使用 dmtracedump 工具解析生成 html 文件如下图(dmtracedump 可以生成图片,但往往混乱到看不出顺序,有兴趣的可以自行查阅相关资料):
从以上三种方式展示的 trace 文件结果来看,结果中包含了 JDK 函数,第三方库函数,以及 Android SDK 中函数,如果想仅分析应用中的方法调用顺序信息,可以根据 trace 文件过滤出当前应用下的方法信息。目前 GitHub 上有一个 Windows 平台下的分析应用方法耗时的 swing 工具,其使用方法很简单:
该工具的思路基于:一个能让你了解所有函数调用顺序以及函数耗时的 Android 库(无需侵入式代码),该库核心就是 2 个 build.gradle 中的 task 基于 dmtracedump 工具对 trace 文件进行解析、过滤。
Method Trace Tool 得到了良好的展示效果,如图:
以上 trace 文件的几种展示方式可以让你了解到关于应用中方法的调用顺序及耗时信息(注意:该耗时信息不代表真正使用场景下的耗时),基于以上信息可以分析出一个方法或者一个环节是否成为性能瓶颈。
另一个跟踪的方法就是 Systrace 的使用了。了解更多,请参阅 Trace 功能的参考文档,以及 Systrace 工具的介绍。