[036]Choreographer Skipped真正含义

前言

我相信大家肯定遇到过下面这个LOG,表面意思是在主线程做了太多的事,但是可能你们不知道这条LOG的真实含义。换句话说你们能写一个Demo生成这条LOG吗?我建议你们先写写看,再继续看下去。

Choreographer: Skipped 60 frames!  The application may be doing too much work on its main thread.

一、可能是大多数人的Demo

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main2);
    }

    @Override
    protected void onResume() {
        Log.d("KobeWang", "MainActivity : onResume start");
        super.onResume();
        try {
            Thread.sleep(1000);
        } catch (Exception e) {

        }
        Log.d("KobeWang", "MainActivity : onResume end");
    }
}

我相信大多数人写的Demo是上面这样子的,但是很遗憾,不会出现Choreographer Skipped的LOG,就算把1000ms改成10s,也不会出现。

03-11 19:01:25.679 15733 15733 D KobeWang: MainActivity : onResume start
03-11 19:01:26.682 15733 15733 D KobeWang: MainActivity : onResume end

二、Choreographer Skipped

2.1 那我们来分析一下Choreographer Skipped生成的流程

    private Choreographer(Looper looper, int vsyncSource) {
        ...
        //每一帧的时间,如果60hz的屏幕,就是16ms,但是这里是微秒
        mFrameIntervalNanos = (long)(1000000000 / getRefreshRate())
        ...
    }

    private final class FrameDisplayEventReceiver extends DisplayEventReceiver
            implements Runnable {

        private long mTimestampNanos;
        private int mFrame;

        @Override
        public void onVsync(long timestampNanos, long physicalDisplayId, int frame) {
            ...
            //onVsync方法将会在Vsync信号接收之后被回调
            //mTimestampNanos就是这次Vsync信号接收的时间
            mTimestampNanos = timestampNanos;
            mFrame = frame;
            //往主线程的Looper中投放一个Asynchronous的Message,callback为this
            //这个Message被处理的时候就会调用下面run-doFrame的方法
            Message msg = Message.obtain(mHandler, this);
            msg.setAsynchronous(true);
            mHandler.sendMessageAtTime(msg, timestampNanos / TimeUtils.NANOS_PER_MS);
        }

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

    void doFrame(long frameTimeNanos, int frame) {
        final long startNanos;
        synchronized (mLock) {
            ...
            //intendedFrameTimeNanos就是预计执行doFrame的时间点
            long intendedFrameTimeNanos = frameTimeNanos;
            //startNanos就是实际执行doFrame的时间点
            startNanos = System.nanoTime();
            //计算实际和预计的时间差
            final long jitterNanos = startNanos - frameTimeNanos;
            //如果时间差大于一帧的时间,也就是16ms
            if (jitterNanos >= mFrameIntervalNanos) {
                //计算丢掉的帧数=时间差/一帧的时长
                final long skippedFrames = jitterNanos / mFrameIntervalNanos;
                //SKIPPED_FRAME_WARNING_LIMIT默认是30
                if (skippedFrames >= SKIPPED_FRAME_WARNING_LIMIT) {
                    //如果丢掉的帧数大于30帧就打印LOG
                    Log.i(TAG, "Skipped " + skippedFrames + " frames!  "
                            + "The application may be doing too much work on its main thread.");
                }
             
            }
            ...
        }
    }

第一步:onVsync方法中保存Vsync信号接收的时间点timestampNanos
第二步:doFrame方法中计算时间差jitterNanos=当前时间-timestampNanos
第三步:如果时间差jitterNanos大于mFrameIntervalNanos(16ms),计算丢失的帧数skippedFrames=jitterNanos/mFrameIntervalNanos
第四步:如果skippedFrames大于30帧,打印Choreographer Skipped

2.2 onVsync方法什么时候被调用

监听到Vsync信号就会调用onVsync

如何监听Vsync信号?
首先要调用ViewRootImpl.scheduleTraversals,对下一个Vsync信号进行监听。
MainActivity的onResume完成之后就会触发ViewRootImpl.scheduleTraversals。

有兴趣的可以看看下面整个调用逻辑

KobeWang: java.lang.Exception: KobeWang
KobeWang:   at android.view.ViewRootImpl.scheduleTraversals(ViewRootImpl.java:1694)
KobeWang:   at android.view.ViewRootImpl.requestLayout(ViewRootImpl.java:1425)
KobeWang:   at android.view.ViewRootImpl.setView(ViewRootImpl.java:856)
KobeWang:   at android.view.WindowManagerGlobal.addView(WindowManagerGlobal.java:387)
KobeWang:   at android.view.WindowManagerImpl.addView(WindowManagerImpl.java:96)
KobeWang:   at android.app.ActivityThread.handleResumeActivity(ActivityThread.java:4399)
KobeWang:   at android.app.servertransaction.ResumeActivityItem.execute(ResumeActivityItem.java:52)
KobeWang:   at android.app.servertransaction.TransactionExecutor.executeLifecycleState(TransactionExecutor.java:176)
KobeWang:   at android.app.servertransaction.TransactionExecutor.execute(TransactionExecutor.java:97)
KobeWang:   at android.app.ActivityThread$H.handleMessage(ActivityThread.java:2102)
KobeWang:   at android.os.Handler.dispatchMessage(Handler.java:107)
KobeWang:   at android.os.Looper.loop(Looper.java:214)
KobeWang:   at android.app.ActivityThread.main(ActivityThread.java:7501)
KobeWang:   at java.lang.reflect.Method.invoke(Native Method)
KobeWang:   at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:492)
KobeWang:   at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:935)
小结:onVsync方法将会在onResume完成之后的下一个Vsync信号接收的时候被调用。

