Android性能优化-检测App卡顿

移动APP性能评测-流畅度评测中,我们介绍了如何准确客观评价APP的流畅度,最终采用SM指标来评价应用的流畅度,在知道如何评价流畅度之后,我们应该如何来检测出APP中的UI卡顿就是我们面临的一个新的问题;在Android性能优化-App卡顿中介绍了Google官方提供的检测卡顿的方法,除此之外还有那边比较好的方法来检测应用卡顿?目前主流的方法主要有:
1.利用UI线程Looper打印的日志,典型代表就是BlockCanary;
2.采用Choreographer;
BlockCanary:blockcanary是国内开发者MarkZhai开发的一套性能监控组件,它对主线程操作进行了完全透明的监控,并能输出有效的信息,帮助开发分析、定位到问题所在,迅速优化应用;
BlockCanary核心原理:通过自定义一个Printer,设置到主线程ActivityThread的MainLooper中。MainLooper在dispatch消息前后都会调用Printer进行打印。从而获取前后执行的时间差值,判断是否超过设置的阈值。如果超过,则会将记录的栈信息及cpu信息发通知到前台。和利用UI线程Looper打印日志原理一样;
下面通过Blockcanary来简单介绍它是如何来检测应用卡顿的,然后简单介绍通过Choreographer来检测应用卡顿;

Blockcanary检测APP卡顿

GitHub地址:BlockCanary
Blog in Chinese: BlockCanary.
blockcanary源码学习随笔
BlockCanary原理图如下图所示:

BlockCanary原理图.png

其中最核心的两步是在调用msg.target.dispatchMessage(msg),进行消息的分发前记录时间T1,调用msg.target.dispatchMessage(msg)进行消息分发后记录时间T2,如果T2-T1大于设置的卡顿阈值就会打印当前方法调用堆栈以及显示其他相关提示或打印日志;
blockcanary充分的利用了Loop的机制,在MainLooper的loop方法中执行dispatchMessage前后都会执行printer的println进行输出,并且提供了方法设置printer。通过分析前后打印的时差与阈值进行比对,从而判定是否卡顿。下面我们来看一下Looper中的loop方法;

Looper.java
    public static void loop() {
        // 获取一个Looper对象
        final Looper me = myLooper();
        if (me == null) {
            throw new RuntimeException("No Looper; Looper.prepare() wasn't called on this thread.");
        }
        // 获取Looper中的消息队列
        final MessageQueue queue = me.mQueue;
        // 死循环,对消息队列里面的消息进行遍历
        for (;;) {
            // 通过queue.next()取出消息,消息是在Handler.sendMessage方法中存到消息队列里的
            Message msg = queue.next(); // might block
            if (msg == null) {
                // No message indicates that the message queue is quitting.
                return;
            }

            // This must be in a local variable, in case a UI event sets the logger
            final Printer logging = me.mLogging;
            if (logging != null) {
                //用户设置自己的Printer,在消息分发前调用Printer打印相关信息,此时获取消息分发前的时间T1;
                logging.println(">>>>> Dispatching to " + msg.target + " " +
                        msg.callback + ": " + msg.what);
            }

            final long slowDispatchThresholdMs = me.mSlowDispatchThresholdMs;

            final long traceTag = me.mTraceTag;
            if (traceTag != 0 && Trace.isTagEnabled(traceTag)) {
                Trace.traceBegin(traceTag, msg.target.getTraceName(msg));
            }
            final long start = (slowDispatchThresholdMs == 0) ? 0 : SystemClock.uptimeMillis();
            final long end;
            try {
                // 调用msg.target.dispatchMessage(msg),进行消息的分发。这里的msg.target就是发送这条消息的Handler对象。
                // 这样Handler发送的消息最终又交回到它的dispatchMessage方法来处理。不同的是,Handler的dispatchMessage
                // 方法是在创建Handler时所使用的Looper中执行的,这样就成功将代码逻辑切换到指定线程中去执行了。
                msg.target.dispatchMessage(msg);
                end = (slowDispatchThresholdMs == 0) ? 0 : SystemClock.uptimeMillis();
            } finally {
                if (traceTag != 0) {
                    Trace.traceEnd(traceTag);
                }
            }
            if (logging != null) {
                //消息分发完成后,调用用户自己设置的Printer.println()方法,此时获取消息分发之后时间T2;
                logging.println("<<<<< Finished to " + msg.target + " " + msg.callback);
            }
        }
    }
    
     /**
     * Control logging of messages as they are processed by this Looper.  If
     * enabled, a log message will be written to <var>printer</var>
     * at the beginning and ending of each message dispatch, identifying the
     * target Handler and message contents.
     *
     * @param printer A Printer object that will receive log messages, or
     * null to disable message logging.
     * 用户可以设置自己的Printer,这样在知道消息分发前后的时间,
     * 通过前后的时差与阈值进行对比,从而确定是否发生了卡顿
     */
    public void setMessageLogging(@Nullable Printer printer) {
        mLogging = printer;
    }

