用户希望App能快速响应并且快速打开。如果一个App启动速度慢,那么有可能会导致用户失望。这一系列的情况会导致用户给你5星差评并且卸载你的软件。
这篇文章提供了一些有用的信息去帮助你控制好你的App启动时间。先解释一下App启动过程,然后讨论如何去分析启动性能,最后讨论一下常见的启动问题,并且给出一些定位这些问题的小技巧。
理解App启动过程
App启动可分为三种类型,每一种类型都会影响到你App展现给用户的时间:冷启动、暖启动和热启动。在冷启动中,你的App是重零开始。在剩下的两种类型中。系统需要把在后台运行的App移到前台。我们强烈建议你总是基于冷启动进行优化,这样也可以提高暖启动和热启动的性能。
了解在App启动过程中系统发生了什么,以及他们在这过程中是如何互动的。对优化你的App启动速度是很有帮助的。
冷启动
冷启动是指一个App是重新创建的,系统进程里面没有App的进程,直到它启动了并且创建了。冷启动会发生在你的App第一次打开(从开机起第一次打开,或者系统杀掉你的App后第一次打开)。这种启动过程在最小化启动优化里面具有最大的挑战,因为这种启动比其他类型,系统和App都要做多更多的工作。
在冷启动的开始,系统有三个任务需要完成,这些任务是:
- 加载和启动App
- 在App启动后里面显示出一个空白的窗口
- 创建App进程
一旦系统创建完App进程,App进程就会进入下一个流程:
- 创建App
- 启动主线程
- 创建main activity
- 加载views
- 布局测量
- 初始化绘图
一旦App进程完成第一个绘制,系统会把App替换成当前活动的activity,这个时候,用户就能开始使用App了。
图一表示系统和App进程在切换过程中的工作原理
在创建App和创建Activity过程中会出现性能问题
Application creation
当你的app启动的时候,系统会加载一个空白的窗口直到系统完成app的第一次绘制,这个时候系统进程会把你的app替换当前的活动窗口,然后用户就可以开始与应用程序进行交互。
如果你有在你的app里面重写Application.OnCreate()
,系统会在启动后调用OnCreate()
这个方法,然后app就会创建主线程,同时也是UI线程,并且通过它来创建你的main activity。
从这一点开始,系统和应用程序级别的App将根据应用程序生命周期阶段进行。
Activity creation
在app创建了你的activity之后,activity会执行以下几个操作:
- 初始化值
- 调用构建方法
- 调用回调函数,例如
Activity.onCreate()
等等Activity的生命周期方法
通常,onCreate()方法对加载时间的影响最大,因为它以最高的开销执行工作:加载和绘制views,以及初始化活动运行所需的对象。
热启动
热启动比冷启动更加简单,开销更低。在热启动中,系统所需要做的就是把你的activity切换到前台。如果你app里面的所有activitys都还在内存里面,那么app可以避免重复初始化对面,布局加载和惠子。
然而,在某些时候如果内存紧张的时候,那么这些对象在热启动中就必须得重新创建。
热启动展现activity的行为跟冷启动样:先显示一个空白的窗口知道app完成第一次绘制
暖启动
暖启动包括冷启动期间发生的一些操作子集;同时,它比热启动更少的开销。有许多潜在的状态可以被视为暖启动。例如:
- 用户点击home,app进入后台,然后重启app,app进程可能还在继续运行。但是activity被回收,app必须从头开始创建。也会调用activity的OnCreate方法。
- 系统回收了你的app,然后用户重启app。这个时候app进程和Activity都需要重新启动,但是它们可以从
onCreate()
方法中的bundle恢复数据。
诊断问题
Android提供了几种方式让你知道你的app有问题,和帮助你诊断它。
Android vitals可以提醒你问题正在发生,诊断工具可以帮助你诊断问题。
Android vitals
Android vitals可以通过Goole Play的控制台在您的应用启动时间过长时提醒您,以帮助提高应用的性能。当应用程序出现一下情况时,Android vitals认为您应用的启动时间过长:
- 冷启动超过5秒
- 暖启动超过2秒
- 热启动产国1.5秒
诊断启动慢问题
为了定位启动时间,你可以通过定位你的application启动花费时间。
初始化显示时间
在Android4.4(API 19)或者更高,logcat的里面有一行输出包含了关键字Displayed
。这个值表示了activity从启动到显示的耗时。这个耗时包含了一下几个事件:
- 启动进程
- 初始化对象
- 创建和初始化Activity
- 绘制布局
- 第一次绘制app
日志输出如下:
1 | ActivityManager: Displayed com.android.myexample/.StartupTiming: +3s534ms |
为了能在Android studio里面找到耗时,你需要修改filter在你的logcat,改成No Filters,而不是你的App进程。
一旦你修改好了,你就很容易在logcat里面查找耗时,如果图二显示如何修改filters,在倒数第二行显示了耗时
Displayed
有时候并不是一个完整的加载时间,它可能会遗漏布局文件中没有引用到的资源文件。例如:
1 | ActivityManager: Displayed com.android.myexample/.StartupTiming: +3s534ms (total +1m22s643ms) |
在这种情况,一个时间仅代表第一次绘制时间,total
的时间表示app进程启动包括一些activity第一次启动但没有被显示出来的时间。
你也可以通过ADB命令去计算你app启动的时间,例如:
1 | adb [-d|-e|-s <serialNumber>] shell am start -S -W |
命令行会输出一下内容:
1 | Starting: Intent |
-c
和-a
参数表示Intent里面的<category>
和<action>
完成的显示时间
你可以使用reportFullyDrawn())去计算一个app启动到完全显示的时间。这个在应用程序执行延迟加载的情况下会很有用。因为在延迟加载中,应用程序不会阻止窗口的初始绘制,而是异步加载资源并更新视图层次结构。
由于延迟加载,一个app的初始化显示不包含所有的资源,你可能要把资源文件和views完成加载和显示作为一个时间度量。例如,你的UI里面的text可能完成加载,但是你的图片还没从网络下载下来。
在这种情况下,你可以手动调用reportFullyDrawn())方法让系统知道你的activity已经完成了延迟加载。当你使用这个方法,logcat会输出从app创建到reportFullyDrawn())方法调用的总耗时。下面就是输出例子:
1 | system_process I/ActivityManager: Fully drawn {package}/.MainActivity: +1s54ms |
其中有可能输出包含total
时间,这个参考初始化显示时间。
如果你看到你的耗时比你想象中的慢,你可以继续定位启动时间的瓶颈在哪里。
定位瓶颈
使用Android studio CPU profiler是一个很好定位瓶颈的方法。更多信息情况,使用CPU profiler去检查CPU情况
注意常见的问题
这一章节讨论几个经常影响到启动表现的问题。这些问题大部分都是初始化app和activity对象,也有一部分是屏幕加载。
繁重的app初始化任务
当你在代码里面重写了Application
对象,并且在里面执行了大量繁重的任务和复杂逻辑的时候.如果你在app初始化里面做了些非必要初始化的工作,那么就会浪费了启动时间。有些初始化是没必要的:例如,当app通过一个intent启动,然后初始化主activity的数据,在intent里面,app只用到了之前初始化好的部分信息。这样就会造成浪费。
另一部分的原因是当app执行初始化的时候,系统在执行垃圾回收。在早期Dalvik虚拟机的时候,垃圾回收会阻塞初始化进程。在Art虚拟机的时候,垃圾回收机制改成了并发的,最小的回收策略。那么就不会阻塞初始化进程。
诊断问题
你可以使用方法跟踪或内联跟踪来尝试诊断问题。
方法跟踪
使用CPU Profiler 来看callApplicationOnCreate())方法,最后会去到com.example.customApplication.onCreate
方法,如果工具显示这些方法花了很上时间去执行,表示你需要查看一下到底在里面做了什么导致耗时。
行内跟踪
使用行内跟踪来定位一下情况:
- 你的app初始化方法
onCreate
- 任何在你的app初始化的时候存在的全局单例对象
- 任何硬盘I/O,反序列化,或者循环可能会导致耗时
解决方案
有很多情况会导致耗时,但是两个常见的问题和解决方案如下:
- view的层级越深,那么app就需要用更多的时间去渲染它。有两个办法可以解决这个问题:
- 通过减少冗余或者使用嵌套布局来减少view的层级
- 在启动过程中,不去渲染看不见的UI.可以使用ViewStub来作为占位符,然后app可以在适当的时机把它渲染出来
- 在主线程进行所有的资源初始化也会导致启动耗时。你可以按照以下方式来解决问题。
- 在其他线程进行延迟加载资源。
- Allow the app to load and display your views, and then later update visual properties that are dependent on bitmaps and other resources.(这句理解不了)
启动主题
你可能希望对应用程序设置了主题,以便应用程序的启动屏幕的主题和应用程序的其他部分保持一致,而不是与系统主题一致。这样做可以隐藏一个缓慢的活动启动。
一个常见的设置启动屏幕主题的方法是使用windowDisablePreview主题属性来关闭启动应用程序时系统进程绘制的初始空白屏幕。然而,这种方式会导致app不支持启动预览窗口从而导致更长的启动时间,这个时候用户在activity启动的过程中会怀疑是否App有启动。
诊断问题
你可以通过在用户启动应用时观察响应缓慢来诊断此问题。在这种情况下,屏幕似乎被冻结,或者点击任何东西都无响应。
解决问题
我们强烈推荐不要禁止预览窗口,而是遵循常见的Material Design开发模式。你可以使用activity的windowBackground
属性来自定义简单的启动图片。
例如,你可以创建一个新的的图片文件和在布局文件的xml使用它:
布局文件xml:
1 | <layer-list xmlns:android="http://schemas.android.com/apk/res/android" android:opacity="opaque"> |
Manifest文件:
1 | <activity ... |
最简单的变换主题的方式是通过在super.onCreate()
和setContentView()
之前调用setTheme(R.style.AppTheme))方法:
java:
1 | public class MyMainActivity extends AppCompatActivity { |
Kotlin:
1 | public class MyMainActivity extends AppCompatActivity { |