Vsync机制和Choreographer详解

本文深入解析了VSync机制及其在Android系统中的作用,包括屏幕刷新率、帧率的概念,以及如何通过VSync协调GPU和Display频率,解决画面撕裂问题。同时,详细介绍了Buffer缓存机制,如单缓存、双缓存和三缓存的工作原理,以及Choreographer如何配合VSync机制,实现界面绘图的统一调度。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

UI 卡顿定义

  • 用户角度:APP 操作比较缓慢,响应不及时,列表滑动卡顿,动画刷新不流畅等

  • 系统角度:屏幕刷新帧率不稳定,无法保证每秒60(跟手机有关)帧刷新频率,出现掉帧现象

卡顿原因及常见解决方式

  1. 过度绘制

    • 去除不必要背景
    • 布局视图尽量扁平化
    • 减少透明色的使用
  2. UI 线程进行过度计算任务

    • 减少在 UI 线程中进行重度计算任务
  3. 频繁 GC

    • 频繁 GC 的原因
      1. 内存抖动
      2. 瞬间产生大量的对象
    • 尽量减少在循环中 new 对象或者使用局部变量
    • 避免在 draw 方法中创建对象

VSync

定义

  • Android4.1(Jelly Bean)引入了Vsync(垂直同步信号量)

  • 屏幕刷新率
    Refresh Rate 或者 SurfaceFlinger,设备刷新屏幕的频率

  • 帧率
    Frame Rate,单位:FPS,指 GPU 生成帧的频率

  • VSync
    屏幕产生硬件的 VSync,由 SurfaceFlinger 将其转换成软件的 VSync 信号。

作用

  • 用来同步渲染,让 AppUI 和 SurfaceFlinger 可以按硬件产生的 VSync 节奏进行工作

  • 主要为了解决 “Tearing”(撕裂) 现象

  • 同步 UI 绘制和动画,使他们尽可能的获得一个达到 60fps 的固定帧率

工作原理

刷新率和帧速率需要协同工作,才能让你的应用程序的内容显示到屏幕上,GPU会获取图像数据进行绘制,然后负责把内容呈现到屏幕上,这将在你的应用程序的生命周期中周而复始地执行.

但是刷新率和帧速率并不是总能够保持相同的节奏,这样就会出现丢帧或者撕裂的现象,Google 为此引入了 Buffer 的缓存机制

Buffer 缓存机制

单缓存

一块缓存区域,由 CPU 计算完成,交给 GPU 进行绘制,然后在输出到 Buffer 缓冲区中,Display 屏幕不断的从 Buffer 中获取内容进行显示。

在这里插入图片描述

在这里插入图片描述

如上图,使用单缓存, VSync 通知刷新,但是第二帧还未准备好,这时继续绘制第一帧,等第二帧准备完毕时,多显示了一次第一帧,后续流程相同。会一直出现等待显示的问题。

双缓存

两块缓存区域,屏幕始终从 Frame Buffer 中取出数据进行显示,GPU 始终将渲染完的数据填充到 Back Buffer 中。这两个区域交换时直接更改物理地址名,即将 FrameBuffer 指向 BackBuffer地址,将 BackBuffer 指向 FrameBuffer地址,所以耗时基本可以忽略

在这里插入图片描述

在这里插入图片描述

如图所示,比较糟糕的一种情况,做了某种重度操作,导致 CPU 和 GPU 特别慢,还是会出现上面那种情况,重复显示同一帧的问题。

三缓存

使用二级缓存基本可以满足了,除非遇到极端情况才会出现丢帧卡顿的现象,这个时候的兜底逻辑,三级缓存就排上用场了。

Back Buffer一共有两块,一块是备用区。当二级缓存不能胜任时,在启用这块备用 Buffer,当二级缓存可以胜任时,备用区继续闲置。

在这里插入图片描述

以上就是有关 VSync 垂直同步了,主要就是协调 GPU 和 Display 的频率,使各个模块达到最佳的工作状态。

Choreographer

用于同Vsync机制配合,实现统一调度界面绘图。
编舞者,一个很有意思的名字。

使用

如果我们需要让自定义的 View 以 16ms 的频率进行刷新,就可以这么干。