通过设置Printer我们可以检测msg.target.dispatchMessage(msg)执行时间,这样就可以知道部分UI线程是否有耗时操作了。
BlockCanary的LooperMonitor的println方法如下:

LooperMonitor
@Override
    public void println(String x) {
        if (!mPrintingStarted) {
            //dispatchMesage前执行的println
            //记录开始时间
            mStartTimestamp = System.currentTimeMillis();
            mStartThreadTimestamp = SystemClock.currentThreadTimeMillis();
            mPrintingStarted = true;
            //开始采集栈及cpu信息,最终会调用Stacksampler.start()方法;
            startDump();
        } else {
            //dispatchMesage后执行的println
            //获取结束时间
            final long endTime = System.currentTimeMillis();
            mPrintingStarted = false;
            //判断耗时是否超过阈值
            if (isBlock(endTime)) {
                notifyBlockEvent(endTime);
            }
           //最终会调用Stacksampler.stop()方法;
            stopDump();
        }
    }
    //判断是否超过阈值
    private boolean isBlock(long endTime) {
        return endTime - mStartTimestamp > mBlockThresholdMillis;
    }
    //回调监听
    private void notifyBlockEvent(final long endTime) {
        final long startTime = mStartTimestamp;
        final long startThreadTime = mStartThreadTimestamp;
        final long endThreadTime = SystemClock.currentThreadTimeMillis();
        HandlerThreadFactory.getWriteLogThreadHandler().post(new Runnable() {
            @Override
            public void run() {
                mBlockListener.onBlockEvent(startTime, endTime, startThreadTime, endThreadTime);
            }
        });
    }

其中在startDump方法最终会调用Stacksampler.start()方法;stopDump最终会调用Stacksampler.stop()方法;相关方法如下:

Stacksampler
    public void start() {
        //在mRunable进行信息采集;  
        HandlerThreadFactory.getTimerThreadHandler().removeCallbacks(mRunnable);
        //通过一个HandlerThread延时执行了mRunnable
        HandlerThreadFactory.getTimerThreadHandler().postDelayed(mRunnable,
                BlockCanaryInternals.getInstance().getSampleDelay());
    }
    public void stop() {
        //取消handler消息,如果未超时就不会采集相关信息
        HandlerThreadFactory.getTimerThreadHandler().removeCallbacks(mRunnable);
    }

在开始进行msg.target.dispatchMessage(msg)消息分发前通过HandlerThread发送一个延时runable,在msg.target.dispatchMessage(msg)消息分发后会remove该runable,如果指定的时间消息分发没有完成,说明应用发生了卡顿,这之后开始执行mRunable,在mRunable进行相关信息采集及提示APP发生卡顿;以上就是BlockCanary监测卡顿的核心原理;

利用Choreographer监测APP卡顿

Android系统每隔16ms发出VSYNC信号,触发对UI进行渲染。开发者可以使用Choreographer#postFrameCallback设置自己的callback与Choreographer交互,你设置的FrameCallCack(doFrame方法)会在下一个frame被渲染时触发。理论上来说两次回调的时间周期应该在16ms,如果超过了16ms我们则认为发生了卡顿,我们主要就是利用两次回调间的时间周期来判断,

    Choreographer.getInstance()
        .postFrameCallback(new Choreographer.FrameCallback() {
            @Override
            public void doFrame(long l) {
                //移除消息
                Handler.removeMessage();
                //发送延时消息
                Hnadler.sendMessageAtTime(...)
                Choreographer.getInstance().postFrameCallback(this);
            }
    });

发送的延时消息在执行的时间没有被remove掉,说明发生了卡顿,这时候可以进行卡顿相关信息的采集,如果在渲染下一帧的时候该消息还没有被处理,这时候将该消息remove掉,此场景说明未发生卡顿;该检测卡顿的思想和BlockCanary类似;
最后,我们可以结合上述原理以及自己需求开发出一个适合自己的卡顿监测方案,也可以参考已有开源方案。

其它

为什么主线程Looper.loop进行消息分发耗时就代表APP卡顿?
答:为了保证应用的平滑性,每一帧渲染时间不能超过16ms,达到60帧每秒;如果UI渲染慢的话,就会发生丢帧,这样用户就会感觉到不连贯性,我们称之为Jank(APP卡顿);VSync信号由SurfaceFlinger实现并定时发送(每16ms发送),Choreographer.FrameDisplayEventReceiver收到信号后,调用onVsync方法组织消息发送到主线程处理。Choreographer主要功能是当收到VSync信号时,去调用使用通过postCallBack设置的回调函数,在postCallBack调用doFrame,在doFrame中渲染下一帧;FrameDisplayEventReceiver相关代码如下:

