Vsync机制和Choreographer详解

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 信号,回调编舞者,从而最终回调我们的注册监听。

  • 3
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 8
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值