Android App秒开的奥秘

作者: HowHardCanItBe | 来源:发表于2017-12-06 21:00 被阅读614次

    什么是秒开

    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帮助你快速定位秒开问题点,让秒开优化变的更加简单。

    相关文章

      网友评论

      • 明明很安静:那么问题来了,如果我把稍微耗时的操作,如很多东西的初始化操作放到onResume里面,会不会稍微好点,因为这个时候界面已经可见了
        HowHardCanItBe:@明明很安静 这个我认为是逻辑上和实现上的区别,就比如外卖小哥给你打电话说你的外卖到了,实际上你还得等小哥走到你家门口把外卖递给你,你再打开包装才真正见到你点的外卖。
        明明很安静:@HowHardCanItBe 我试了,确实如你所说,那么所谓的start可见,是指activity可见?不是指内容可见,如同画板和画的区别,这么理解对不对?
        HowHardCanItBe:然而并不是这样的,onCreate->onStart->onResume是在同一个系统消息中处理的。并且在onResume之后还会有渲染,Activity切换动画等系统消息要处理。其实onResume执行完毕的时候界面并不可见。你可以在onResume最后一行打个断点,看看那时候手机屏幕上Activity界面显示出来没有。
      • 1d65221a9608:关注了。
        希望博主可以持续发文
      • 狂奔的鸡翅膀:支付宝就是“秒开”的领军者。我的说是60秒:smile:
        码无止境:60s怎么还没卸载app
      • 00e20104771c:主工程和你的这些
        com.android.support:appcompat-v7
        com.android.support:recyclerview-v7
        版本不一致,无法编译,有好的解决办法吗?
        HowHardCanItBe:你先直接使用源码依赖,把版本号改成和你的工程一致

      本文标题:Android App秒开的奥秘

      本文链接:https://www.haomeiwen.com/subject/vcwqixtx.html