概述
作者:散人丶
来源: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
,所以在这里就直接将DecorView
和ViewRootImpl
给绑定起来了,所以也验证了上述的结论,子 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自动化脚本多渠道加固、打包
扫一扫,欢迎关注我的微信公众号 stormjun94(徐公码字)
最后
以上就是粗心铃铛为你收集整理的刷新机制_你真的了解16.6ms刷新机制吗?的全部内容,希望文章能够帮你解决刷新机制_你真的了解16.6ms刷新机制吗?所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
发表评论 取消回复