Android 屏幕刷新机制

相关概念

刷新率:每秒内屏幕刷新的次数,单位是 hz 例如 60hz 现在一些新设备支持更高的刷新率并且有些设备支持多种刷新率
帧速率:GPU 在一秒内操作画面的帧数,单位是 fps 例如 60fps
tearing(屏幕撕裂):屏幕内的数据来自 2 个不同的帧,画面会出现撕裂感

screen tearing

屏幕撕裂的原因:画面显示过程简单的说就是 CPU/GPU 准备好数据存入 buffer 屏幕 从 buffer 中取出数据然后一行一行显示出来,屏幕处理的频率是固定的比如每 60ms 显示完一帧,但是CPU/GPU 写数据是不可控的,所以会出现有些数据根本没显示出来就被重写了 buffer 里的数据可能是来自不同的帧出现画面撕裂。解决这个问题的办法就是使用双缓存 GPU 往 back buffer 中写,屏幕从 frame buffer 中取数据展示,在合适的时机交换两个 buffer 的数据。当扫描完一个屏幕后,设备需要重新回到第一行以进入下一次的循环,此时有一段时间空隙,称为VerticalBlanking Interval(VBI) 因为此时屏幕没有在刷新,也就避免了交换过程中出现 screen tearing的状况。VSync(垂直同步)是 VerticalSynchronization 的简写,它利用 VBI 时期出现的vertical sync pulse来保证双缓冲在最佳时间点才进行交换
Drawing without VSync
在 Android 4.1 之前是上图这样工作的,屏幕显示第 0 帧,CPU/GPU 绘制第一帧在下一个周期内正常展示第一帧,但在展示第一帧的时候 CPU/GPU 没有及时去绘制第二帧导致在第三个周期内只能继续显示第一帧造成 Jank(卡顿)原因是第二帧开始绘制的时机太晚了在需要显示的时候还没有绘制完成,解决这个问题是方式就是利用 VSync 信号
Drawing with VSync
既然造成 Jank 的原因是绘制时机太晚那么就控制 CPU/GPU 在接收到 VSync 信号后立即绘制充分利用 16ms 的时间避免 Jank 这样在帧率和屏幕刷新率一致的情况下就可以很好的显示double buffer但如果帧率低于刷新率的情况下还是会出现 Jank (如上图)原因是第一帧的时候 GPU 没有完成绘制并且两个 buffer 都被占用, CPU 只能等待 buffer ,在第三个周期内 CPU 才拿到 buffer 造成 A 帧重复显示,并且由于 GPU 的处理时长又超出了一个周期所以 B 帧又重复了展示一次。为了解决这个问题就有了 Triple buffer triple buffer
可以看到在增加了一个 buffer 后再次出现上述情况 CPU 就可以使用第三个 buffer 去绘制,这样虽然第一帧还是会重复显示但之后就可以正常展示了,相对于帧率与刷新率相同的情况下只是后续的每一帧都延时了一个周期,接下来从代码层面看一下 VSync 信号

Choreographer

Choreographer 的作用是给它发送一个 runnable 这个 runnable 最快在下一个 Vsync 信号来的时候触发。View 所有触发刷新相关的操作都会一层层的往上遍历找到自己的 mParent 直到 ViewRootImpl(ViewRootImpl 实现了 ViewParent 接口)最后会调用 scheduleTraversals 方法,所以从这里开始

    void scheduleTraversals() {
        if (!mTraversalScheduled) {
       		// 在一个 Vsync 周期里只会有一次重绘请求有效
            mTraversalScheduled = true;
            // 插入同步屏障消息,同步消息屏障的作用是为了优先处理异步消息
            // 消息队列在发现同步屏障消息后会忽略队列里的同步消息只处理异步消息
            mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
            // 调用 postCallback 传入 mTraversalRunnable
            mChoreographer.postCallback(
                    Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
            if (!mUnbufferedInputDispatch) {
                scheduleConsumeBatchedInput();
            }
            notifyRendererOfFramePending();
            pokeDrawLockIfNeeded();
        }
    }

mChoreographer 是 Choreographer 实例跟 Looper 一样是线程内的单例

	// 这里的 action 就是上面的 mTraversalRunnable
    public void postCallback(int callbackType, Runnable action, Object token) {
    	// 最后一个参数传的是 0 
        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) {
        synchronized (mLock) {
            final long now = SystemClock.uptimeMillis();
            final long dueTime = now + delayMillis;
            // mCallbackQueues 是一个数组保存不同类型的 CallbackQueue 
            // CallbackQueue 是一个根据执行时间排序的单链表
            // addCallbackLocked 的作用是从对象池中取一个 CallbackRecord 对象并且根据 dueTime 插入到单链表中 
            // callbackType 的值是 Choreographer.CALLBACK_TRAVERSAL
            mCallbackQueues[callbackType].addCallbackLocked(dueTime, action, token);
			// 由于上面传的 0 所以走这个分支
            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);
            }
        }
    }

