Android App秒开的奥秘

什么是秒开

Android App秒开,狭义的讲是指你的App的Activity从启动到显示所花费的时间在1秒以内,广义的讲是指这个过程所花费的时间越少越好。这个时间越短,你的App给用户的感觉就是响应越快,使用越流畅,用户体验更好。秒开是Android App的一个很重要的性能指标。需要我们持续的给予关注和优化。

如何优化秒开

Google提供了很多性能优化的建议和官方的工具,网上也有非常多的关于Android App性能优化的文章和工具,可以帮助你解决大部分卡顿的问题。但是现实却可能是即使你付出了很多精力去做优化,你的App还是在启动新Activity的时候花费过多的时间。特别是随着需求的不断增长,你的App会变得复杂而庞大,要做优化首先要定位需要优化的点,而这会变得愈发困难。同时大型App在启动新Activity的时间花费过多情况出现的可能性反而会越来越大。

在众多的优化建议中,有一条比较基本的原则是尽量避免在主线程(或者说UI线程)中进行耗时操作。例如文件读写操作、网络请求、大量计算、循环等等。直观的理解是因为启动新Activity需要在主线程执行很多代码,例如onCreate()等生命周期的回调。如果此时有耗时操作的代码在主线程被执行,到新Activity展示出来所需要的时间就会延长。要优化秒开,首先要能监测主线程的运行状态,那么问题来了,主线程到底是怎样在运行呢?你的代码又是什么时候,如何在主线程被执行的呢?

深入主线程

要了解主线程的工作过程,首先要了解Android的消息机制。

消息机制

先看一下现实生活中的一个例子,虽然现在都是移动支付了,但相信大家都去银行取过钱。当你到达银行的时候,如果你是第一个,那恭喜你,你可以马上到柜员那里办理你的业务;如果你前面还有人,那就比较惨了,你需要排队,得等到你前面的人都办完业务才会轮到你;更可怕的是如果你前面有几位需要办理的业务花费的时间比较长,那你需要等更长的时间;后面来的人则会按顺序排在你身后,和你一样不耐烦的琢磨什么时候才能轮到自己。

抽象一下,消息机制其实和这个例子十分类似。每个人都看做是个消息,什么时候到的银行是不确定的。柜员可以看做一个消息处理器,他帮你办业务就相当于在处理你的消息;而人们按照先后顺序排起来的队伍可以看做是个消息队列。所以这个过程可以抽象为有个消息处理器,他有个消息队列,随机来到的消息按照一定顺序排列在这个队列里,消息处理器不停的从队列头部获取消息然后处理之,周而复始的循环重复这个过程。如下图所示:


消息机制

那么Android是怎样怎样实现这个消息机制的呢?

Android的消息机制

消息机制首先得有消息,在Android中就是Message。怎样能确定一个消息呢?消息要有来源或者目标,也就是target;消息要表明自己要做什么,也就是what或者callback;消息要表明自己希望在什么时候执行,也就是when。有了这几个要素,基本上这个消息就是个完备的消息了,可以被加入到消息队列中了。Android中的消息队列是MessageQueue。消息处理循环是Looper。Looper是个死循环,不停的从MessageQueue中获取消息然后处理之,具体的执行是在Handler里面进行的。另外消息加入消息队列也需要Handler来操作。Message,MessageQueue,Looper,Handler组合在一起,就构成了整个Android的消息机制。

Android的主线程就运行着这样一个消息机制。

Android的主线程

主线程是在ActivityThread中创建的,可以看到在main函数中

public static void main(String[] args) {
        ...
        Looper.prepareMainLooper();
        ...
        Looper.loop();
    }

主线程实现了一个消息机制。所以Android的主线程就是个消息处理的循环。它所做的工作就是在不停的从消息队列获取消息,处理消息,周而复始。你的App所有的在UI上的操作,例如点击事件的处理、页面动画、显示更新页面、View绘制、启动新Activity等操作都是在给主线程发消息,主线程然后挨个处理这些消息。

主线程如何影响秒开

我们了解了主线程的工作机制后,就要看看主线程中的消息处理是如何影响Activity秒开的。
当我们要启动一个新的Activity的时候,从调用startActivity开始到新Activity显示出来,Android系统会发送一系列的消息给主线程。这一系列的消息处理所花费的总时间会影响页面的秒开,如果执行时间过长,用户就会有响应非常慢的感觉。此外,除了Android系统会给主线程发消息,App自身也会给主线程发消息,如果在启动新Activity的过程中,这些App自己的消息正好插入这一系列的Android系统消息中,那也会导致总的处理时间延长,造成不能秒开。


秒开示意

上图代表了启动新Activity的主线程的三种情况,每个矩形代表主线程处理一个消息所花的时间,越宽代表处理的时间越长。绿色填充的代表这是一个Android系统发过来的消息;蓝色填充的代表这是一个App自己发过来的消息。最下方的向右箭头代表时间,起点是startActivity被调用的时刻。

  • 第一种状况代表正常的情形,主线程中只有和startActivity相关的系统消息被处理,而且处理每个消息所花费的时间都在合理范围内。所以这个页面可以满足秒开。
  • 第二种情况代表一个异常的情形,虽然主线程处理的消息都是系统消息,但是某一个或某几个消息的处理时间超出了合理值,导致页面不能秒开。
  • 第三种情况代表另一种异常的情形,在系统消息中混入了App自己的消息,主线程不仅要处理系统消息,还要处理App自己的消息,结果就是总的启动时间要额外加上App消息的处理时间,导致页面不能秒开。
    实际情况中还有可能会出现既有系统消息处理时间过长同时也混有App自己的消息的情形。

