Android屏幕刷新机制原理分析

基本概念

CPU:执行应用层的measure、layout、draw等操作,绘制完成后,将数据交由GPU
GPU:处理数据,将数据发送到缓冲区
屏幕:由一个一个像素组成,以固定频率(1000ms,60次,即16.6ms一次)去缓冲区里读取数据填充像素点
在这里插入图片描述

双缓冲机制

看完上面的流程图,我们很容易想到一个问题,屏幕是以16.6ms的固定频率进行刷新的,但是我们应用层触发绘制的时机是完全随机的(比如我们随时都可以触摸屏幕触发绘制),如果在GPU向缓冲区写入数据的同时,屏幕也在向缓冲区读取数据,会发生什么情况呢?有可能屏幕上就会出现一部分是前一帧的画面,一部分是另一帧的画面,这显然是无法接受的,那怎么解决这个问题呢?
所以,在屏幕刷新中,Android系统引入了双缓冲机制。GPU只向Back Buffer中写入绘制数据,且GPU会定期交换Back Buffer和Frame Buffer,也就是让Back Buffer 变成Frame Buffer交给屏幕进行绘制,让原先的Frame Buffer变成Back Buffer进行数据写入。交换的频率也是60次/秒,这就与屏幕的刷新频率保持了同步
在这里插入图片描述

VSync - 垂直同步信号

vsync是固定频率的脉冲信号,屏幕根据这个信号周期性的刷新,屏幕每次收到这个信号,就从屏幕缓存区读取一帧的图像数据进行显示,而绘制是由应用端(任何时候都有可能)发起的,如果屏幕收到vsync信号,但是这一帧的还没有绘制完,就会显示上一帧的数据,这并不是因为绘制这一帧的时间过长(超过了信号发送周期),只是信号快来的时候才开始绘制,如果频繁的出现的这种情况,用户就会感知屏幕的卡顿,即使绘制时间优化的再好也无济于事,因为这是底层刷新机制的缺陷。如下图,2就会被丢失,即所谓的丢帧。
在这里插入图片描述

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

Choreography原理分析

从ViewRootImpl#requestLayout刷新UI开始看起

@Override
public void requestLayout() {
    if (!mHandlingLayoutInLayoutRequest) {
        // 检查线程
        checkThread();
        mLayoutRequested = true;
        scheduleTraversals();
    }
}

void scheduleTraversals() {
    if (!mTraversalScheduled) {
        mTraversalScheduled = true;
        // 发送一个同步屏障信号,等下再来解释这个信号的作用
        mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
        // 核心,调用postCallback,把一个Runnable传进去
        mChoreographer.postCallback(
                Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
        if (!mUnbufferedInputDispatch) {
            scheduleConsumeBatchedInput();
        }
        notifyRendererOfFramePending();
        pokeDrawLockIfNeeded();
    }
}
// Choreographer类
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) {
    synchronized (mLock) {
        final long now = SystemClock.uptimeMillis();
        final long dueTime = now + delayMillis;
        // 把Runnable的回调,添加到CallbackQueues队列里,并且回调类型callbackType是CALLBACK_TRAVERSAL
        mCallbackQueues[callbackType].addCallbackLocked(dueTime, action, token);

        if (dueTime <= now) {
            // 如果已经到触发时间就立即申请,请求垂直同步信号(申请不表示马上就有,而是等到下一个VSync下来的时候)
            scheduleFrameLocked(now);
        } else {
            // 向Handler发送一个消息,指定的时间执行,并且是异步消息
            Message msg = mHandler.obtainMessage(MSG_DO_SCHEDULE_CALLBACK, action);
            msg.arg1 = callbackType;
            msg.setAsynchronous(true);
            mHandler.sendMessageAtTime(msg, dueTime);
        }
    }
}

// scheduleFrameLocked里最终通过scheduleVsyncLocked,向系统申请,请求垂直同步信号
private void scheduleVsyncLocked() {
    // mDisplayEventReceiver是内部类FrameDisplayEventReceiver
    mDisplayEventReceiver.scheduleVsync();
}

