刷新机制_你真的了解16.6ms刷新机制吗?

作者:散人丶
来源:https://juejin.im/post/5ce686a46fb9a07ec754f470

前言

之前在整理知识的时候,看到android屏幕刷新机制这一块,以前一直只是知道,Android每16.6ms会去刷新一次屏幕,也就是我们常说的60fpx,那么问题也来了:

16.6ms刷新一次是什么一次,是以这个固定的频率去重新绘制吗?但是请求绘制的代码时机调用是不同的,如果操作是在16.6ms快结束的时候去绘制的,那么岂不是就是时间少于16.6ms,也会产生丢帧的问题?再者熟悉绘制的朋友都知道请求绘制是一个Message对象,那这个Message是会放进主线程Looper的队列中吗,那怎么能保证在16.6ms之内会执行到这个Message呢?

文章较长,请耐心观看,水平不足,如果错误,还望指出

View ## invalidate()

既然是绘制,那么就从这个方法看起吧

 1public void invalidate() {
2        invalidate(true);
3    }
4    public void invalidate(boolean invalidateCache) {
5        invalidateInternal(0, 0, mRight - mLeft, mBottom - mTop, invalidateCache, true);
6    }
7    void invalidateInternal(int l, int t, int r, int b, boolean invalidateCache, 8            boolean fullInvalidate) {
9            ......
10            final AttachInfo ai = mAttachInfo;
11            final ViewParent p = mParent;
12            if (p != null && ai != null && l 13                final Rect damage = ai.mTmpInvalRect;
14                damage.set(l, t, r, b);
15                p.invalidateChild(this, damage);
16            }
17           .....
18        }
19    }

主要关注这个p,最终调用的是它的invalidateChild()方法,那么这个p到底是个啥,ViewParent是一个接口,那很明显p是一个实现类,答案是ViewRootImpl,我们知道View树的根节点是DecorView,那DecorView的Parent是不是ViewRootImpl

熟悉Activity启动流程的朋友都知道,Activity 的启动是在 ActivityThread
里完成的,handleLaunchActivity() 会依次间接的执行到 Activity 的 onCreate()onStart(),onResume()。在执行完这些后 ActivityThread 会调用 WindowManager#addView(),而这个addView()最终其实是调用了 WindowManagerGlobal 的 addView()
方法,我们就从这里开始看,因为是隐藏类,所以这里借助Source Insight查看WindowManagerGlobal

 1public void addView(View view, ViewGroup.LayoutParams params, 2            Display display, Window parentWindow) {
3        synchronized (mLock) {
4            .....
5            root = new ViewRootImpl(view.getContext(), display);
6            view.setLayoutParams(wparams);
7            mViews.add(view);
8            mRoots.add(root);
9            mParams.add(wparams);
10        }
11        try {
12            root.setView(view, wparams, panelParentView);
13        } catch (RuntimeException e) {
14            // BadTokenException or InvalidDisplayException, clean up.
15            synchronized (mLock) {
16                final int index = findViewLocked(view, false);
17                if (index >= 0) {
18                    removeViewLocked(index, true);
19                }
20            }
21            throw e;
22        }
23    }
24 public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
25        synchronized (this) {
26                 ....
27                view.assignParent(this);
28                   ...
29            }
30        }
31void assignParent(ViewParent parent) {
32        if (mParent == null) {
33            mParent = parent;
34        } else if (parent == null) {
35            mParent = null;
36        } 
37      }

参数是ViewParent,所以在这里就直接将DecorViewViewRootImpl给绑定起来了,所以也验证了上述的结论,子 View
里执行 invalidate() 之类的操作,最后都会走到 ViewRootImpl 里来

ViewRootImpl##scheduleTraversals

根据上面的链路最终是会执行到scheduleTraversals方法

 1void scheduleTraversals() {
2        if (!mTraversalScheduled) {
3            mTraversalScheduled = true;
4            mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
5            mChoreographer.postCallback(
6                    Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
7            if (!mUnbufferedInputDispatch) {
8                scheduleConsumeBatchedInput();
9            }
10            notifyRendererOfFramePending();
11            pokeDrawLockIfNeeded();
12        }
13    }

方法不长,首先如果mTraversalScheduled为false,进入判断,同时将此标志位置位true,第二句暂时不管,后续会讲到,主要看postCallback方法,传递进去了一个mTraversalRunnable对象,可以看到这里是一个请求绘制的Runnable对象

 1final class TraversalRunnable implements Runnable {
2        @Override
3        public void run() {
4            doTraversal();
5        }
6    }
7void doTraversal() {
8        if (mTraversalScheduled) {
9            mTraversalScheduled = false;
10    mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);
11            if (mProfile) {
12                Debug.startMethodTracing("ViewAncestor");
13            }
14            performTraversals();
15            if (mProfile) {
16                Debug.stopMethodTracing();
17                mProfile = false;
18            }
19        }
20    }

