Android coder进阶Android开发Android开发经验谈

应用冷启动优化分析

2017-12-01  本文已影响129人  事多店

前言

关于冷启动的优化方法,网上已经有很多的文章了,总结起来,大概有以下几种优化方式:

  1. 优化布局,这一步是最简单的,然而如果你的布局不是特别重的话其实优化后效果也不明显。
  2. 异步加载,现在一个app都会使用各种各样的第三方SDK,这些SDK大都需要在Application中去初始化,如果能让这些SDK在工作线程中去初始化的话,能减轻不少主线程的负担。
  3. 懒加载,常见的场景是ViewPager + Fragment,或者是一些资源等到使用的时候再去加载。
  4. 预加载,如果有图片的话可以尝试着去预加载,不要等到布局的时候才去加载,这一步往往能够省出很多时间,因为解码图片是一个比较耗时的操作,特别是对于比较低端的机型。

道理大家都懂,但是怎么去分析,找出这些优化点出来,网上的文章还是比较少的,或者说得没有那么详细。所以,这篇文章主要是记录下最近几个月来,做冷启动优化的一些分析的思路,文章篇幅可能有点长,希望看完之后,对你能有一些帮助。

通过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的按钮,然后会出现一个弹窗。

Systrace 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的onCreateViewHolderonBindViewHolder这些关键的地方。

@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中有一帧是这样子的:

Systrace

发现了吗?同样都是onBindViewHolder,第一次跟后面七次的总时间加起来差不多,执行时间是50ms,占了这一帧的一半,说明这里肯定是有问题的。这就是systrace的好处,它能让我们很直观的发现到一些问题。同样的,利用systrace,我也发现了项目在Application的onCreate耗时太长,这点不通过systrace其实也很容易找出来,因为在这个回调里面都是一些初始化操作。相反,像上面的第一次onBindViewHolder耗时太长就比较难发现了,最后我查出来的原因是第一次调用时会用到一个Util类,它有一块静态的代码,在这里面去加载so库导致了耗时过长。

使用systrace分析冷启动的使用就到这里,关于它的使用还是要多积累经验才能得心应手。

另外,从systrace上,你会发现inflate一个布局是一个比较耗时的操作,并且如果布局里面有使用到图片资源的话还需要去解码这些资源,也都是耗时的操作。所以这其实也是一个优化点来着。

通过 TraceView 分析应用冷启动

通过Systrace,虽然我们可以大致知道是在哪个流程慢了,比如说bindApplicationActivityStart或者是整个布局绘制流程慢了。但我们能知道的,也仅限于此了。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打开它。

TraceView

无论是上面的时间轴,还是下面的方法区域,看起来都密密麻麻的,刚开始接触这个工具的时候看着就烦。

我们暂时不用看上面的时间轴,在下面的方法区域,点击Incl Cpu Time,根据这个时间来排序,看不懂这些指标的请先看参考文章。可以看到前面那几行都是android相关的,这些我们并不是很关心,直接往下看,或者在下面的搜索框那里输入我们关心的onCreate

TraceView

展开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的操作也是比较耗时的,这里也是有优化空间的。

TraceView

上面的Log.d其实也代表着一种情况,就是它本身并不是耗时的操作,但是它被调用了很多次,或者是递归调用了很多次。这种情况我们可以通过Calls+Recur Calls/Total条件来排序。

总结

讲到这里,也就差不多了。最后再来一个总结。对于冷启动的分析,我们可以先通过systrace来分析,先有个大概的了解。在这个阶段,建议在关键的方法中打上自定义的标签。通过systrace大概知道哪些过程比较耗时的时候,就可以通过TraceView来分析了。这里再强调一遍,由于TraceView自身会对性能产生影响,所以它给出的时间并不准确,但这不妨碍我们进行分析,如果有必要,再在相应的地方打上标签再跑一次systrace就好了。

最后,冷启动优化是一个比较考验耐心的过程,不要指望着一次就能找出所有的问题来。并且就算找到了,有时候也是要根据产品的需求来做取舍的。

参考资料

测量Activity 的启动时间
性能工具Systrace
Android 性能优化:使用 TraceView 找到卡顿的元凶
手把手教你使用Systrace(一)

上一篇下一篇

猜你喜欢

热点阅读