Android屏幕刷新机制原理分析

您所在的位置:网站首页 view绘制原理面试 Android屏幕刷新机制原理分析

Android屏幕刷新机制原理分析

2023-09-03 01:50| 来源: 网络整理| 查看: 265

60Hz刷新频率由来 12fps:由于人类眼睛的特殊生理结构,如果所看画面之帧率高于每秒约10~12帧的时候,就会认为是连贯的24fps:有声电影的拍摄及播放帧率均为美秒24帧,对一般人而言已经算可接受30fps:早期的高动态电子游戏,帧率少于美秒30帧的话就会显得不连贯,这是因为没有动态模糊使流畅度降低60fps:在与手机交互的过程中,如触摸和反馈 60帧以下是能感觉出来的,60帧以上不能察觉变化当帧率低于60fps时感觉画面有卡顿迟滞现象

Android系统每隔16ms发出VSYNC信号(1000ms/60=16.66ms),触发对UI进行渲染,如果每次渲染都成功,这样就能够达到流畅的画面所需要的60fps,为了能够实现60fps,这意味着计算渲染的大多数操作都必须在16ms内完成。

屏幕刷新机制大致流程介绍

首先应用程序向系统服务申请一块buffer(缓存),系统服务返回buffer,应用拿到buffer之后就可以进行绘制,绘制完之后将buffer提交给系统服务,系统服务将buffer写到屏幕的一块缓存区,屏幕会以一定的帧率刷新,每次刷新的时候,就会从缓存区将图像数据读取显示出来。如果缓存区没有新的数据,就一直用旧的数据,这样屏幕看起来就没有变 在这里插入图片描述 屏幕的图像缓存不止一个,假如只有一个缓存,如果屏幕这边正在读缓存,而系统服务又在写缓存,这有可能导致屏幕显示不正常,如一半显示第一帧图像的画面,另一半显示第二帧图像的画面。如何避免这种问题的发生呢?可以弄多个缓存,屏幕从一块缓存读取数据显示,系统服务向另一块缓存写入数据。如果要显示下一帧图像,将两个缓存换一下即可,即屏幕从缓存2读取显示,系统服务向缓存1写入数据。 在这里插入图片描述 vsync(垂直同步机制)是固定频率的脉冲信号,屏幕根据这个信号周期性的刷新,屏幕每次收到这个信号,就从屏幕缓存区读取一帧的图像数据进行显示,而绘制是由应用端(任何时候都有可能)发起的,如果屏幕收到vsync信号,但是这一帧的还没有绘制完,就会显示上一帧的数据,这并不是因为绘制这一帧的时间过长(超过了信号发送周期),只是信号快来的时候才开始绘制,如果频繁的出现的这种情况,用户就会感知屏幕的卡顿,即使绘制时间优化的再好也无济于事,因为这是底层刷新机制的缺陷。 在这里插入图片描述

当然系统提供了解决方案,如果绘制和vsync信号同步就好了,每次收到vsync信号时,一方面屏幕获取图像数据刷新界面,另一方面应用开始绘制准备下一帧图像数据。如果优化的好,每一帧图像绘制控制在16ms以内,就可以非常流畅了。那么问题来了,应用层view的重绘一般调用requestLayout触发,这个函数随时都能调用,如何控制只在vsync信号来时触发重绘呢?有一个关键类Choreography(舞蹈指导,编舞),它最大的作用就是你往里面发送一个消息,这个消息最快也要等到下一个vsync信号来的时候触发。比如说绘制可能随时发起,封装一个Runnable丢给Choreography,下一个vsync信号来的时候,开始处理消息,然后真正的开始界面的重绘了。相当于UI绘制的节奏完全由Choreography来控制。 在这里插入图片描述

Choreography原理分析

就从比较熟系的requestLayout方法开始吧,进行UI操作时,通过checkThread方法进行线程检查,UI操作是否在UI线程,然后调用scheduleTraversals方法:

@Override public void requestLayout() { if (!mHandlingLayoutInLayoutRequest) { checkThread(); mLayoutRequested = true; scheduleTraversals(); } }