doTraversal方法里面,又将mTraversalScheduled置位了false,对应上面的scheduleTraversals方法,可以看到一个是postSyncBarrier(),而在这里又是removeSyncBarrier(),这里其实涉及到一个很有意思的东西,叫同步屏障,等会会拉出来单独讲解,然后调用了performTraversals(),这个方法应该都知道了,View
的测量、布局、绘制都是在这个方法里面发起的,代码逻辑太多了,就不贴出来了,暂时只需要知道这个方法是发起测量的开始。

这里我们暂时总结一下,当子View调用invalidate的时候,最终是调用到ViewRootImpl的performTraversals()方法的,performTraversals()方法又是在doTraversal里面调用的,doTraversal又是封装在mTraversalRunnable之中的,那么这个Runnable的执行时机又在哪呢

Choreographer##postCallback

回到上面的scheduleTraversals方法中,mTraversalRunnable是传递进了Choreographer的postCallback方法之中

 1private void postCallbackDelayedInternal(int callbackType, 2            Object action, Object token, long delayMillis) {
3        if (DEBUG_FRAMES) {
4        synchronized (mLock) {
5            final long now = SystemClock.uptimeMillis();
6            final long dueTime = now + delayMillis;
7            mCallbackQueues[callbackType].addCallbackLocked(dueTime, action, token);
8
9            if (dueTime <= now) {
10                scheduleFrameLocked(now);
11            } else {
12                Message msg = mHandler.obtainMessage(MSG_DO_SCHEDULE_CALLBACK, action);
13                msg.arg1 = callbackType;
14                msg.setAsynchronous(true);
15                mHandler.sendMessageAtTime(msg, dueTime);
16            }
17        }
18    }

可以看到内部好像有一个类似MessageQueue的东西,将Runnable通过delay时间给存储起来的,因为我们这里传递进来的delay是0,所以执行scheduleFrameLocked(now)方法

 1private void scheduleFrameLocked(long now) {
2        if (!mFrameScheduled) {
3            mFrameScheduled = true;
4                if (isRunningOnLooperThreadLocked()) {
5                    scheduleVsyncLocked();
6                } else {
7                    Message msg = mHandler.obtainMessage(MSG_DO_SCHEDULE_VSYNC);
8                    msg.setAsynchronous(true);
9                    mHandler.sendMessageAtFrontOfQueue(msg);
10                }
11        }
12    }
13private boolean isRunningOnLooperThreadLocked() {
14        return Looper.myLooper() == mLooper;
15    }

这里有一个判断isRunningOnLooperThreadLocked,看着像是判断当前线程是否是主线程,如果是的话,调用scheduleVsyncLocked()方法,不是的话会发送一个MSG_DO_SCHEDULE_VSYNC消息,但是最终都会调用这个方法

1public void scheduleVsync() {
2        if (mReceiverPtr == 0) {
3            Log.w(TAG, "Attempted to schedule a vertical sync pulse but the display event "
4                    + "receiver has already been disposed.");
5        } else {
6            nativeScheduleVsync(mReceiverPtr);
7        }
8    }

如果mReceiverPtr不等于0的话,会去调用nativeScheduleVsync(mReceiverPtr),这是个native方法,暂不跟踪到C++里面去了,看着英文方法像是一个安排信号的意思

之前是把CallBack存储在一个Queue之中了,那么必然有执行的方法

 1void doCallbacks(int callbackType, long frameTimeNanos) {
2        CallbackRecord callbacks;
3        synchronized (mLock) {
4        try {
5            Trace.traceBegin(Trace.TRACE_TAG_VIEW, CALLBACK_TRACE_TITLES[callbackType]);
6            for (CallbackRecord c = callbacks; c != null; c = c.next) {
7                if (DEBUG_FRAMES) {
8                    Log.d(TAG, "RunCallback: type=" + callbackType
9                            + ", action=" + c.action + ", token=" + c.token
10                            + ", latencyMillis=" + (SystemClock.uptimeMillis() - c.dueTime));
11                }
12                c.run(frameTimeNanos);
13            }
14        } finally {
15            synchronized (mLock) {
16                mCallbacksRunning = false;
17                do {
18                    final CallbackRecord next = callbacks.next;
19                    recycleCallbackLocked(callbacks);
20                    callbacks = next;
21                } while (callbacks != null);
22            }
23            Trace.traceEnd(Trace.TRACE_TAG_VIEW);
24        }
25    }

看一下这个方法在哪里调用的,走到了doFrame方法里面

 1void doFrame(long frameTimeNanos, int frame) {
2        final long startNanos;
3        synchronized (mLock) {
4        try {
5             .....
6            mFrameInfo.markInputHandlingStart();
7            doCallbacks(Choreographer.CALLBACK_INPUT, frameTimeNanos);
8
9            mFrameInfo.markAnimationsStart();
10            doCallbacks(Choreographer.CALLBACK_ANIMATION, frameTimeNanos);
11
12            mFrameInfo.markPerformTraversalsStart();
13            doCallbacks(Choreographer.CALLBACK_TRAVERSAL, frameTimeNanos);
14
15            doCallbacks(Choreographer.CALLBACK_COMMIT, frameTimeNanos);
16        } finally {
17            AnimationUtils.unlockAnimationClock();
18            Trace.traceEnd(Trace.TRACE_TAG_VIEW);
19        }
20              .....
21    }

那么这样一来,就找到关键的方法了:doFrame(),这个方法里会根据一个时间戳去队列里取任务出来执行,而这个任务就是 ViewRootImpl
封装起来的 doTraversal() 操作,而 doTraversal() 会去调用 performTraversals()
开始根据需要测量、布局、绘制整颗 View 树。所以剩下的问题就是 doFrame() 这个方法在哪里被调用了。

 1private final class FrameDisplayEventReceiver extends DisplayEventReceiver 2            implements Runnable {
3        private boolean mHavePendingVsync;
4        private long mTimestampNanos;
5        private int mFrame;
6
7        public FrameDisplayEventReceiver(Looper looper, int vsyncSource) {
8            super(looper, vsyncSource);
9        }
10
11        @Override
12        public void onVsync(long timestampNanos, int builtInDisplayId, int frame) {     
13                scheduleVsync();
14                return;
15            }
16            mTimestampNanos = timestampNanos;
17            mFrame = frame;
18            Message msg = Message.obtain(mHandler, this);
19            msg.setAsynchronous(true);
20            mHandler.sendMessageAtTime(msg, timestampNanos / TimeUtils.NANOS_PER_MS);
21        }
22        @Override
23        public void run() {
24            mHavePendingVsync = false;
25            doFrame(mTimestampNanos, mFrame);
26        }
27    }

可以看到,是在onVsync回调中,Message msg = Message.obtain(mHandler,
this),传入了this,然后会执行到run方法里面,自然也就执行了doFrame方法,所以最终问题也就来了,这个onVsync()这个是什么时候回调来的,这里查询了网上的一些资料,是这么解释的

>
FrameDisplayEventReceiver继承自DisplayEventReceiver接收底层的VSync信号开始处理UI过程。VSync信号由SurfaceFlinger实现并定时发送。FrameDisplayEventReceiver收到信号后,调用onVsync方法组织消息发送到主线程处理。这个消息主要内容就是run方法里面的doFrame了,这里mTimestampNanos是信号到来的时间参数。

那也就是说,onVsync是底层回调回来的,那也就是每16.6ms,底层会发出一个屏幕刷新的信号,然后会回调到onVsync方法之中,但是有一点很奇怪,底层怎么知道上层是哪个app需要这个信号来刷新呢,结合日常开发,要实现这种一般使用观察者模式,将app本身注册下去,但是好像也没有看到哪里有往底层注册的方法,对了,再次回到上面的那个native方法,nativeScheduleVsync(mReceiverPtr),那么这个方法大体的作用也就是注册监听了,

同步屏障

总结下上面的知识,我们调用了 invalidate(),requestLayout(),等之类刷新界面的操作时,并不是马上就会执行这些刷新的操作,而是通过
ViewRootImpl 的 scheduleTraversals() 先向底层注册监听下一个屏幕刷新信号事件,然后等下一个屏幕刷新信号来的时候,才会去通过
performTraversals() 遍历绘制 View 树来执行这些刷新操作。

那么这样是不是产生一个问题,因为我们知道,平常Handler发送的消息都是同步消息的,也就是Looper会从MessageQueue中不断去取Message对象,一个Message处理完了之后,再去取下一个Message,那么绘制的这个Message如何尽量保证能够在16.6ms之内执行到呢,

这里就使用到了一个同步屏障的东西,再次回到scheduleTraversals代码

 1void scheduleTraversals() {
2        if (!mTraversalScheduled) {
3            mTraversalScheduled = true;
4            mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
5            mChoreographer.postCallback(
6                    Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
7            if (!mUnbufferedInputDispatch) {
8                scheduleConsumeBatchedInput();
9            }
10            notifyRendererOfFramePending();
11            pokeDrawLockIfNeeded();
12        }
13    }

mHandler.getLooper().getQueue().postSyncBarrier(),这一句上面没有分析,进入到方法里

 1private int postSyncBarrier(long when) {
2        synchronized (this) {
3            final int token = mNextBarrierToken++;
4            final Message msg = Message.obtain();
5            msg.markInUse();
6            msg.when = when;
7            msg.arg1 = token;
8            Message prev = null;
9            Message p = mMessages;
10            if (when != 0) {
11                while (p != null && p.when <= when) {
12                    prev = p;
13                    p = p.next;
14                }
15            }
16            if (prev != null) { // invariant: p == prev.next
17                msg.next = p;
18                prev.next = msg;
19            } else {
20                msg.next = p;
21                mMessages = msg;
22            }
23            return token;
24        }
25    }

可以看到,就是生成Message对象,然后进一些赋值,但是细心的朋友可能会发现,这个msg为什么没有设置target,熟悉Handler流程的朋友应该清楚,最终消息是通过msg.target发送出去的,一般是指Handler对象

那我们再次回到MessageQueue的next方法中看看

 1Message next() {
2        for (;;) {
3              ....
4            synchronized (this) {
5                   ...
6                //对,就是这里了,target==null
7                if (msg != null && msg.target == null) {                 
8                    do {
9                        prevMsg = msg;
10                        msg = msg.next;
11                    } while (msg != null && !msg.isAsynchronous());
12                }
13                if (msg != null) {
14                    if (now when) {                     
15                        nextPollTimeoutMillis = (int) Math.min(msg.when - now, Integer.MAX_VALUE);
16                    } else {
17                        // Got a message.
18                        mBlocked = false;
19                        if (prevMsg != null) {
20                            prevMsg.next = msg.next;
21                        } else {
22                            mMessages = msg.next;
23                        }
24                        msg.next = null;
25                        if (DEBUG) Log.v(TAG, "Returning message: " + msg);
26                        msg.markInUse();
27                        return msg;
28                    }
29                } else {                
30                    nextPollTimeoutMillis = -1;
31                }
32        }
33    }

可以看到有一个Message.target==null的判断, do
while循环遍历消息链表,跳出循环时,msg指向离表头最近的一个消息,此时返回msg对象

可以看到,当设置了同步屏障之后,next函数将会忽略所有的同步消息,返回异步消息。换句话说就是,设置了同步屏障之后,Handler只会处理异步消息。再换句话说,同步屏障为Handler消息机制增加了一种简单的优先级机制,异步消息的优先级要高于同步消息

这样的话,能够尽快的将绘制的Message给取出来执行,嗯,这里为什么说是尽快呢,因为,同步屏障是在 scheduleTraversals()
被调用时才发送到消息队列里的,也就是说,只有当某个 View 发起了刷新请求时,在这个时刻后面的同步消息才会被拦截掉。如果在scheduleTraversals() 之前就发送到消息队列里的工作仍然会按顺序依次被取出来执行

总结

  • View的刷新请求都会走到ViewRootImpl的scheduleTraversals() 中,通过Runnable的形式形成Message放进队列中,并且发送同步屏障,拦截所以同步消息,处理异步消息,以此尽快的保证执行刷新任务

  • 常说的每隔16.6ms刷新一次屏幕实际上是,底层会以这个频率来切换每一帧的画面,只有当View发起了刷新请求时,App才会想底层去注册监听下一个屏幕的刷新信号,并且才能受到下一次信号到来的通知回调onVsync

  • App负责计算屏幕刷新的数据,但是并非计算完成后就会立即刷新数据,更多的取决于是否到了下一次底层要刷新屏幕的指令回调的时机,所以也就回答了上面的问题,每次指令到达时才会去刷新数据,尽可能的保证刷新数据的任务有足够16.6ms的时间

  • 造成屏幕丢帧的原因也就很明显了,1.View树绘制的任务时长大于了16.6ms,此时下一个信号来临,导致丢帧 2.虽然采用了同步屏障的方法来保证足够View绘制任务的时间,但是如果同步屏障之间的Message耗时过长,也导致遍历绘制 View 树的工作迟迟不能开始,从而超过了 16.6 ms 底层切换下一帧画面的时机,这也就是主线程不要做耗时操作的原因了

参考资料

https://www.jianshu.com/p/a769a6028e51

https://blog.csdn.net/asdgbc/article/details/79148180

推荐阅读

一步步带你读懂 Okhttp 源码Android 点九图机制讲解及在聊天气泡中的应用职场上这四件事,越早知道越好自定义View之双层波纹气泡(xFermode)Android-自定义气泡View,让我们告别.9图面试官:今日头条启动很快,你觉得可能是做了哪些优化?

面试官:你有m个鸡蛋,如何用最少的次数测出鸡蛋会在哪一层碎?

花式实现时间轴,样式由你来定!

面试官:Android 子线程更新UI了解吗?

Android 优雅地处理后台返回的骚数据

Android自动化脚本多渠道加固、打包

368bc05ac1bc87bbd7ff1492d9223569.png

扫一扫,欢迎关注我的微信公众号 stormjun94(徐公码字)

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值