2.3 doFrame方法什么时候被调用

onVsync方法中往主线程的Looper中投放一个Asynchronous的Message。
这个Asynchronous的Message什么时候被处理,doFrame就是什么时候被调用。

整个Message的处理流程有兴趣的可以看我朋友的文章
https://zhenkunhuang.github.io/2019/05/05/android-handler-java/

小结:主线程Looper会是按照Message.when时间以及入队顺序执行,如果遇到一个同步屏障的message,从队列中选取最靠前的Asynchronous的Message优先处理。

Asynchronous的Message是Android隐藏接口,主要是系统用于UI显示的Message,从侧面也是看出安卓设计的初衷UI相关的Message优先处理。

2.4 总结

Choreographer Skipped真正反映的是onVsync和doFrame两个方法调用的时间间隔

其实这句话并不准确,我在[037]Choreographer Skipped含义再探中进行了修正。

三、我的Demo

前面的Demo为什么会失败是因为在onResume中sleep 1000ms,只能整体推迟调用onVsync和doFrame而已,并不能增加onVsync和doFrame之间调用的时间。

如何增加onVsync和doFrame之间调用的时间

如果在onVsync方法中投放Asynchronous的Message之前,主线程Looper有未处理的Message,就可以增加onVsync和doFrame两个方法调用的时间间隔,就会打印出Choreographer Skipped的LOG。

我写了如下的Demo

public class MainActivity extends AppCompatActivity {

    private Handler mHandler = new Handler();

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main2);
    }

    @Override
    protected void onResume() {
        Log.d("KobeWang", "MainActivity : onResume start");
        super.onResume();
        mHandler.post(new Runnable() {
            @Override
            public void run() {
                try {
                    Thread.sleep(1000);
                } catch (Exception e) {

                }
            }
        });
        Log.d("KobeWang", "MainActivity : onResume end");
    }
}

我在onResume完成之前,post了一个Runnable,也就是普通Message,并且Runnable休眠了1000ms。看我运行的LOG,果然出现了 Skipped 60 frames,正好和休眠的1000ms对应上。

2020-03-11 19:19:38.281 19118-19118/com.kobe.jankblock D/KobeWang: MainActivity : onResume start
2020-03-11 19:19:38.281 19118-19118/com.kobe.jankblock D/KobeWang: MainActivity : onResume end
2020-03-11 19:19:39.294 19118-19118/com.kobe.jankblock I/Choreographer: Skipped 60 frames!  The application may be doing too much work on its main thread.

四、为什么要理解Choreographer Skipped

因为在解决性能问题的时候,尤其是解决启动慢,丢帧问题的时候,大家要完整的梳理整个主线程中处理所有的Message任务,然后自己想办法写出Demo模拟,这样子才能从根本上解决这类性能问题,而不是简单的一句“少在主线程中做事情”,就可以解决问题。我在[035] onStop提前投放问题中就按照这个思路解决了一个启动慢的问题。

尾巴

[037]Choreographer Skipped含义再探中我会介绍另外一种Demo。

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

推荐阅读更多精彩内容