// 内部类FrameDisplayEventReceiver
private final class FrameDisplayEventReceiver extends DisplayEventReceiver
        implements Runnable {
    private boolean mHavePendingVsync;
    private long mTimestampNanos;
    private int mFrame;

    public FrameDisplayEventReceiver(Looper looper, int vsyncSource) {
        super(looper, vsyncSource);
    }
    
    @Override
    public void onVsync(long timestampNanos, int builtInDisplayId, int frame) {
        // 当VSYNC垂直同步下来时,会回调改方法
        long now = System.nanoTime();
        mTimestampNanos = timestampNanos;
        mFrame = frame;
        // 向Handler发送消息,时间一到则执行run方法,即下面重写的run
        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);
    }
}

void doFrame(long frameTimeNanos, int frame) {
    final long startNanos;
    synchronized (mLock) {
        if (!mFrameScheduled) {
            return; // no work to do
        }

        long intendedFrameTimeNanos = frameTimeNanos;
        startNanos = System.nanoTime();
        final long jitterNanos = startNanos - frameTimeNanos;
        if (jitterNanos >= mFrameIntervalNanos) {
            final long skippedFrames = jitterNanos / mFrameIntervalNanos;
            final long lastFrameOffset = jitterNanos % mFrameIntervalNanos;
            frameTimeNanos = startNanos - lastFrameOffset;
        }
        // 垂直同步时间上一次时间还小,就安排下次垂直同步,直接返回
        if (frameTimeNanos < mLastFrameTimeNanos) {
            scheduleVsyncLocked();
            return;
        }

        if (mFPSDivisor > 1) {
            long timeSinceVsync = frameTimeNanos - mLastFrameTimeNanos;
            if (timeSinceVsync < (mFrameIntervalNanos * mFPSDivisor) && timeSinceVsync > 0) {
                scheduleVsyncLocked();
                return;
            }
        }

        mFrameInfo.setVsync(intendedFrameTimeNanos, frameTimeNanos);
        mFrameScheduled = false;
        mLastFrameTimeNanos = frameTimeNanos;
    }

    try {
        Trace.traceBegin(Trace.TRACE_TAG_VIEW, "Choreographer#doFrame");
        AnimationUtils.lockAnimationClock(frameTimeNanos / TimeUtils.NANOS_PER_MS);

        mFrameInfo.markInputHandlingStart();
        // 执行回调,最终就是从CallbackQueues队列里,拿出对应类型的callBackType,执行其run方法,也就是
        // mChoreographer.postCallback带进来的Runnable
        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);
    }
}

// ViewRootImpl类
final class TraversalRunnable implements Runnable {
    @Override
    public void run() {
        doTraversal();
    }
}

// ViewRootImpl类
void doTraversal() {
    if (mTraversalScheduled) {
        mTraversalScheduled = false;
        // 移除同步屏障信号
        mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);
        // 执行measure、layout、draw
        performTraversals();
    }
}

同步消息屏障

void scheduleTraversals() {
    if (!mTraversalScheduled) {
        mTraversalScheduled = true;
        // 往MainLooper里发送一个同步消息屏障信号
        mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
    }
}

我们知道,Android是基于消息机制的,每一个操作都是一个Message,如果在触发绘制的时候,消息队列中还有很多消息没有被执行,那是不是意味着要等到消息队列中的消息执行完成后,绘制消息才能被执行到,那么依然无法保证Vsync信号和绘制的同步,所以依然可能出现丢帧的现象
还记不记得我们之前在Choreographer#scheduleFrameLocked()和FrameDisplayEventReceiver#onVsync()中提到,我们会给与Message有关的绘制请求设置成异步消息(msg.setAsynchronous(true)),为什么要这么做呢?这时候MessageQueue#postSyncBarrier()就发挥它的作用了,简单来说,它的作用就是一个同步消息屏障,能够把我们的异步消息(也就是绘制消息)的优先级提到最高。