在scheduleTraversals方法里面,主要做了两件事,一件事是向线程的消息队列中加入了syncBarrier,另外就是往mChoreographer的mCallbackQueue数组插入了一个callback(需要执行的相关操作,这里主要是UI绘制),syncBarrier是一个屏障,将它插入到消息队列后,这个屏障后面的普通消息就不能处理了,等到屏障撤除之后才能处理。但是这个屏障对异步消息是没有影响的。主要是有些类型的消息非常紧急,需要马上处理。如果普通消息太多,容易耽误事(影响紧急消息的执行),所以插入了一个屏障,优先处理异步消息。请求同步Vsync信号,就是一个异步消息,就是请求系统服务SurfaceFlinger在下一次Vsync信号过来时,立即通知我们,我们就可以立即执行mChoreographer的callback数组里面对应callback的相关操作,即UI绘制了,这里先简单提一下,后面会具体分析。

@UnsupportedAppUsage void scheduleTraversals() { if (!mTraversalScheduled) {// 注释1 // 注释2 mTraversalScheduled = true; // 注释3 // 插入同步屏障syncBarrier到消息队列,挡住普通的同步消息,优先执行异步消息 mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier(); // 注释4 mChoreographer.postCallback( Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null); if (!mUnbufferedInputDispatch) { scheduleConsumeBatchedInput(); } ... } }

Choreography和ViewRootImpl一起创建的,是通过ThreadLocal存储的,即在不同的线程调用getInstance得到的是不同的Choreography对象。

public static Choreographer getInstance() { // ThreadLocal return sThreadInstance.get(); }

Choreographer要执行的操作就是mTraversalRunnable即TraversalRunnable的run方法,即doTraversal

final TraversalRunnable mTraversalRunnable = new TraversalRunnable(); final class TraversalRunnable implements Runnable { @Override public void run() { doTraversal(); } }

顺着代码调用逻辑进入doTraversal方法,在scheduleTraversals方法中mTraversalScheduled赋值为true,在doTraversal注释1处赋值为false,即使多次调用requestLayout方法,也不会多次执行,因为只有下一次vsync信号过来的时候,执行doTraveersal方法时才会置为false,即在一个vsync信号周期内,只会触发一次界面重绘。

void doTraversal() { if (mTraversalScheduled) { // 注释1 mTraversalScheduled = false; // 注释2 // 移除同步消息屏障 mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier); ... // 开始绘制流程 performTraversals(); ... } } }

到目前为止,我们已经知道Choreography控制UI绘制和Vsync同步的基本原理了,但是同步屏障挡住同步消息,优先执行异步消息,关于异步消息的相关逻辑貌似目前还没有涉及到,接着看下去,进入Choreography的postCallback方法:

@TestApi public void postCallback(int callbackType, Runnable action, Object token) { postCallbackDelayed(callbackType, action, token, 0); }

顺着调用逻辑进入postCallbackDelayed方法

@TestApi public void postCallbackDelayed(int callbackType, Runnable action, Object token, long delayMillis) { if (action == null) { throw new IllegalArgumentException("action must not be null"); } if (callbackType CALLBACK_LAST) { throw new IllegalArgumentException("callbackType is invalid"); } // 注释1 postCallbackDelayedInternal(callbackType, action, token, delayMillis); }

我们需要关注的是注释1处的postCallbackDelayedInternal方法:

private void postCallbackDelayedInternal(int callbackType, Object action, Object token, long delayMillis) { ... synchronized (mLock) { // 注释1 // 获取当前时间 final long now = SystemClock.uptimeMillis(); // 注释2 // 超时时间或者理解为触发时间 final long dueTime = now + delayMillis; // 注释3 // 将执行动作放在mCallbackQueue数组中 mCallbackQueues[callbackType].addCallbackLocked(dueTime, action, token); if (dueTime // 注释5 // 如果还没有到触发时间,使用handler在发送一个延时的异步消息。 // 这个延时消息会在到触发时间的时候执行 Message msg = mHandler.obtainMessage(MSG_DO_SCHEDULE_CALLBACK, action); msg.arg1 = callbackType; msg.setAsynchronous(true); mHandler.sendMessageAtTime(msg, dueTime); } } }

注释3处,Choreography里面有一个mCallbackQueue数组,里面的每一个元素都是一个callback的单链表,添加callback一方面要根据callback的类型callbackType插到对应的单链表,另一方面要根据callback执行的时间顺序排序,越是马上要执行的callback,越是插入到链表的前面,然后等待被调用。如果还没有到触发时间就走注释5处逻辑,发送延时异步消息请求同步vsync信号,如果到了触发时间,进入注释4处的scheduleFrameLocked方法:

private void scheduleFrameLocked(long now) { ... // If running on the Looper thread, then schedule the vsync immediately, // otherwise post a message to schedule the vsync from the UI thread // as soon as possible. // 如果在Choreography的UI线程中,就直接调用立即安排垂直同步,否则就发送一个消息到UI线程 // 尽快安排请求一个垂直同步 if (isRunningOnLooperThreadLocked()) { scheduleVsyncLocked(); } else { Message msg = mHandler.obtainMessage(MSG_DO_SCHEDULE_VSYNC); // 异步消息 msg.setAsynchronous(true); // 插入到消息队列的头部,可见消息之紧急因为要告诉SurfaceFlinger, // vsync信号过来时第一时间通知 mHandler.sendMessageAtFrontOfQueue(msg); } ... }

这个方法的作用很明确,就是请求同步Vsync信号,就是说vsync信号过来的时候,让系统服务SurfaceFlinger第一时间通知我们,我们去执行绘制的相关操作。如果不在Choreography的UI线程,就发送异步消息让UI线程请求同步Vsync信号,再看注释5处之后的逻辑

private final class FrameHandler extends Handler { public FrameHandler(Looper looper) { super(looper); } @Override public void handleMessage(Message msg) { switch (msg.what) { case MSG_DO_FRAME: doFrame(System.nanoTime(), 0); break; case MSG_DO_SCHEDULE_VSYNC: doScheduleVsync(); break; case MSG_DO_SCHEDULE_CALLBACK: //postCallbackDelayedInternal()方法中当未到期的时候发送过来的 doScheduleCallback(msg.arg1); break; } } }

以上代码我们可以看出这个,FramHandler拿到 whate属性值为MSG_DO_SCHEDULE_CALLBACK的时候会去执行 doScheduleCallback(msg.arg1)方法

void doScheduleCallback(int callbackType) { synchronized (mLock) { if (!mFrameScheduled) { final long now = SystemClock.uptimeMillis(); if (mCallbackQueues[callbackType].hasDueCallbacksLocked(now)) { scheduleFrameLocked(now); } } } }

这个方法中先是做了一些判断,mFrameSceduled为false 并且hasDueCallbacksLocked()这个方法的返回值为true,看方法名就能猜出这个callback是否到了触发时间,下面我们再分析这个。最终如果满足条件的情况下它会调用 scheduleFrameLocked()这个方法,也就是说注释4处到了触发时间,注释5处还没有到触发时间,就发送一个延迟异步消息,到了触发时间,最终都是调用scheduleVsyncLocked方法,该后续调用逻辑如下

@UnsupportedAppUsage private void scheduleVsyncLocked() { mDisplayEventReceiver.scheduleVsync(); } @UnsupportedAppUsage 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 { // native方法 // 请求同步vsync信号 nativeScheduleVsync(mReceiverPtr); } }

前面分析了请求同步vsync信号的过程,当下一个vsync信号发生的时候,SurfaceFlinger就会通知我们,就会回调该类的onVsync函数,参数timestampNanos就是vsync的时间戳,该函数里面会发送一个消息到Choreography的工作线程里面去了,这里并不是要切换工作线程,因为onVsync本身就在Choreography的工作线程。这个消息带了时间戳的,表示消息触发的时间,有了这个时间戳,就可以按照时间戳的顺序来处理消息。到时间了,就会去执行run方法,即执行doFrame方法

private final class FrameDisplayEventReceiver extends DisplayEventReceiver implements Runnable { ... @Override public void onVsync(long timestampNanos, int builtInDisplayId, int frame) { mTimestampNanos = timestampNanos; mFrame = frame; 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); } }

进入doFrame方法:

void doFrame(long frameTimeNanos, int frame) { final long startNanos; synchronized (mLock) { if (!mFrameScheduled) { return; // no work to do } //当前时间 startNanos = System.nanoTime(); //当前时间和垂直同步时间 final long jitterNanos = startNanos - frameTimeNanos; //垂直同步时间和当前时间的差值如果大于一个周期就修正一下 if (jitterNanos >= mFrameIntervalNanos) { //取插值和始终周期的余数 final long lastFrameOffset = jitterNanos % mFrameIntervalNanos; //当前时间减去上一步得到的余数当作最新的始终信号时间 frameTimeNanos = startNanos - lastFrameOffset; } //垂直同步时间上一次时间还小,就安排下次垂直同步,直接返回 if (frameTimeNanos Trace.traceBegin(Trace.TRACE_TAG_VIEW, "Choreographer#doFrame"); AnimationUtils.lockAnimationClock(frameTimeNanos / TimeUtils.NANOS_PER_MS); mFrameInfo.markInputHandlingStart(); doCallbacks(Choreographer.CALLBACK_INPUT, frameTimeNanos); mFrameInfo.markAnimationsStart(); doCallbacks(Choreographer.CALLBACK_ANIMATION, frameTimeNanos); mFrameInfo.markPerformTraversalsStart(); doCallbacks(Choreographer.CALLBACK_TRAVERSAL, frameTimeNanos); doCallbacks(Choreographer.CALLBACK_COMMIT, frameTimeNanos); } finally { AnimationUtils.unlockAnimationClock(); Trace.traceEnd(Trace.TRACE_TAG_VIEW); } if (DEBUG_FRAMES) { final long endNanos = System.nanoTime(); Log.d(TAG, "Frame " + frame + ": Finished, took " + (endNanos - startNanos) * 0.000001f + " ms, latency " + (startNanos - frameTimeNanos) * 0.000001f + " ms."); } }

doFrame分为两个阶段,先看第一个阶段,参数frameTimeNanos表示这一帧的时间戳先计算当前时间和这个时间戳的间隔有多大,间隔越大,表示要延时了,如果延时超过一个周期(mFrameIntervalNanos), 就要计算到底延迟了几个周期,如果延迟周期数(丢帧数,跳过的帧数)达到一个常量SKIPPED_FRAME_WARNING_LIMIT ,就会打印日志"应用在主线程做了太多的事情(耗时操作)"导致绘制延迟,丢帧。第二阶段就是处理callback了,callback有四种类型,每种类型对应一个单链表callbackQueue,给vsync事件分别分发到四种callback,然后执行对应的doCallbacks函数,单链表里面的callback是有时间戳的,只有到了时间的的callback才会回调,extractDueCallbacksLocked从callbackQueue里面取出到了时间的callback,然后在循环里面执行他们的run函数,requestLayout里面的scheduleTraversals函数传的callback是什么?就是准备绘制,mTraversalRunnable的run方法其实调用的是performTraversals,真正的开始执行UI绘制流程。

总结

大致调用流程图如下: 在这里插入图片描述 应用程序调用requestLayout发起重绘,通过Choreographer发送异步消息,请求同步vsync信号,即下一次vsync信号过来时,系统服务SurfaceFlinger在第一时间通知我们,触发UI绘制。虽然可以手动多次调用,但是在一个vsync周期内,requestLayout只会执行一次。

常见的问题

1.丢帧一般是什么原因引起的? 答:主线程有耗时操作,耽误了view的绘制

2.Android刷新频率60帧/秒,每隔16ms调onDraw绘制一次? 答: 60帧/秒也是vsync信号的频率,但不一定每次vsync信号都会去绘制,先要应用端主动发起重绘,才会向SurfaceFlinger请求接收vsync信号,这样当vsync信号来的时候,才会真正去绘制。

3.onDraw执行完之后屏幕会马上刷新么? 答: 不会马上刷新,会等到下一次vsync信号时才会刷新。 4.如果界面没有重绘,还会每隔16ms刷新屏幕么? 答:界面没有重绘,应用就不会收的vsync信号,屏幕还是会刷新,画面数据用的是旧的,看起来没什么变化而已 5.如果屏幕快要刷新的时候才去onDraw绘制会丢帧么? 答: 重绘不会立即执行,而是等到下一次vsync信号来时才开始, 所以什么时候发起重绘影响不大



【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3