class ChoreographerView extends View implements Choreographer.FrameCallback{
    public ChoreographerView(Context context) {
        super(context);
        Choreographer.getInstance().postFrameCallback(this);
    }
    @Override
    public void doFrame(long frameTimeNanos) {
        invalidate();
        Choreographer.getInstance().postFrameCallback(this);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        //do somthing
    }
}

源码分析

上面的使用方法,为什么可以尽可能的保证以 16ms 的频率进行回调呢,我们就来看看他的实现。

首先看一下他的构造方法,通过 ThreadLocal 保证在该线程始终只有一个该对象,使用该线程作为 key,该对象作为 value 保存在一个 map 中。

public final class Choreographer {
    public static Choreographer getInstance() {
        return sThreadInstance.get();
    }
    private static volatile Choreographer mMainInstance;
    // Thread local storage for the choreographer.
    private static final ThreadLocal<Choreographer> sThreadInstance =
            new ThreadLocal<Choreographer>() {
        @Override
        protected Choreographer initialValue() {
            Looper looper = Looper.myLooper();
            if (looper == null) {
                throw new IllegalStateException("The current thread must have a looper!");
            }
            Choreographer choreographer = new Choreographer(looper, VSYNC_SOURCE_APP);
            if (looper == Looper.getMainLooper()) {
                mMainInstance = choreographer;
            }
            return choreographer;
        }
    };
    private Choreographer(Looper looper, int vsyncSource) {
        mLooper = looper;
        mHandler = new FrameHandler(looper);
        mDisplayEventReceiver = USE_VSYNC
                ? new FrameDisplayEventReceiver(looper, vsyncSource)
                : null;
        mLastFrameTimeNanos = Long.MIN_VALUE;

        mFrameIntervalNanos = (long)(1000000000 / getRefreshRate());

        mCallbackQueues = new CallbackQueue[CALLBACK_LAST + 1];
        for (int i = 0; i <= CALLBACK_LAST; i++) {
            mCallbackQueues[i] = new CallbackQueue();
        }
    }
}

在看看构造函数里面,初始化了一些相关变量,比如使用当前线程的 Looper 创建一个处理回调的 FrameHandler。还有接受屏幕 VSync 回调的 FrameDisplayEventReceiver。

同样,先来看一下我们调用 postFrameCallback 之后发生了什么。

看到最终只是将当前的 callback 添加到回调队列中。当处理时间小于当前时间是,就执行 FrameDisplayEventReceiver 的 scheduleVsync 方法等待垂直同步信号回调,然后在执行相应的回调。

public final class Choreographer {
    public void postFrameCallback(FrameCallback callback) {
        postFrameCallbackDelayed(callback, 0);
    }
    public void postFrameCallbackDelayed(FrameCallback callback, long delayMillis) {
        ......
        postCallbackDelayedInternal(CALLBACK_ANIMATION,
                callback, FRAME_CALLBACK_TOKEN, delayMillis);
    }
    private void postCallbackDelayedInternal(int callbackType,
            Object action, Object token, long delayMillis) {
        ......
        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);
        }
    }
    private void scheduleFrameLocked(long now) {
        ......
        if (USE_VSYNC) {//使用垂直同步
            Message msg = mHandler.obtainMessage(MSG_DO_SCHEDULE_VSYNC);
            msg.setAsynchronous(true);
            mHandler.sendMessageAtFrontOfQueue(msg);
        }
    }
    private final class FrameHandler extends Handler {
        @Override
        public void handleMessage(Message msg) {
            switch (msg.what) {
                ......
                case MSG_DO_SCHEDULE_VSYNC:
                    doScheduleVsync();
                    break;
                ......
            }
        }
    }
    void doScheduleVsync() {
        ......
        scheduleVsyncLocked();
        ......
    }
    private void scheduleVsyncLocked() {
        mDisplayEventReceiver.scheduleVsync();
    }
}

我们再来看垂直同步信号的请求与回调。当调用 nativeScheduleVsync 方法时,就开始等待垂直同步信号了,当信号回来时,会回调 onVsync 方法执行后续流程。这里的回调一次的时间间隔,就相当于屏幕的刷新时间间隔,所以能尽可能的达到 16ms 的刷新频率。

