应用冷启动优化分析
前言
关于冷启动的优化方法,网上已经有很多的文章了,总结起来,大概有以下几种优化方式:
- 优化布局,这一步是最简单的,然而如果你的布局不是特别重的话其实优化后效果也不明显。
- 异步加载,现在一个app都会使用各种各样的第三方SDK,这些SDK大都需要在Application中去初始化,如果能让这些SDK在工作线程中去初始化的话,能减轻不少主线程的负担。
- 懒加载,常见的场景是ViewPager + Fragment,或者是一些资源等到使用的时候再去加载。
- 预加载,如果有图片的话可以尝试着去预加载,不要等到布局的时候才去加载,这一步往往能够省出很多时间,因为解码图片是一个比较耗时的操作,特别是对于比较低端的机型。
道理大家都懂,但是怎么去分析,找出这些优化点出来,网上的文章还是比较少的,或者说得没有那么详细。所以,这篇文章主要是记录下最近几个月来,做冷启动优化的一些分析的思路,文章篇幅可能有点长,希望看完之后,对你能有一些帮助。
通过Log查看冷启动时间
要优化应用的冷启动,首先,必须得先知道应用的启动时间。这里找到了三种方法可以大致获取到应用到冷启动时间。
对于应用的冷启动,我们可以通过Activitymanager
打印出的log来大致判断
I ActivityManager: Displayed com.xxx.android/.app.ui.activity.MainActivity: +2s725ms
这个时间是从应用启动到第一次绘制之间的时间,但对于用户来讲,关心的是什么时候能够看到内容。所以这个时间并不是很准确。举个例子来说,我们的首页内容是展示图片,Activitymanager
打印出来的时间是200ms,这真的是很快的启动速度了。但图片从加载到全部显示给用户的过程耗时是500ms,加起来就是700ms。这个时候你还能说你的启动速度很快吗?另外,这个信息还有一个最大的缺点就是,它只告诉你你的app有多慢,但却不告诉你慢在哪里。不知道问题所在,也就无从下手优化了。
通过录像查看冷启动时间
如果有条件的话,可以用上高速摄像机来将App启动的过程给录下来,然后再通过逐帧回放来确定冷启动的时间。这种方法虽然比较准确,但是性价比太差了!
不过,好消息是,即使我们不用高速摄像机,也可以通过另外的途径来达到这个效果。那就是使用screenrecord
命令。(这里参考的是测量Activity 的启动时间)
$ adb shell screenrecord --bugreport /sdcard/launch.mp4
启动命令之后,就可以启动我们的app了(在测试之前,最好将系统的所有动画都给关掉,这样能够不受窗口动画的干扰)。当界面显示出来后,终止掉录像。然后将文件拉到电脑上。
$ adb pull /sdcard/launch.mp4
录像是有了,但却是正常速度播放的,所以我们还需要一个能逐帧查看的视频播放器,比如说Mac上的Quicktime,如果你的Mac带TouchBar的话,那你会发现TouchBar + Quicktime简直是绝配。通过逐帧查看,找到启动的那一刻,然后记录下屏幕左上角的时间点,然后再找到界面显示出来的那一帧,两个时间相减就得到启动时间了。
通过 Systrace 分析应用冷启动
虽然用录屏的方式可以比较准确的获取到启动时间,可通过录屏我们还是不知道到底慢在哪。这时候就该Systrace上场了。不知道Systrace 是什么或者不知道怎么使用的可以看性能工具Systrace。
Systrace有命令行和图形界面两种使用方式,我个人比较喜欢图形界面的方式,因为不用去记住命令,所以这里以图形界面来举例说明。
由于是要测试冷启动,需要先杀掉进程,所以我们选中system_process
进程,然后点击启动systrace的按钮,然后会出现一个弹窗。
Trace Buffer Size(kb): 一般默认就好,如果抓出来的trace出现了did not finish..
之类的字样,适当调高这个值。
Enbale Application Traces from: 如果你使用了自定义的trace标签,那这里就需要选中你的进程,这样自定义标签才会生效。
关于下面的勾选项,选得越多,生成的trace越详细。设置完之后,记得先杀掉App进程,然后点击确定,再启动App,生成的trace用Chrome打开。
Systrace从截图中可以大概看到几个过程:ActivityThreadMain
-> bindApplication
-> activityStart
-> 布局绘制流程(头顶上有F
的那几块)。整个冷启动的过程大概是356ms,点击每一个色块,比如截图中我点中的是activityStart
,可以在下方到面板看到它的执行时间信息。
这些信息就是系统默认会帮我们打出来的,然而这些信息对于我们来说远远是不够的,好在Android提供了自定义标签。我们可以在感兴趣的阶段插入标签,比如说Application的onCreate
方法,Activity的各个生命周期方法,RecyclerView的onCreateViewHolder
和onBindViewHolder
这些关键的地方。
@Override
protected void onCreate(Bundle savedInstanceState) {
// 插入自定义标签
TraceCompat.beginSection("onCreate");
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// 和beginSection对应上
TraceCompat.endSection();
}
Systrace
插入标签后,在启动systrace的弹窗中需要选中我们要测试的进程。这里有个小问题就是冷启动需要杀掉进程,而杀掉进程后,在这里你是看不到你的进程的,所以我们可以取个巧,先选中进程,然后再杀掉进程,最后再启动,这样就能抓到我们自定义的trace了。
Systrace再次看trace,可以看到在activityStart
下面多了个我们自定义的onCreate
。
方法讲完了,举个例子先,我在项目中的onBindViewHolder
打上了标签,得到的trace中有一帧是这样子的:
发现了吗?同样都是onBindViewHolder
,第一次跟后面七次的总时间加起来差不多,执行时间是50ms,占了这一帧的一半,说明这里肯定是有问题的。这就是systrace的好处,它能让我们很直观的发现到一些问题。同样的,利用systrace,我也发现了项目在Application的onCreate
耗时太长,这点不通过systrace其实也很容易找出来,因为在这个回调里面都是一些初始化操作。相反,像上面的第一次onBindViewHolder
耗时太长就比较难发现了,最后我查出来的原因是第一次调用时会用到一个Util类,它有一块静态的代码,在这里面去加载so库导致了耗时过长。
使用systrace分析冷启动的使用就到这里,关于它的使用还是要多积累经验才能得心应手。
另外,从systrace上,你会发现inflate一个布局是一个比较耗时的操作,并且如果布局里面有使用到图片资源的话还需要去解码这些资源,也都是耗时的操作。所以这其实也是一个优化点来着。
通过 TraceView 分析应用冷启动
通过Systrace,虽然我们可以大致知道是在哪个流程慢了,比如说bindApplication
、ActivityStart
或者是整个布局绘制流程慢了。但我们能知道的,也仅限于此了。Systrace只是给我们指明了一个方向,除非我们在每一个方法上打上Systrace的标签,这样得到的trace就能看见这些方法的耗时情况了,但这样做可行性并不高。并且,这个想法,TraceView已经帮我们实现了。TraceView通过收集某个阶段所有函数的运行信息,最后通过图表的形式给我们展示出来。关于TraceView,可以看Android 性能优化:使用 TraceView 找到卡顿的元凶
虽然TraceView很强大,但它的代价就是会让系统变得很卡,因为它的运行时开销严重干扰了运行环境。你想想,TraceView本身的应用就是卡顿场景,然后再加上自身的卡顿,用了之后有时候都在怀疑到底是谁造成的卡顿了。这也是我以前一直拒绝使用TraceView的理由,但是在这一轮冷启动优化的过程中,它的强大,足以让我接受它的缺点。由于TraceView的使用会带来一定的卡顿,所以关于它的数据,大家参考一下,看一下时间的占比就好了,不要太纠结于它给出的真实时间,如果怀疑哪个方法有问题,可以利用Log打印时间或者Systrace来验证一下。
还记得上面在弹窗界面通过取巧的方式来让Systrace可以在冷启动过程中打印出我们的自定义标签吗?最开始我也是用这种思路来操作TraceView的,但后来发现行不通。好在TraceView也可以像Systrace一样通过标签来打点。并且不用像Systrace一样需要选中进程。直接运行app就可以了。
需要注意的是: TraceView默认的文件大小是8M,对于冷启动来说可能还不够,可以根据自己的情况修改文件大小,如指定文件大小为20M:
Debug.startMethodTracing("cold-start", 20 * 1024 * 1024);
这里简单写了一个例子,在MainActivity的onCreate
方法中模拟一些耗时的操作。然后打上开始和结束到标签。
public class AppImpl extends Application {
@Override
protected void attachBaseContext(Context base) {
super.attachBaseContext(base);
// 启动TraceView
Debug.startMethodTracing("cold-start");
}
@Override
public void onCreate() {
super.onCreate();
}
}
public class MainActivity extends AppCompatActivity {
private static final String TAG = "MainActivity";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
new LongTimeClass();
try {
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
String test = "";
for (int i = 0; i < 500; i++) {
test += i;
}
}
private class LongTimeClass {
public LongTimeClass() {
for (int i = 0; i < 10000; i++) {
Log.d(TAG, "LongTimeClass: ");
}
}
}
@Override
protected void onResume() {
super.onResume();
// 结束TraceView
Debug.stopMethodTracing();
}
}
冷启动该测试应用后,会在SD卡中生成一个cold-start.trace
的文件,我们把它拉到电脑上,用DDMS打开它。
无论是上面的时间轴,还是下面的方法区域,看起来都密密麻麻的,刚开始接触这个工具的时候看着就烦。
我们暂时不用看上面的时间轴,在下面的方法区域,点击Incl Cpu Time
,根据这个时间来排序,看不懂这些指标的请先看参考文章。可以看到前面那几行都是android相关的,这些我们并不是很关心,直接往下看,或者在下面的搜索框那里输入我们关心的onCreate
展开onCreate
后,可以看到谁调用它的,以及它都调用了谁。这里有个比较奇怪的地方就是Thread.sleep(50)
只占了0.146ms,但它实际上是会阻塞50ms的。下面的图中在后面有一段是空白的就是睡眠50ms。所以如果有一些操作太耗时我们也是可以通过时间线看出来的。
先来看耗时最长的LongTimeClass,点击它就会跳转到这个方法里面去,如图。
TraceView从下面的区域可以看到,调用它的是MainActivity的onCreate
方法,然后这个方法耗时最长的地方是在Log.d
方法上面。看到后面的10000/10001了吗?这里表示LongTimeClass的构造方法里面调用了Log.d
方法10000次,而Log.d
方法总共被调用了100001次,还有一次是在其它地方,同样的,点击Log.d
可以查看谁调用了它。
还有一点是,当我们刚才在onCreate
那里点击LongTimeClass进行跳转的时候,上面的时间轴会刷新,并标记出时间段,如图上面标记出来的地方,仔细看,可以看到这一段区间下面被括号括起来来,同时,上面也有一段红色的标记。反过来,我们点击时间轴上的方法,下面的方法区域也会跳转到相应的地方。
这里为什么要在类的构造方法中模拟耗时操作也是有原因的。我们在冷启动的时候,什么东西都没有,因此每个对象都需要去重新创建,而在类的构造方法中,可能经常会有耗时的操作是我们容易忽略掉的。还记得上面onBindeViewHolder
的例子吗?所以在冷启动阶段,我们要特别注意这一点。快速定位的方法就是先以Incl Cpu Time
条件来排序,再通过搜索框,搜索<init>
,这能把类的初始化给过滤出来,然后我们再看它的耗时情况。到了后面某个类不再耗时的时候也就不用继续搜索下去了。
看完LongTimeClass,我们再回到onCreate
方法中去,可以看到对于String的操作也是比较耗时的,这里也是有优化空间的。
上面的Log.d
其实也代表着一种情况,就是它本身并不是耗时的操作,但是它被调用了很多次,或者是递归调用了很多次。这种情况我们可以通过Calls+Recur Calls/Total
条件来排序。
总结
讲到这里,也就差不多了。最后再来一个总结。对于冷启动的分析,我们可以先通过systrace来分析,先有个大概的了解。在这个阶段,建议在关键的方法中打上自定义的标签。通过systrace大概知道哪些过程比较耗时的时候,就可以通过TraceView来分析了。这里再强调一遍,由于TraceView自身会对性能产生影响,所以它给出的时间并不准确,但这不妨碍我们进行分析,如果有必要,再在相应的地方打上标签再跑一次systrace就好了。
最后,冷启动优化是一个比较考验耐心的过程,不要指望着一次就能找出所有的问题来。并且就算找到了,有时候也是要根据产品的需求来做取舍的。
参考资料
测量Activity 的启动时间
性能工具Systrace
Android 性能优化:使用 TraceView 找到卡顿的元凶
手把手教你使用Systrace(一)