Choreographer.java
    /**
     * FrameDisplayEventReceiver继承自DisplayEventReceiver接收底层的VSync信号开始处理UI过程。
     * VSync信号由SurfaceFlinger实现并定时发送。FrameDisplayEventReceiver收到信号后,
     * 调用onVsync方法组织消息发送到主线程处理。这个消息主要内容就是run方法里面的doFrame了,
     * 这里mTimestampNanos是信号到来的时间参数。
     */
    private final class FrameDisplayEventReceiver extends DisplayEventReceiver
            implements Runnable {
        private boolean mHavePendingVsync;
        private long mTimestampNanos;
        private int mFrame;

        public FrameDisplayEventReceiver(Looper looper, int vsyncSource) {
            super(looper, vsyncSource);
        }

        @Override
        public void onVsync(long timestampNanos, int builtInDisplayId, int frame) {
            mTimestampNanos = timestampNanos;
            mFrame = frame;
            // 发送Runnable(callback参数即当前对象FrameDisplayEventReceiver)到FrameHandler,请求执行doFrame
            Message msg = Message.obtain(mHandler, this);
            msg.setAsynchronous(true);
            // 此处mHandler为FrameHandler,该Handler对应的Looper是主线程的Looper
            mHandler.sendMessageAtTime(msg, timestampNanos / TimeUtils.NANOS_PER_MS);
        }

        @Override
        public void run() {
            mHavePendingVsync = false;
            doFrame(mTimestampNanos, mFrame);
        }
    }

在mHandler.sendMessageAtTime发送消息之后,最终会在主线程的Looper.loop()方法中调用msg.target.dispatchMessage(msg);Looper.loop相关代码可以参考在本文上边进行查看;然后在Handler.dispatchMeassange分发消息,如下所示:

Handler.java
    public void dispatchMessage(Message msg) {
        // Message的callback实际上就是Handler的post方法所传递的Runnable参数
        // 这里首先检查是否有由Runnable封装的消息,如果有,首先处理;
        if (msg.callback != null) {
            handleCallback(msg);
        } else {
            // 其次处理mCallback
            if (mCallback != null) {
                // 如果mCallback的handleMessage方法返回true,那么handler中的handleMessage方法是不会被执行的
                if (mCallback.handleMessage(msg)) {
                    return;
                }
            }
            handleMessage(msg);
        }
    }
    private static void handleCallback(Message message) {
        //在此执行FrameDisplayEventReceiver中的run方法,最终执行doFrame渲染下一帧;
        message.callback.run();
    }

通过以上流程可以发现,Android渲染每一帧都是通过消息机制来实现的,最终都会在主线Looper.loop()方法中开始渲染下一帧,因为Looper.loop方法在进行消息分发时是串行执行的,这样如果上一个消息分发时间过长即msg.target.dispatchMessage(msg)执行时间过长,就会导致在VSYNC到来时进行下一帧渲染延迟执行,就不能保证该帧在16ms内完成渲染,从而导致丢帧;所以主线程Looper.loop方法中msg.target.dispatchMessage(msg)执行时间过长就会导致APP卡顿;因此通过检测msg.target.dispatchMessage(msg)执行时间就可以检测APP卡顿;
Android消息机制的重要性
1.在卡顿监测会用到消息机制;主要是发送一个延时消息来监测是否,在执行时间内没有remove该消息就代码APP发生卡顿;
2.ANR监测也是通过发送一个延时消息来监测是否发生ANR;ANR是APP卡顿的极端情况;
3.View监测事件是否长按也用到消息机制,在发生Down的时候会发送一个延时消息,在Up的时候会将该消息Remove掉,如果指定的时间没有发生UP就会触发长按事件;
4.Choreographer在渲染每一帧的时候也是通过发送一个消息,然后在Looper.loop中处理下一个消息时才会去渲染下一帧;
5.Activity生命周期的控制也是在ActivityThread发送不同的消息来切换Activity生命周期;
6.消息机制可以将一个任务切换到其它指定的线程,如AsyncTask;
以上这些场景都用到Android消息机制,还有很多其他未知的场景可能也会用到Android消息机制,所以消息机制在Android中具有很重要的地位;

参考资料
鸿洋:Android UI性能优化 检测应用中的UI卡顿
BlockCanary GitHub地址
Blog in Chinese: BlockCanary.
blockcanary源码学习随笔
Android Choreographer 源码分析 讲的很好很重要

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,547评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,399评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,428评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,599评论 1 274
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,612评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,577评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,941评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,603评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,852评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,605评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,693评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,375评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,955评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,936评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,172评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,970评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,414评论 2 342

推荐阅读更多精彩内容