接下来看一下 scheduleFrameLocked()

    private void scheduleFrameLocked(long now) {
    	// 标志位一帧内只执行一次
        if (!mFrameScheduled) {
            mFrameScheduled = true;
            // 如果启用了 vsync 信号特性
            if (USE_VSYNC) {
                // 如果当前线程是创建 Choreographer 所在的线程则执行 scheduleVsyncLocked
                if (isRunningOnLooperThreadLocked()) {
                    scheduleVsyncLocked();
                } else {
                	// 否则发送一个异步消息到消息队列的最前方立即执行
                	// 最后还是会执行 scheduleVsyncLocked
                    Message msg = mHandler.obtainMessage(MSG_DO_SCHEDULE_VSYNC);
                    msg.setAsynchronous(true);
                    mHandler.sendMessageAtFrontOfQueue(msg);
                }
            } else {
            	// 如果是低版本没有开启 VSync 信号则发送一个消息直接去执行下面要分析的 doFrame 方法
                final long nextFrameTime = Math.max(
                        mLastFrameTimeNanos / TimeUtils.NANOS_PER_MS + sFrameDelay, now);
                Message msg = mHandler.obtainMessage(MSG_DO_FRAME);
                msg.setAsynchronous(true);
                mHandler.sendMessageAtTime(msg, nextFrameTime);
            }
        }
    }

    private void scheduleVsyncLocked() {
        mDisplayEventReceiver.scheduleVsync();
    }

最后调用到 mDisplayEventReceiver.scheduleVsync 方法 mDisplayEventReceiver 是 FrameDisplayEventReceiver 对象继承自 DisplayEventReceiver 上面发送的消息都是异步消息因为在 scheduleTraversals 插入了同步屏障消息

    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);
        }
    }

最终调用一个 native 方法注册接收下一次垂直同步信号,当接收到信号后会调用 mDisplayEventReceiver.onVsync 方法

    // Called from native code.
    @SuppressWarnings("unused")
    private void dispatchVsync(long timestampNanos, long physicalDisplayId, int frame,
            long frameTimelineVsyncId, long frameDeadline, long frameInterval) {
        onVsync(timestampNanos, physicalDisplayId, frame,
                new VsyncEventData(frameTimelineVsyncId, frameDeadline, frameInterval));
    }

    public void onVsync(long timestampNanos, int builtInDisplayId, int frame) {
     	// 同样发送一个异步消息包含一个 runnable 执行时调用下面的 run 方法
        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);
    }

收到 Vsync 信号后发送一个异步消息去执行 doFrame 方法

    void doFrame(long frameTimeNanos, int frame) {
        final long startNanos;
        synchronized (mLock) {
            if (!mFrameScheduled) {
                return; // no work to do
            }
			// VSync 信号的时间戳
            long intendedFrameTimeNanos = frameTimeNanos;
            startNanos = System.nanoTime();
            final long jitterNanos = startNanos - frameTimeNanos;
            // 如果超出一帧执行时间
            if (jitterNanos >= mFrameIntervalNanos) {
                final long skippedFrames = jitterNanos / mFrameIntervalNanos;
                // 如果超出指定帧数打印日志告诉应用程序在主线程做了太多事耽误了绘制
                if (skippedFrames >= SKIPPED_FRAME_WARNING_LIMIT) {
                    Log.i(TAG, "Skipped " + skippedFrames + " frames!  "
                            + "The application may be doing too much work on its main thread.");
                }
                final long lastFrameOffset = jitterNanos % mFrameIntervalNanos;
                // 修正时间
                frameTimeNanos = startNanos - lastFrameOffset;
            }
			// 异常情况注册接收下一个 VSync 信号
            if (frameTimeNanos < mLastFrameTimeNanos) {
                if (DEBUG_JANK) {
                    Log.d(TAG, "Frame time appears to be going backwards.  May be due to a "
                            + "previously skipped frame.  Waiting for next vsync.");
                }
                scheduleVsyncLocked();
                return;
            }
        }

        try {
            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();
            // 执行 CALLBACK_TRAVERSAL 类型的 CallbackRecord
            doCallbacks(Choreographer.CALLBACK_TRAVERSAL, frameTimeNanos);

            doCallbacks(Choreographer.CALLBACK_COMMIT, frameTimeNanos);
        } finally {
            AnimationUtils.unlockAnimationClock();
            Trace.traceEnd(Trace.TRACE_TAG_VIEW);
        }
    }