秒开优化

了解了影响秒开的因素之后,我们只要有办法能监测主线程中每个消息处理时间,我们就能定位到造成页面卡慢的原因,然后再做优化。
幸好Android工程师为我们在Looper中预留了打log的位置。

public static void loop() {
        final Looper me = myLooper();
        ...
        final MessageQueue queue = me.mQueue;
        ...
        for (;;) {
            Message msg = queue.next(); // might block
           ...
            Printer logging = me.mLogging;
            if (logging != null) {
                logging.println(">>>>> Dispatching to " + msg.target + " " +
                        msg.callback + ": " + msg.what);
            }

            msg.target.dispatchMessage(msg);

            if (logging != null) {
                logging.println("<<<<< Finished to " + msg.target + " " + msg.callback);
            }
           ...
            msg.recycleUnchecked();
        }
    }

public void setMessageLogging(@Nullable Printer printer) {
        mLogging = printer;
    }

可见在消息被处理的开始和处理结束之后都会打印log。
你只需要在代码中调用Looper.setMessageLogging()设置一下就好。

Looper.getMainLooper().setMessageLogging(new Printer() {
                @Override
                public void println(String s) {   
                    Log.v("debug", s);
                }
            });

编译运行你的程序,你会在logcat输出看到类似这样的log:


Message logging

每行 “>>>>> Dispatching to”开头的log代表一个消息即将开始被处理;紧接着下一行“<<<<< Finished to”开头的log代表这一消息处理完毕。通过这些log你可以知道所有被主线程处理的消息,并可以根据开始结束的时间差知道每个消息消耗的时间。有了这些信息你可以找到导致你的app卡慢的消息,然后进一步去debug问题。

在你启动一个新的Activity的时候你可以观测这样的log输出,看看里面有没有处理时间比较长的消息,或者看看里面有没有App自己的消息被处理,如果有的话,这些都是需要优化的点。

然而直接看log的缺点是这样的log会比较多,而且并不容易定位启动Activity的开始和结束时间点,另外每个消息处理的时间也要自己计算,并不是十分直观。

StallBuster

为了方便的进行秒开优化,我做了个工具叫StallBuster来协助定位Activity秒开失败的原因。

集成StallBuster非常简单,只需要两步就可以了

  1. 添加对StallBuster的依赖
dependencies {
    compile 'com.github.zhangjianli:stallbuster:1.1'
}
  1. 在你的App的Application中添加以下代码
public class YourApplication extends Application {
    @Override
    public void onCreate() {
        StallBuster.getInstance().init(this);
        super.onCreate();
    }
}

这样就可以了,编译运行你的App。在你的App中打开新的Activity,StallBuster会发出一个Notification。告诉你刚启动这个Activity花了多少毫秒


notification

点击这个Notification就会打开StallBuster的历史记录页面。


records

这个页面按照时间顺序列出了你的App启动每个Activity的历史记录。每条记录最左边是启动所花费的时间。绿色代表所费时间符合秒开要求;红色代表时间太长。需要关注。右边是这条记录对应的Activity名称。点击某条记录就会进入详情页。
详情页

在详情页里你可以看到启动这个Activity的过程中主线程处理过的消息。上方的复选框可以过滤执行时间比较短的消息,方便定位问题。

对于每条记录,首先显示的是这条消息开始被处理的时间戳。然后是cost字段,表示处理这条消息花了多长时间。正常情况下是字体是黑色的;如果处理时间过长,则显示为红色。表明这里可能是我们需要优化的地方。

接下来是target字段,对应的是这个消息是被哪个Handler处理的。Android系统的Handler会显示为黑色;App自己的Handler会显示为红色,表明这个消息不应该在启动Activity的时候出现,这里也可能是需要优化的地方。

例如上图中第一条记录,.MainActivity$StallHandler处理这个消息花费了142ms。这会使启动SubActivity的时间至少延长了142ms。而这个Handler是App自己的Handler。我们需要调试代码使得在启动这个Activity的时候确保不会有来自这个Handler的消息,142ms的时间就会节省下来。

最后一个字段是message或者callback。对应的是Message中的what或callback。有了这些信息我们就能很方便的定位主线程中影响秒开的消息,进而优化我们的App。

StallBuster就给大家介绍到这里,希望StallBuster能帮到你。如果大家有任何建议或者问题请给我留言。

总结

App秒开是是一项非常重要的性能指标。秒开的优化是个复杂的工作,有很多因素会影响App秒开。其中比较重要的一个因素是启动Activity的时候主线程的消息处理情况。在启动Activity过程中需要避免消息处理时间过长,也要避免在此期间有App自己的消息需要处理。优化的关键点是要定位到主线程中的耗时操作,我们可以通过打印分析主线程的消息处理log来定位,但这种方式并不是很直观方便。这时可以使用StallBuster帮助你快速定位秒开问题点,让秒开优化变的更加简单。

推荐阅读更多精彩内容