主题
我们今天讨论的主题是:
- 使用第三方工具(CPU Profile)来优化app的启动时间。
背景
想要进行app的启动优化有一点必须要知道的就是Android的启动流程和启动状态。
启动流程
Android的启动流程相关的知识点,各位可以去查阅Android framework相关的资料,这里只是简单的说下启动流程。大家可以看下下面这张图。
- 点击桌面App图标,Launcher进程采用Binder IPC向system_server进程发起startActivity请求。
- system_server进程收到请求后,向zygote进程发送创建进程的请求。
- zygote进程fork出新的子进程,即app进程
- App进程,通过Binder IPC向system_server进程发起attachApplication请求。
- system_server进程在收到请求后,进行一系列准备工作后,再通过binder IPC向App进程发送scheduleLaunchActivity请求。
- App进程的binder线程(ApplicationThread)在收到请求后,通过handler向主线程发送LAUNCH_ACTIVITY消息。
- 主线程在收到Message后,通过反射机制创建目标Activity,并回调Activity.onCreate等方法。
- 到此,App便正式启动,开始进入activity生命周期。
因此,我们可以 整体的将应用启动分成三个阶段:
- 第一阶段:点击桌面Launcher应用的图标,通过与AMS(ActivityManagerService)通信,启动应用的过程。
- 第二阶段:应用Application执行过程。
- 第三阶段:启动Activitv执行过程过程。
启动状态
应用有三种启动状态,每种状态都会影响应用向用户显示所需的时间:冷启动、温启动与热启动。
在冷启动中,应用从头开始启动。在另外两种状态中,系统需要将后台运行的应用带入前台。建议始终在假定冷启动的基础上进行优化。这样做也可以提升温启动和热启动的性能。
- 冷启动——冷启动是指应用从头开始启动:系统进程在冷启动后才创建应用进程。发生冷启动的情况包括应用
自设备启动后或系统终止应用后首次启动。
- 热启动——在热启动中,系统的所有工作就是将 Activity 带到前台。只要应用的所有 Activity 仍驻留在内存中,应用就不必重复执行对象初始化、布局加载和绘制。
- 温启动——包含了在冷启动期间发生的部分操作;同时,它的开销要比热启动高。有许多潜在状态可视为温启动。
例如:用户在退出应用后又重新启动应用。进程可能末被销毁,继续运行,但应用需要执行oncreate()从头开始重新创建 Activity。
系统将应用从内存中释放,然后用户又重新启动它。进程和 Activity 需要重启,但传递到oncreate()的已保存的实例state bundle 对于完成此任务有一定助益。
疑问
我们该如何衡量启动时间呢?或者说如何证明启动速度变快了呢?
- 冷启动统计耗时
在性能测试中存在启动时间2-5-8原则:
- 当用户能够在2秒以内得到响应时,会感觉系统的响应很快;
- 当用户在2-5秒之间得到响应时,会感觉系统的响应速度还可以;
- 当用户在5-8秒以内得到响应时,会感觉系统的响应速度很慢,但是还可以接受;
- 而当用户在超过8秒后仍然无法得到响应时,会感觉系统糟透了,或者认为系统已经失去响应。
Google 也提出一项计划:Android Vitals。该计划旨在改善 Android 设备的稳定性和性能。当选择启用了该计划的用户运行您的应用时,其 Android 设备会记录各种指标,包括应用稳定性、应用启动时间、电池使用情况、呈现时问和权限遭拒等方面的数据。Google Play 管理中心 会汇总这些数据,并将其显示在 Android Vitals 信息中心内。
当应用启动时间过长时,Android vitals 可以通过 Play 管理中心提醒您,从而帮助提升应用性能。Android Vitals在您的应用出现以下情况时将其启动时间视为过长:
- 冷启动用了5秒或更长时间。
- 温启动用了 2秒或更长时间。
- 热启动用了 1.5 秒或更长时间。
注意:这些时间其实只是一个参考值,并不是一个标准,不能说某台设备冷启动时间超过5s就认为启动时间过长了。因为app的启动时间跟设备有着很大的关系,如果一台性能很差的设备来运行app,可能它就是需要这么长的时间。
实际上不同的应用因为启动时需要初始化的数据不同,启动时间自然也会不同。相同的应用也会因为在不同的设备,因为设备性能影响启动速度不同。所以实际上启动时间并没有绝对统一的标准,我们之所以需要进行启动耗时的统计的,可能在于产品对我们应用启动时间提出具体的要求。
耗时统计的两种手段:系统日志统计和adb命令统计。
系统日志统计
在Android 4.4(AP1 级别 19)及更高版本中,logcat 包含一个输出行,其中包含名为 Displayed 的值。此值代表从启动进程到在屏幕上完成对应 Activity 的绘制所用的时间。
ActivityManager: Displayed com.android.myexample/.StartupTiming: +3s534ms
如果我们使用异步懒加载的方式来提升程序画面的显示速度;这通常会导致的一个问题是,程序画面已经显示,同时 Displayed 日志已经打印,可是内容却还在加载中。为了衡量这些异步加载资源所耗费的时间,我们可以在异步加载完毕之后调用 activity.reportFul1yDrawn()方法来让系统打印到调用此方法为止的启动耗时。
adb命令统计
查看启动时间的另一种方式是使用命令:
adb shell am start -S -W com.example.app/.MainActivity
-c android.intent.category.LAUNCHER//可以不加这句
-a android.intent.action.MAIN//可以不加这句
启动完成后,将输出:
ThisTime:415
TotalTime:415
WaitTime:437
- WaitTime:总的耗时,包括前一个应用Activity pause的时间和新应用启动的时间;
- ThisTime:表示一连串启动Activity的最后一个Activity的启动耗时;
- TotalTime:表示新应用启动的耗时,包括新进程的启动和Activity的启动,但不包括前一个应用Activity pause的耗时。
开发者一般只要关心TotalTime即可,这个时间才是自己应用真正启动的耗时。
优化方向
上面我们说到app的启动流程可以分为3个阶段。
-
关于第一阶段的优化方向,这个需要framework工程师来实现的,如果是app开发程序员基本上是无能为力的。
-
关于第二阶段的优化方向,我们首先需要了解下application执行阶段会做些什么事情?首先会初始化构造方法,然后会执行他的第一个函数attachBaseContext方法, 接着还有会执行它的oncreate回调方法。
在Application 执行这两个方法的时候,我们的app程序都在显示什么?显示的是一个黑白屏(究竟是黑屏还是白屏这就要看我们应用设置的theme),这个黑白屏要持续多长时间?这个时间就是我们要启动优化的时间。
在实际开发中,我们往往会讲这个黑白屏进行设置,设置为一个需要显示的图片,或者广告图片,这个图片的显示会使用户产生错觉:以为进入了app。然而,app其实并没有任何的优化。
因此,针对这个阶段的优化主要有三个部分:第一部分是attachBaseContext回调方法的优化,第二部分是onCreate
回调方法的优化,第三部分就是对应用执行到Activity之前的白屏处理(注意如果是对application所使用的theme的android:windowBackground进行设置要在显示activity的设置会原来的形式,因为android:windowBackground是影响整个app的,即该app的所有页面都共用同一个windowBackground)。
这里,说下windowBackground这个属性的作用。我们知道一个app有一个window,因此如果这个属性被改动了,那么整个app的背景都会受影响。通常我们app的背景颜色是白色或者黑色就是与这个属性有密切的关系。假设一种情况,如果我们将该属性设置为透明,在我们的布局文件不设置backgroundColor的情况下,大家猜猜会是怎么样的一个效果。没错,你将会看到app的背景是手机的桌面。
-
关于第三阶段的优化方向,在这个阶段,主要是第一个activity 运行的阶段,直到activity执行完成onResume函数之后,这个启动过程才算是完
成了,因此,第一个activity启动过程中的 onCreate()函数,onStart()函数,onResume()函数都将是我们需要优化的点。不能在这几个函数里面进行耗时的工作。
onCreate()函数的优化
一般这个函数里面做的事情是setContentView (resld);这个函数相对耗时,这个函数的执行其实是反射,用反射去解析xml,这个过程自然是耗时的。
另外,大家一定要尽可能的使用 约束布局来改造相对布局和线性布局,这个过程可以达到減少布局层级的效果,Google有分析,constraintlayout 会比RelativeLayout的效率要高40%。
onStart()、onResume()函数优化
为了快速加载Activity所需的数据,可以采用缓存的方式。如果数据异步加载成功后,会更新缓存。启动初始化时,可能会有很多接口,这里最好在启动时只通过一个接口拉取应用启动和MainActivity的主Fragment展示的所有数据。需要显示多少,就加载多少数据,不要额外的加载数据。此时,懒加载是一个非常好的选择。
另外,IdleHandler 是 MessageQueue 内定义的一个接口,一般可用于做性能优化。当消息队列内没有需要立即执行的 message 时,会主动触发 IdleHandler 的 queueIdle 方法。返回值为 false,即只会执行一次;返回值为 true,即每次当消息队列内没有需要立即执行的消息时,都会触发该方法。
Looper.myQueue().addIdleHandler(new IdleHandle() {
@override
public boolean queueIdled{
//你想做的任何事情
//...
return false;
}}):
android中主线程要处理的所有的事务全部都是在handler里面完成的,小到每一个按键事件,大到每一个activity 的生命周期,都是主线程handler中的一个Message。那么每一个Message都是存储在—MessageQueue里面,当我们messagequeue里面没有消息要立刻执行的时候,说明主线程
没有任务要做,这个就是主线程的空档,于是,我们就可以利用这个空档去做耗时任务。这个时候我们开发者就可以利用这一点,来把耗时任务放到这个idleHandler里面执行,从而提升了运行的速度。注意:不能够太耗时哦,因为本质上也是通过handler发送消息的,如果太耗时会导致后面的事件无法及时响应。
接下来,进入我们的重头戏:使用CPU Profile来优化启动速度。
请注意:开启这个功能可能会导致电脑卡顿,app打开时间要比一般情况下长。
Sample Java Methods
对Java 方法采样:在应用的Java 代码执行期间,频繁捕获应用的调用堆栈。分析器会比较捕获的数据集,以推导与应用的Java 代码执行有关的时间和资源使用信息。如果应用在捕获调用堆栈后进入一个方法并在下次捕获前退出该方法,分析器将不会记录该方法调用。如果您想要跟踪生命周期如此短的方法,应使用检测跟踪。
Trace Java Methods
跟踪Java 方法:在运行时检测应用,以在每个方法调用开始和结束时记录一个时间戳。系统会收集并比较这些时间戳,以生成方法跟踪数据,包括时间信息和 CPU 使用率。
Sample C/C++ Functions
对C/C++方法采样:捕获应用的原生线程的采样跟踪数据。要使用此配置,您必须将应用部署到搭载Android 8.0 (API 级别 26) 或更高版本的设备上。
Trace System Calls
跟踪系统调用:捕获非常详细的细节,以便您检查应用与系统资源的交互情况。您可以检查线程状态的确切时间和持续时间、直观地查看所有内校的 CPU 瓶颈在何处,并添加要分析的自定义跟踪事件。要使用此配
置,您心须将应用部署到搭载 Android 7.0 (API级别 24) 或更高版本的设备上。
此跟踪配置在 systrace 的基础上构建而成。您可以使用 systrace 命令行实用程序指定除 CPU Profiler 提供的选项之外的其他选项。systrace 提供的其他系统级数据可帮助您检查原生系统进程并排查丢帧或帧延迟问题。
如果我们只是进行app启动方面的优化,一般选择第2个即可。
下面,我们以一个案例来演示下如何使用这个工具。
public class MyApplication extends Application {
public MyApplication(){
Debug. startMethodTracing("brett");
}
}
public class MainActivity extends AppCompatActivity {
@Override
public void onWindowFocusChanged (boolean hasFocus) {
super.onWindowFocusChanged(hasFocus);
Debug. stopMethodTracing();
}
}
运行App,则会在sdcard中生成一个brett.trace文件(需要sdcard读写权限)。将手机中的trace文件保存至电脑,随后拖入Android studio即可。因为Profile是需要Android8以上的手机才可以使用的,针对与android8以下系统的手机我们可以使用这种方式。
补充:严苛模式
StrictMode是一个开发人员工具,它可以检测出我们可能无意中做的事情,并将它们提请我们注意,以便我们能够修复它们。
StrictMode最常用于捕获应用程序主线程上的意外磁盘或网络访问。帮助我们让磁盘和网络操作远离主线程,可以使应用程序更加平滑、响应更快。一般建议在debug中开启。
public class MyApplication extends Application
@Override
public void onCreate(){
if (BuildConfig.DEBUG) {
//线程检测策略
StrictMode.setThreadPolicy(newStrictMode.ThreadPolicy.Builder()
.detectDiskReads()
//读、写操作
.detectDiskWrites()
.detectNetwork()
// or .detectAll() for all detectable problems
•penaltyLog()
•build());
StrictMode.setVmPolicy(newStrictMode.VmPolicy.Builder()
.detectLeakedSqlLiteObjects() //Sqlite对象泄露
.detectLeakedClosableobjects() //末关闭的C1osable对象泄露
.penaltyLog() //违规打印日志
.penaltyDeath() //违规崩溃
.build());
}
}
最后
启动速度优化也会涉及到布局优化与卡顿优化,包括内存抖动等问题。优化是一条持续的道路,很多时候我们会发现通过各种检测手段花费了大量的精力去对代码进行修改得到的优化效果可能并不理想。因为优化就是一点一滴积累下来的,我们平时在编码的过程中就需要多注意自己的代码性能。
可能实际过程中优化并不会很顺利,不同的设备上可能表现不一样。我们只能结合对业务、对自己代码的了解去不断去实践。