做的主要事情就当发生掉帧的时候打印日志并且对时间进行修正,接收到垂直脉冲信号后执行指定类型的 callbacks

    void doCallbacks(int callbackType, long frameTimeNanos) {
        CallbackRecord callbacks;
        synchronized (mLock) {
			// 取出指定类型的执行时间大于当前时间的 CallbackRecord 链表
            final long now = System.nanoTime();
            callbacks = mCallbackQueues[callbackType].extractDueCallbacksLocked(
                    now / TimeUtils.NANOS_PER_MS);
            if (callbacks == null) {
                return;
            }
            mCallbacksRunning = true;
        }
        try {
            Trace.traceBegin(Trace.TRACE_TAG_VIEW, CALLBACK_TRACE_TITLES[callbackType]);
            for (CallbackRecord c = callbacks; c != null; c = c.next) {
                if (DEBUG_FRAMES) {
                    Log.d(TAG, "RunCallback: type=" + callbackType
                            + ", action=" + c.action + ", token=" + c.token
                            + ", latencyMillis=" + (SystemClock.uptimeMillis() - c.dueTime));
                }
                // 遍历执行链表里的任务
                c.run(frameTimeNanos);
            }
        } finally {
            synchronized (mLock) {
                mCallbacksRunning = false;
                do {
                	// 回收 CallbackRecord
                    final CallbackRecord next = callbacks.next;
                    recycleCallbackLocked(callbacks);
                    callbacks = next;
                } while (callbacks != null);
            }
            Trace.traceEnd(Trace.TRACE_TAG_VIEW);
        }
    }

CallbackRecord 就是一个单链表的数据结构 run 方法里根据 token 判断执行逻辑

    private static final class CallbackRecord {
        public CallbackRecord next;
        public long dueTime;
        public Object action; // Runnable or FrameCallback
        public Object token;

        public void run(long frameTimeNanos) {
        	// 如果是 token 是 FRAME_CALLBACK_TOKEN 说明是通过 postFrameCallback 加入的 CallbackQueue 可以通过这个方法监听 App 帧率
            if (token == FRAME_CALLBACK_TOKEN) {
                ((FrameCallback)action).doFrame(frameTimeNanos);
            } else {
            	// 上述分析流程 token 是 null 所以会直接调用 action 的 run 方法
            	// 这里的 action 是上面传进来的 mTraversalRunnable 直接调用了 doTraversal 方法
                ((Runnable)action).run();
            }
        }
    }
    void doTraversal() {
    	// 与 scheduleTraversals 方法对应
        if (mTraversalScheduled) {
        	// 先 mTraversalScheduled 置为 false
            mTraversalScheduled = false;
            // 移除同步屏障消息
            mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);

            if (mProfile) {
                Debug.startMethodTracing("ViewAncestor");
            }
			
			// 执行真正的测量、布局和绘制工作一帧的刷新到这里就完成了
            performTraversals();

            if (mProfile) {
                Debug.stopMethodTracing();
                mProfile = false;
            }
        }
    }

重绘动作只是注册了一个 VSync 信号的监听并且提交了一个 runnable 当收到 VSync 信号后才去真正执行这个 runnable 真正的去做重绘然后交给屏幕去展示,如果没有重绘请求屏幕依然会刷新但是应用不会接收 vsync 信号也不会做真正的绘制操作屏幕画面不会有变化,所以 Choreographer 的作用就是接收到 VSync 信号后立即去重绘充分利用一帧的时长并且通过插入同步屏障消息尽可能的优先执行绘制任务避免丢帧

总结

跟界面刷新相关的方法里应该都会有一个循环找自己的 parent 的方法,或者是不断调用 parent 的方法,这样最终才都会走到 ViewRootImpl 里,也就是说实际上 View 的刷新都是由 ViewRootImpl 来控制的。即使是界面上一个小小的 View 发起了重绘请求时,都要层层走到 ViewRootImpl,由它来发起重绘请求,然后再由它来开始遍历 View 树,一直遍历到这个需要重绘的 View 再调用它的 onDraw() 方法进行绘制

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

Android 刷新频率 60 帧/秒,每隔 16ms 调 onDraw 会绘制一次 ?
  刷新频率是指 Vsync 信号的频率,但是不是每次 Vsync 信号来的时候都会去绘制,需要应用端先发起重绘去向 SurfaceFlinger 去请求 Vsync 信号,这样在下次 Vsync 信号来的时候才会真正的去绘制

onDraw 完之后屏幕会马上刷新吗 ?
  不会,等到下次 Vsync 信号来的时候才刷新

如果界面没有重绘,还会每隔 16ms 刷新屏幕吗 ?
  如果界面没有重绘就不会收到 Vsync 的信号,但是屏幕还是会以 16ms 的频率刷新的,只不过屏幕数据一直都是旧的看上去没有变化

如果在屏幕快要刷新的时候才去绘制会丢帧吗 ?
  代码里发起的 View 重绘不会马上执行,需要等到下次 Vsync 信号来的时候才执行,所以什么时候发起绘制操作没关系

参考与感谢

android屏幕刷新显示机制
Android 屏幕刷新机制
“终于懂了” 系列:Android屏幕刷新机制—VSync、Choreographer 全面理解!
通俗易懂的Android屏幕刷新机制
说说屏幕刷新的机制-1

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值