- Android刷新机制
- SurfaceView理解
一、Android屏幕刷新机制
-
首先需要了解一些基本概念
- 在一个显示系统里,一般包括CPU、GPU、Display三部分,CPU负责计算数据,把计算号的数据交给CPU,GPU会对图形数据进行渲染,渲染后放到buffer里存起来,然后Display(可称为屏幕或者显示器)负责把buffer里的数据呈现到屏幕上。显示过程,简单来说就是CPU/GPU准备好数据,存入buffer,Display每隔一段时间去buffer里面取数据,然后显示出来。Display每次读取的频率是固定的,比如16ms一次,但是CPU/GPU写数据是完全无规律的。
- CPU计算数据指的是View树的绘制过程,也就是Activity对应视图树从根布局DecorView开始遍历View,分别执行测量、布局、绘制三个操作过程。我们常说的16.6ms刷新一次屏幕其实就是底层以固定的频率将buffer中的屏幕数据显示出来。
-
在之前的几篇文章里,例如 Window和WindowManager相关知识点(六) 以及 Android View相关知识点以及原理(四) 内对DecorView和setContentView以及Window的联系可以知道在onResume的时候才会创建ViewRootImpl将DecorView和Window关联起来,并且和任意View调用invalidate等刷新一样都会走ViewRootImpl中的 scheduleTraversals()方法,然后调用Choreographer的postCallBack方法
void scheduleTraversals() { if (!mTraversalScheduled) { //编号1 mTraversalScheduled = true; //编号2 mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier(); //编号3 mChoreographer.postCallback( Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null); if (!mUnbufferedInputDispatch) { scheduleConsumeBatchedInput(); } notifyRendererOfFramePending(); pokeDrawLockIfNeeded(); } }
-
Choreographer的postCallBack中传了一个mTraversalRunnable,大家可以看下面代码,run方法内调用了doTraversal方法
final class TraversalRunnable implements Runnable { @Override public void run() { doTraversal(); } } void doTraversal() { if (mTraversalScheduled) { //编号4 mTraversalScheduled = false; //编号5 mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier); if (mProfile) { Debug.startMethodTracing("ViewAncestor"); } //这里开始对view树进行测量、布局、绘制 performTraversals(); if (mProfile) { Debug.stopMethodTracing(); mProfile = false; } } }
-
这里会产生一个疑问,调用了scheduleTraversals方法后,代码里只是将Runnable作为参数传递到了Choreographer的postCallback方法中,要想调用doTraversal方法,那必须要有某处执行这个Runnable。为了解清楚执行逻辑,请看Choreographer的postCallback的代码
public void postCallback(int callbackType, Runnable action, Object token) { postCallbackDelayed(callbackType, action, token, 0); } public void postCallbackDelayed(int callbackType, Runnable action, Object token, long delayMillis) { if (action == null) { throw new IllegalArgumentException("action must not be null"); } if (callbackType < 0 || callbackType > CALLBACK_LAST) { throw new IllegalArgumentException("callbackType is invalid"); } postCallbackDelayedInternal(callbackType, action, token, delayMillis); } private void postCallbackDelayedInternal(int callbackType, Object action, Object token, long delayMillis) { if (DEBUG_FRAMES) { Log.d(TAG, "PostCallback: type=" + callbackType + ", action=" + action + ", token=" + token + ", delayMillis=" + delayMillis); } synchronized (mLock) { final long now = SystemClock.uptimeMillis(); final long dueTime = now + delayMillis; //编号6 mCallbackQueues[callbackType].addCallbackLocked(dueTime, action, token); if (dueTime <= now) { scheduleFrameLocked(now); } else { Message msg = mHandler.obtainMessage(MSG_DO_SCHEDULE_CALLBACK, action); msg.arg1 = callbackType; msg.setAsynchronous(true); mHandler.sendMessageAtTime(msg, dueTime); } } }
它最终会调用postCallbackDelayedInternal方法,而传递进来的delayMillis是0,所以dueTime = now,所以调用的是scheduleFrameLocked方法,接下来看它的调用代码
//方法 private void scheduleFrameLocked(long now) { if (!mFrameScheduled) { mFrameScheduled = true; if (USE_VSYNC) { if (DEBUG_FRAMES) { Log.d(TAG, "Scheduling next frame on vsync."); } //是否在主线程 if (isRunningOnLooperThreadLocked()) { scheduleVsyncLocked(); } else { Message msg = mHandler.obtainMessage(MSG_DO_SCHEDULE_VSYNC); msg.setAsynchronous(true); mHandler.sendMessageAtFrontOfQueue(msg); } } else { final long nextFrameTime = Math.max( mLastFrameTimeNanos / TimeUtils.NANOS_PER_MS + sFrameDelay, now); if (DEBUG_FRAMES) { Log.d(TAG, "Scheduling next frame in " + (nextFrameTime - now) + " ms."); } Message msg = mHandler.obtainMessage(MSG_DO_FRAME); msg.setAsynchronous(true); mHandler.sendMessageAtTime(msg, nextFrameTime); } } } //方法 private void scheduleVsyncLocked() { mDisplayEventReceiver.scheduleVsync(); } //方法 DisplayEventReceiver public void scheduleVsync() { if (mReceiverPtr == 0) { Log.w(TAG, "Attempted to schedule a vertical sync pulse but the display event " + "receiver has already been disposed."); } else { nativeScheduleVsync(mReceiverPtr); } } //方法 FrameDisplayEventReceiver类 private static native void nativeScheduleVsync(long receiverPtr);
可以发现最终调用的是native方法,到这里就没法跟下去了,换种思路,既然之前把Runnable放到了CallbackQueue中,见第4点的编号6注释,那么调用时机一定对应于从CallbackQueue取出Runnable,也就是CallbackQueue的extractDueCallbacksLocked方法,经过查找,可以得出是doCallbacks方法内调用了该方法( CallbackQueue 是Choreographer的内部类 ) ,而调用doCallbacks方法的是doFrame方法。(这里可以看到一个callbackType对应一个Runnable队列,mCallbackQueue[callbackType])
void doCallbacks(int callbackType, long frameTimeNanos) { CallbackRecord callbacks; synchronized (mLock) { // We use "now" to determine when callbacks become due because it's possible // for earlier processing phases in a frame to post callbacks that should run // in a following phase, such as an input event that causes an animation to start. final long now = System.nanoTime(); callbacks = mCallbackQueues[callbackType].extractDueCallbacksLocked( now / TimeUtils.NANOS_PER_MS); ..... } } void doFrame(long frameTimeNanos, int frame) { final long startNanos; synchronized (mLock) { ...... try { Trace.traceBegin(Trace.TRACE_TAG_VIEW, "Choreographer#doFrame"); AnimationUtils.lockAnimationClock(frameTimeNanos / TimeUtils.NANOS_PER_MS); mFrameInfo.markInputHandlingStart(); doCallbacks(Choreographer.CALLBACK_INPUT, frameTimeNanos); ......
那么doFrame方法是哪里调用的呢?从之前得出了是调用FrameDisplayEventReceiver的native方法后就跟不下去了,那么进入这个类中可以发现它的 run 方法恰恰调用了doFrame方法,而官方对它其中的onVsync方法也有注释 Called when a vertical sync pulse is received ,说明这是底层回调用的。
@Override public void onVsync(long timestampNanos, int builtInDisplayId, int frame) { if (builtInDisplayId != SurfaceControl.BUILT_IN_DISPLAY_ID_MAIN) { Log.d(TAG, "Received vsync from secondary display, but we don't support " + "this case yet. Choreographer needs a way to explicitly request " + "vsync for a specific display to ensure it doesn't lose track " + "of its scheduled vsync."); scheduleVsync(); return; } // Post the vsync event to the Handler. long now = System.nanoTime(); if (timestampNanos > now) { timestampNanos = now; } if (mHavePendingVsync) { Log.w(TAG, "Already have a pending vsync event. There should only be " + "one at a time."); } else { mHavePendingVsync = true; } mTimestampNanos = timestampNanos; mFrame = frame; //编号7 Message msg = Message.obtain(mHandler, this); msg.setAsynchronous(true); mHandler.sendMessageAtTime(msg, timestampNanos / TimeUtils.NANOS_PER_MS); } @Override public void run() { mHavePendingVsync = false; doFrame(mTimestampNanos, mFrame); }
从Handler的处理消息机制来看,我们知道一般如果传递了Runnable,会执行它的run方法,否则看你有没有指定Callback,有的话会执行你的Callback,没有则会调用handleMessage,所以 编号7 处,将this传递给了Message,后续将执行自己的run方法,继而执行doFrame方法,然后就会执行doCallbacks方法,然后会执行Runnable对象的run方法,run方法内又会执行doTraversals方法,这就开始刷新view咯
-
小结一下,FrameDisplayEventReceiver继承DisplayEventReceiver接受底层的VSync信号开始处理UI过程,VSync信号由SurfaceFlinger实现并定时发送。FrameDisplayEventReceiver收到信号后,调用onVsync方法组织消息发送到主线程处理。这个消息的内容主要就是run方法里面的doFrame了。FrameDsiplayEventReceiver之所以能收到信号,回调onVsync方法,可以理解为APP层在调用native方法nativeScheduleVsync时向底层注册了一个屏幕刷新信号监听事件,要不然底层怎么知道APP需要刷新数据呢?并且APP对底层是隐藏的,底层压根不知道APP的存在。
梳理一下,APP通过native方法向底层注册了下一帧的屏幕刷新界面,然后在每16.6ms的帧信号到来时,它就会回调onVsync刷新屏幕了
-
那么问题来了,是不是在16.6ms内或者是屏幕刷新前,我可以无限注册该监听事件,也就是一帧内是不是会注册很多重复监听
-
首先看段代码
void scheduleTraversals() { if (!mTraversalScheduled) { mTraversalScheduled = true; //编号 8 mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier(); mChoreographer.postCallback( Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null); ...... } void doTraversal() { if (mTraversalScheduled) { mTraversalScheduled = false; //编号 9 mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier); ...... }
-
首先看scheduleTraversals方法,它在向底层注册监听前,有一个mTraversalScheduled变量,该变量默认false,多次调用scheduleTraversals方法,只要mTraversalScheduled为false才会重复注册,而从doTraversal方法中可以看出只有接收到一帧信号是该变量才会重置为false。那为什么这么设计呢?查看performTraversals方法就可以知道,界面刷新调用了这个方法,方法内可以层层遍历View树的,需要刷新的View都会遍历到并刷新的,所以也就没有必要重复注册吧
-
-
接下来细心的人可能会问,在onVsync方法内的 编号7 处代码,这里会将刷新消息封装到Message里面放到主线程的MessageQueue中,而咱们的主线程也是一直在处理MessageQueue里面的消息的,同一时间只能处理一个Message,如果其它消息耗时,导致刷新消息在16.6ms内还没处理,该怎么办
- 这种情况是避免不了的,大家只能尽量不要在这其中处理过多耗时操作
- Android系统对其有一个优化,就是 编号 8 和 编号 9 处代码,可以先理解为设置和移除屏障消息的标识。首先是通过 编号 8 处代码会往队列里发送一个同步屏障,主Looper通过next不断取出队头的Message执行,如果队头是一个同步屏障消息时,将会遍历整个队列,寻找设置了异步标识的消息,找到该消息就取出该消息来执行,否则就让next方法陷入阻塞阶段。所以在屏障消息后面的所有同步消息都将拦截,直到通过doTraversal方法将屏障消息移除队列。这样就能保证刷新消息先执行,但是在之前取出的Message还是会依旧执行,所以说如果Message内内容耗时的话,还是会影响刷新消息的执行的。
-
小结
- 界面上任意一个View的刷新请求都会调用ViewRootImpl的scheduleTraversals()方法,这个方法内会过滤一帧内的重复调用,保证同一帧内只会进行一遍View树的遍历刷新
- 当调用scheduleTraversals()方法后,会向主线程的消息队列发送一个同步屏障,拦截这个时刻之后的所有同步消息,直到底层回调onVsync后产生的具有异步标识的刷新界面消息被执行
- 然后会把刷新界面的操作,也就是doTraversals方法放到Runnable里,通过Choreographer的postCallback方法以时间戳的形式放到队列里面,如果当前是主线程,将直接调用native方法,如果不是主线程,将会以最高优先级,将Message放到主线程,保证尽量第一个执行该native方法
- native的这个方法是用来向底层注册监听下一个屏幕刷新信号,当下一个屏幕刷新信号发出时,底层就会回调Choreographer的onVsync方法
- onVsync方法调用后,由于是具有异步标识的方法,所以不会被同步屏障拦截,就能尽量保证第一时间取出并刷新界面,同时移除同步屏障
- 最后就是执行View的遍历刷新了
- 如果界面一直保持没变的话,也就是没有注册监听事件,但是底层的还是会以每16.6ms固定频率来切换每一帧的画面,只是最后这些画面都是相同的而已。
- 底层是以固定的频率来切换屏幕的画面的,即使CPU计算完了数据,也就是说测量,布局,绘制等等都算完了,它也不会立即显示,而是要等到信号来的时候。
二、SurfaceView
- 首先列举最常见的和View的区别
- View的绘图效率低,主要用于动画变化较少的程序,必须在主线程更新
- SurfaceView绘图效率高,用于界面更新频繁的程序,一般在子线程更新
- SurfaceView拥有独立的Surface(绘图表面),即它不与其宿主窗口共享同一个Surface
- SurfaceView使用双缓冲机制,播放视频时画面更流畅
- 每个窗口在SurfaceFlinger服务中都对应有一个Layer,用它来描述它的绘制表面。对于那些具有SurfaceView的窗口来说,每一个SurfaceView在SurfaceFlinger服务中还对应于一个独立的Layer或者LayerBuffer,用来单独描述它的绘图表面,以区别于它的宿主窗口的绘图表面。所以SurfaceView的UI就可以在一个独立的线程中进行绘制,可以不占用主线程资源,它产生原因也是为了应对耗时的操作,例如Camera X。
- 部分概念
- Canvas是Java层构建的数据结构,是给View用的画布,ViewGroup会将Canvas拆分给子View,在onDraw方法里将图形数据绘制在它获得的Canvas上
- Surface是Native层构建的数据结构,是给SurfaceFlinger用的画布,它是直接被用来绘制到屏幕上的数据结构
- 开发者一般所用到的View都是在Canvas进行绘制,然后最顶层的View的Canvas的数据信息会转换到一个Surface上,SurfaceFlinger会将各个应用窗口的Surface进行合成,然后绘制到屏幕上。
- 双缓冲机制
- SurfaceView在更新视图时用到了两张Canvas,一张frontCanvas和一张backCanvas,每次实际显示的是frontCanvas,backCanvas存储的是上一次更改的视图。当你在播放这一帧的时候,它已经提前帮你加载好后面一帧了,所以播放起来流畅。
- 当使用lockCanvas()获取画布时,得到的实际是backCanvas而不是正在显示的frontCanvas,之后我们再在backCanvas上绘制新的视图,再通过unlockCanvasAndPost(canvas)此视图,然后上传的这张Canvas将替换原来的frontCanvas作为新的frontCanvas,原来的frontCanvas将切换到后台作为backCanvas。相当于多线程交替解析和渲染每一帧视频数据
- SurfaceView是Zorder排序的,默认在宿主Window的后面,SurfaceView通过在Window上面”挖洞“(设置透明区域进行显示)
- SurfaceView是一个有自己的Surface的View,它的渲染可以放到单独线程而不是主线程中,其缺点是不能做变形和动画
- SurfaceTexture可以用作非直接输出的内容流,这样就提供二次处理的机会,与SurfaceView直接输出相比,这样会有若干帧的延迟,内存消耗也会大一些
- TextureView是在View hierachy中做绘制,因此它一般是在主线程做的
- 具体介绍可以参考 SurfaceTexture,TextureView, SurfaceView 和 GLSurfaceView 区别知多少