public final class Choreographer {
    private final class FrameDisplayEventReceiver extends DisplayEventReceiver
            implements Runnable {
        // 这个是 DisplayEventReceiver 中的方法,放在这里便于查看调用流程
        // 等待下一个垂直同步的脉冲信号
        public void scheduleVsync() {
            ......
            nativeScheduleVsync(mReceiverPtr);
            ......
        }
        public void onVsync(long timestampNanos, long physicalDisplayId, int 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);
        }
    }
}

最后再来看下垂直同步信号回来后的回调操作。

public final class Choreographer {
    void doFrame(long frameTimeNanos, int frame) {
        ......
        AnimationUtils.lockAnimationClock(frameTimeNanos / TimeUtils.NANOS_PER_MS);
        ......
        doCallbacks(Choreographer.CALLBACK_INPUT, frameTimeNanos);
        ......
        doCallbacks(Choreographer.CALLBACK_ANIMATION, frameTimeNanos);
        ......
        doCallbacks(Choreographer.CALLBACK_INSETS_ANIMATION, frameTimeNanos);
        ......
        doCallbacks(Choreographer.CALLBACK_TRAVERSAL, frameTimeNanos);
        ......
        doCallbacks(Choreographer.CALLBACK_COMMIT, frameTimeNanos);
        ......
    }
    void doCallbacks(int callbackType, long frameTimeNanos) {
        CallbackRecord callbacks = mCallbackQueues[callbackType].extractDueCallbacksLocked(
                    now / TimeUtils.NANOS_PER_MS);
        ......
        for (CallbackRecord c = callbacks; c != null; c = c.next) {
            ......
            c.run(frameTimeNanos);
        }
    }
}

看到这里是不就明白了,说白了就是注册一个监听,什么时候回调呢?就是屏幕触发物理硬件的 VSync 信号,在通过 SurfaceFlinger 转换成软件的 VSync 信号,回调编舞者,从而最终回调我们的注册监听。

### VSYNC 接口概述 VSYNC(Vertical Synchronization,垂直同步)是一种用于图形渲染显示刷新的信号机制。它的主要作用是在显示器或屏幕更新画面时提供一个时间基准,从而实现帧率与屏幕刷新频率的一致性。这种一致性可以有效减少图像撕裂现象并提升用户体验。 #### VSYNC 的基本原理 VSYNC 通常由硬件生成,在某些情况下也可以通过软件控制。当 GPU 完成一帧的画面绘制后,会等待下一个 VSYNC 信号的到来再将该帧提交给显示屏进行渲染。这种方式能够确保每一帧都在屏幕上完全呈现而不会中途切换到下一帧[^3]。 #### 在 Android 系统中的应用 在 Android 平台中,`SurfaceFlinger` 负责管理窗口合成操作,并依赖于 VSYNC 来协调多个进程间的绘图请求。具体来说: - `VSYNC_APP`: 表示应用程序接收到的 VSYNC 事件。 - `VSYNC_SF`: 则表示 SurfaceFlinger 自身处理的 VSYNC 事件。 这些事件的时间戳可以通过工具如 Systrace 进行捕获分析,帮助开发者优化 UI 渲染性能以及排查卡顿等问题[^1]。 #### 实现方式 以下是基于 Linux 内核环境下的一个简单例子展示如何监听 VSYNC 事件: ```c #include <fcntl.h> #include <linux/fb.h> #include <sys/ioctl.h> int main() { int fb_fd = open("/dev/fb0", O_RDONLY); struct fb_vblank vblank; ioctl(fb_fd, FBIOGET_VBLANK, &vblank); printf("VSync count: %d\n", vblank.count); close(fb_fd); return 0; } ``` 此代码片段展示了打开 framebuffer 设备文件 `/dev/fb0`, 然后调用 `ioctl()` 函数获取当前垂直空白区域 (即 VSYNC) 的计数值。 ### § 1. 如何利用 Systrace 工具进一步深入理解 Offset 参数? 2. CVBS HDMI 各自适用场景有哪些区别? 3. 嵌入式开发过程中遇到 VSYNC 不稳定的情况应怎样解决? 4. 如果想降低功耗同时保持流畅体验,是否能调整 VSYNC 频率设置呢? 5. 对于多屏输出的应用场合,不同屏幕间如何共享同一个 VSYNC 信号源?
评论 8
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值