主线程的 Looper 会一直循环调用 MessageQueue 的 next() 来取出队头的 Message 执行,当 Message 执行完后再去取下一个。当 next() 方法在取 Message 时发现队头是一个同步屏障的消息时,就会去遍历整个队列,只寻找设置了异步标志的消息,如果有找到异步消息,那么就取出这个异步消息来执行,否则就让 next() 方法陷入阻塞状态。如果 next() 方法陷入阻塞状态,那么主线程此时就是处于空闲状态的,也就是没在干任何事。所以,如果队头是一个同步屏障的消息的话,那么在它后面的所有同步消息就都被拦截住了,直到这个同步屏障消息被移除,否则主线程就一直不会去处理同步屏障后面的同步消息
那这么同步屏障是什么时候被移除的呢?
其实我们就是在我们上面提到的ViewRootImp#doTraversal()方法中

void doTraversal() {
    if (mTraversalScheduled) {
        mTraversalScheduled = false;
        // 移除同步消息屏障信号
        mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);
        // 执行measure、layout、draw
        performTraversals();
    }
}

整体流程图
在这里插入图片描述

Q&A
Q1:这个 16.6ms 刷新一次屏幕到底是什么意思呢?是指每隔 16.6ms 调用 onDraw() 绘制一次么?
A1:不是,16.6ms一次VSYNC信号的同步,但不一定每次都回去绘制,先要应用端发起绘制,向SurfaceFlinger注册后,当垂直同步信号到来,才会发起绘制。

Q2:如果界面一直保持没变的话,那么还会每隔 16.6ms 刷新一次屏幕么?
A2:界面没有重绘,应用就不会收的vsync信号,CPU没有向GPU提交数据,但屏幕还是会刷新,依旧会隔16.6ms读取缓冲区数据,只是数据还是旧的,所以看起来没什么变化而已

Q3:界面的显示其实就是一个 Activity 的 View 树里所有的 View 都进行测量、布局、绘制操作之后的结果呈现,那么如果这部分工作都完成后,屏幕会马上就刷新么?
A3:不会

Q4:网上都说避免丢帧的方法之一是保证每次绘制界面的操作要在 16.6ms 内完成,但如果这个 16.6ms 是一个固定的频率的话,请求绘制的操作在代码里被调用的时机是不确定的啊,那么如果某次用户点击屏幕导致的界面刷新操作是在某一个 16.6ms 帧快结束的时候,那么即使这次绘制操作小于 16.6 ms,按道理不也会造成丢帧么?这又该如何理解?
A4:重绘不是马上开始,是在下一次垂直同步信号到来的时候,才进行绘制

Q5:大伙都清楚,主线程耗时的操作会导致丢帧,但是耗时的操作为什么会导致丢帧?它是如何导致丢帧发生的
A5:造成丢帧大体上有两类原因,一是遍历绘制 View 树计算屏幕数据的时间超过了 16.6ms;二是,主线程一直在处理其他耗时的消息,导致遍历绘制 View 树的工作迟迟不能开始,从而超过了 16.6 ms 底层切换下一帧画面的时机。

  • 0
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Android的视图绘制Android应用程序中的重要部分,它涉及到将用户界面元素绘制屏幕上。以下是Android视图绘制的基本流程: 1. 触发绘制:当应用程序启动、布局发生变化或者手动调用 `invalidate()` 方法时,会触发视图绘制。 2. 测量布局:在绘制之前,Android会测量每个视图的大小。这个过程称为“测量布局”。测量布局是为了确定每个视图在屏幕上的位置和大小。 3. 布局:一旦测量完成,Android会根据视图的测量结果进行布局,确定每个视图在屏幕上的位置。 4. 绘制:布局完成后,Android会调用每个视图的 `draw()` 方法进行绘制。在 `draw()` 方法中,视图会绘制自己的内容,包括背景、文字、图片等。 5. 绘制层次:视图的绘制按照层次结构进行,即从父视图到子视图的顺序。父视图会先绘制自己,然后再绘制子视图。 6. 递归绘制:当父视图绘制完成后,它会递归地调用子视图的 `draw()` 方法,依次完成整个视图树的绘制过程。 7. 绘制缓存:为了提高绘制性能,Android使用了绘制缓存。绘制缓存可以将视图的绘制结果保存起来,在下次绘制时直接使用缓存,而不需要重新执行绘制操作。 总结来说,Android的视图绘制过程包括测量布局、布局、绘制绘制缓存。通过这个过程,Android应用程序可以将用户界面元素绘制屏幕上,实现丰富多样的交互效果。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值