音视频同步与渲染:PTS、VSYNC 与 SurfaceFlinger 的协作之道

0 阅读16分钟

你应该有过这种体验:看一段讲课视频,口型和声音差了半秒,难受到想关掉。或者打王者荣耀直播,英雄死了一秒后才听到"一血"的提示音。

这些体验的背后,都是音视频同步出了问题。

Android 的视频子系统每秒要处理 30 到 120 帧的视频,同时还要精确控制音频输出的时机,确保两者在时间上对齐到几十毫秒的精度。这不是一件容易的事——尤其是视频帧的解码时间本身就是不均匀的,网络传输的抖动更会带来不可预测的延迟。

本文是「Android 15 视频子系统」系列的收官篇,我们来把整个渲染链路从头捋一遍:从 PTS 时间戳的生成,到 SurfaceFlinger 的 VSYNC 驱动合成,把"为什么音视频会不同步"和"如何让它们同步"讲清楚。


为什么音视频同步是个难题

在理想世界里,音频和视频都从同一个时间轴上取数据,同时解码,同时输出,天下太平。

现实世界里:

视频帧 #100,编码时间: 3330ms
  → 解码耗时: 8msI帧)或 2msP帧)
  → 等待 Output Buffer: 可能额外延迟 5~20ms
  → 渲染到屏幕:下一个 VSYNC 时机

音频帧 #100,编码时间: 3330ms
  → 解码耗时: < 1msAAC 软解)
  → AudioFlinger ring buffer 延迟: 20~60ms
  → 扬声器实际播放: 约 40ms

两者从"编码完成"到"用户感知",经历了完全不同的路径,自然会有偏差。没有主动的同步机制,音画错位是必然的。

Android 的解法很简单粗暴:以音频输出时间为绝对基准,视频帧主动向音频时钟对齐


PTS:时间戳的生命旅程

什么是 PTS

PTS(Presentation Time Stamp,显示时间戳)是容器格式(MP4、MKV 等)为每一帧数据打上的时间标记,单位通常是微秒(µs)或时间基(time base)。

它回答了一个问题:这一帧应该在什么时刻展示给用户?

注意区分:

  • DTS(Decoding Time Stamp):解码时间戳——视频压缩中 B 帧的解码顺序与展示顺序不同,DTS 记录解码顺序
  • PTS(Presentation Time Stamp):显示时间戳——无论解码顺序如何,最终显示给用户的顺序

对于音频,PTS 和 DTS 通常相同(音频没有 B 帧的概念)。

PTS 从容器到屏幕的旅程

MP4 容器
  → MediaExtractor.readSampleData()
  → ABuffer.meta()->setInt64("timeUs", pts)  ← PTS 附在数据包上
  → NuPlayer::Decoder Input Buffer
  → MediaCodec.queueInputBuffer(timeUs = pts)  ← 传给硬件解码器
  → MediaCodec.dequeueOutputBuffer(info.presentationTimeUs)  ← 从解码器取出
  → NuPlayer::Renderer 视频队列
  → 与音频时钟对比 → 决定渲染时机
  → releaseOutputBuffer(render=true)  → Surface

关键一点:PTS 在整个链路中全程跟随数据包,不在中途重新计算。这意味着只要 Renderer 能拿到正确的 PTS,它就能知道这帧该在什么时候显示。

PTS 精度问题

MediaExtractor 读出的 PTS 精度依赖容器格式:

// MP4 的时间基通常是 1/timescale
// 如果 timescale = 90000(常见值),则精度 ≈ 11µs
// 如果 timescale = 1000,则精度只有 1ms

// MediaExtractor 会自动换算为微秒
long sampleTimeUs = extractor.getSampleTime(); // 单位: µs

90000 这个时间基是 MPEG 的"老规矩",能被常见帧率(24/25/30/50/60fps)整除。如果你见到 PTS 不是 33333µs 的整数倍,但帧率是 30fps,通常是因为时间基精度问题,不是 bug。


音视频时钟同步

三种时钟源

NuPlayer 的 Renderer 在内部维护了一个 MediaClock,可以使用三种时钟源:

时钟源优先级精度适用场景
音频时钟(AudioTrack.getTimestamp)最高~1ms正常含音频播放
系统时钟(CLOCK_MONOTONIC)亚微秒无音频/音频暂停
视频时钟(估算)最低较差降级场景
// frameworks/av/media/libstagefright/MediaClock.cpp
int64_t MediaClock::getMediaTime(int64_t realUs) const {
    if (mAnchorTimeMediaUs < 0) {
        return -1; // 时钟未启动
    }
    // 核心公式:媒体时间 = 锚点 + (实际流逝时间 × 播放速率)
    int64_t mediaUs = mAnchorTimeMediaUs +
        (realUs - mAnchorTimeRealUs) * (double)mPlaybackRate;
    return mediaUs;
}

音频时钟的获取

这是精度最高的路径,也是默认路径:

// Renderer 从 AudioTrack 获取精确时钟
bool NuPlayer::Renderer::getAnchorTime(int64_t *mediaUs, int64_t *realUs) {
    AudioTimestamp ts;
    if (mAudioSink->getTimestamp(ts) == OK) {
        // mPosition: AudioTrack 已播放的帧数
        // mTime: 对应的系统时间(来自 AudioFlinger)
        int64_t playedFrames = ts.mPosition;
        int64_t playedUs = playedFrames * 1000000LL / mAudioSampleRate;

        // 把"已播放帧数对应的媒体时间"和"那个时刻的系统时间"记录下来
        // 作为锚点,之后用实时系统时钟推算当前媒体时间
        *mediaUs = mAnchorStartMediaUs + playedUs;
        *realUs = convertTimespecToUs(ts.mTime);
        return true;
    }
    return false;
}

AudioTimestamp 的精度来自内核的音频硬件时间戳,通常在 1ms 以内,远比轮询系统时钟更准确。这就是为什么音频时钟是首选。

同步仲裁:Renderer 的核心决策

音视频同步的核心逻辑在 NuPlayer::Renderer::onDrainVideoQueue()

void NuPlayer::Renderer::onDrainVideoQueue() {
    QueueEntry &entry = *mVideoQueue.begin();

    // 从 MediaCodec Output Buffer 取出 PTS
    int64_t videoPtsUs = entry.mTimeUs;

    // 当前音频时钟位置(换算到媒体时间)
    int64_t nowMediaUs = -1;
    if (!getAnchorTime(nullptr, nullptr)) {
        // 音频时钟还没建立(刚开始播放),用系统时钟
        nowMediaUs = ALooper::GetNowUs() - mAnchorTimeRealUs
                     + mAnchorTimeMediaUs;
    } else {
        nowMediaUs = getCurrentMediaTime(ALooper::GetNowUs());
    }

    // 计算视频帧与音频时钟的偏差
    // 正值 = 视频落后音频(需要加速/丢帧)
    // 负值 = 视频超前音频(需要等待)
    int64_t lateByUs = nowMediaUs - videoPtsUs;

    if (lateByUs > kTooLateThresholdUs) {
        // 落后超过 40ms:丢帧!宁可跳帧也不卡顿
        mCodec->releaseOutputBuffer(entry.mBufferIx, false);
        mVideoQueue.erase(mVideoQueue.begin());
        ++mNumFramesDropped;

        ALOGV("[video] late by %.2f ms, dropping frame",
              lateByUs / 1E3);
        return;
    }

    if (lateByUs < -kEarlyThresholdUs) {
        // 超前超过 30ms:还没到时候,重新调度
        sp<AMessage> msg = new AMessage(kWhatDrainVideoQueue, this);
        // 延迟投递:等到快要到时间再检查
        msg->post(-lateByUs - kEarlyThresholdUs);
        return;
    }

    // 处于 [-30ms, +40ms] 窗口内:立即渲染
    mCodec->releaseOutputBuffer(entry.mBufferIx, true /* render */);
    mVideoQueue.erase(mVideoQueue.begin());
    ++mNumFramesRendered;
}

// 阈值定义
static const int64_t kTooLateThresholdUs  = 40000LL;  // 40ms
static const int64_t kEarlyThresholdUs    = 30000LL;  // 30ms

10-01-av-sync.png

这 70ms 的窗口(-30ms 到 +40ms)是经验值:人类视觉对视频超前声音的容忍度比滞后更低,所以允许视频落后的空间(40ms)大于超前的空间(30ms)。


Surface 渲染 Pipeline

从 releaseOutputBuffer 到像素点亮

当 Renderer 调用 releaseOutputBuffer(bufferIx, true) 时,一帧视频开始了它的最后一段旅程:

MediaCodec.releaseOutputBuffer(true)
    │
    ▼ GraphicBuffer 句柄(零拷贝)
Surface.queueBuffer()
    │
    ▼ BufferQueue(生产者侧)
SurfaceFlinger 接收信号
    │
    ▼ 等待下一个 VSYNC
Layer 合成(GPU or HWC)
    │
    ▼ Framebuffer
Display Controller
    │
    ▼ 屏幕上的像素发光

整个过程没有一次内存拷贝:HW Decoder 解码输出的 GraphicBuffer,通过 BufferQueue 的 slot 机制,被 SurfaceFlinger 直接取走合成,零拷贝。

BufferQueue:生产者与消费者

BufferQueue 是 Android 图形系统的核心抽象,连接"生产图像的人"和"消费图像的人":

生产者(Decoder/App)            消费者(SurfaceFlinger)
─────────────────────────────────────────────────────
1. dequeueBuffer(slot)         2. acquireBuffer(slot)
   获取空闲的 GraphicBuffer槽      取出已填充的槽
3. 填充解码数据(VPU 直写)     4. 合成到屏幕
5. queueBuffer(slot)           6. releaseBuffer(slot)
   标记为"已就绪"                  归还槽,供生产者复用

槽(slot)的数量通常是 2 个(double buffer)或 3 个(triple buffer):

  • Double Buffer(2 槽):内存占用小,但如果 SurfaceFlinger 没及时消费,生产者会在 queueBuffer 时阻塞,可能导致掉帧
  • Triple Buffer(3 槽):多一个备用槽,让生产者和消费者之间有更多调度余地,以内存换流畅度

Android 9+ 默认使用 triple buffer。

SurfaceFlinger:图层的幕后导演

SurfaceFlinger 的核心工作是在每个 VSYNC 信号到来时,把所有应用提交的图层合成为一帧送给显示控制器。

合成方式有两种:

GPU Composition(软件合成)

  • 用 OpenGL ES 把所有图层绘制到一个 Framebuffer
  • 支持任意旋转、缩放、Alpha 混合
  • 消耗 GPU 资源,产生额外延迟(GPU 处理时间)

HWC Composition(硬件合成)

  • Hardware Composer HAL 直接控制显示控制器的 overlay 层
  • 不走 GPU,延迟更低,更省电
  • 有数量限制(通常 4~8 个 overlay 层)
  • 全屏视频播放走这条路
// SurfaceFlinger 决策示意(简化)
void SurfaceFlinger::composeLayers() {
    // 询问 HWC 能处理哪些图层
    hwc2_error_t err = mHwcDevice->validateDisplay(hwcDisplay, ...);

    for (auto& layer : layers) {
        if (layer->getCompositionType() == HWC2::Composition::Client) {
            // HWC 拒绝处理,走 GPU 合成
            renderLayerWithGpu(layer);
        }
        // else: HWC Overlay 直接处理,不需要 CPU/GPU 介入
    }
    // 提交给 HWC,等待 VSYNC 后输出
    mHwcDevice->presentDisplay(hwcDisplay, ...);
}

10-02-render-pipeline.png


VSYNC 与 Choreographer

为什么需要 VSYNC

屏幕的刷新是周期性的,60Hz 的屏幕每隔 16.67ms 扫描一次。如果你在屏幕扫描到一半时更新了图像内容,就会出现画面撕裂(Tearing)——屏幕上半部分是旧帧,下半部分是新帧。

VSYNC(Vertical Synchronization)信号就是屏幕每次刷新时发出的同步脉冲,所有图像更新都应该与这个脉冲同步。

Android 的 VSYNC 分发体系

Android 4.1(Project Butter)引入了 Choreographer 框架,把 VSYNC 信号精确分发给需要的组件:

屏幕硬件 → HW VSYNC (16.67ms)
              │
              ▼
         DispSync (软件 PLL)
         ├── SF VSYNC → SurfaceFlinger (合成时机)
         └── App VSYNC → Choreographer (动画时机)
                              │
                              ▼
                    View.invalidate() 触发重绘
                    MediaCodec.releaseOutputBuffer() 触发渲染
                    Animator.update() 更新动画

SF VSYNC 通常比 App VSYNC 提前约一个合成时间(4~6ms),给 SurfaceFlinger 留足合成时间。

Choreographer 在视频渲染中的作用

对于视频播放,Choreographer 的直接作用是:让 App 知道屏幕刷新的时机,从而在正确的时刻投递下一帧

// 如果需要在应用层精确控制视频帧投递时机
class VideoSurface(context: Context) : View(context) {

    private val choreographer = Choreographer.getInstance()

    private val frameCallback = Choreographer.FrameCallback { frameTimeNanos ->
        // frameTimeNanos 是这个 VSYNC 的时间戳(纳秒)
        val frameTimeUs = frameTimeNanos / 1000L

        // 检查是否有应该在这个时机渲染的视频帧
        checkAndRenderFrame(frameTimeUs)

        // 注册下一帧回调
        choreographer.postFrameCallback(this.frameCallback)
    }

    fun start() {
        choreographer.postFrameCallback(frameCallback)
    }

    private fun checkAndRenderFrame(vsyncTimeUs: Long) {
        val nextFrame = videoFrameQueue.peek() ?: return
        // 如果帧的 PTS 在这个 VSYNC 窗口内,就渲染
        if (nextFrame.ptsUs <= vsyncTimeUs + VSYNC_TOLERANCE_US) {
            val frame = videoFrameQueue.poll()
            surface.attachFrame(frame)
        }
    }
}

NuPlayer 的 Renderer 内部做的事与此类似,但集成在 ALooper 消息循环里,而不是直接使用 Choreographer API。


深入 AudioTrack 时间戳

为什么 AudioTrack.getTimestamp 如此重要

前面多次提到"音频时钟",它的精确性来源是 AudioTrack.getTimestamp()

val audioTrack = AudioTrack.Builder()
    .setAudioFormat(AudioFormat.Builder()
        .setSampleRate(44100)
        .setEncoding(AudioFormat.ENCODING_PCM_16BIT)
        .setChannelMask(AudioFormat.CHANNEL_OUT_STEREO)
        .build())
    .build()

audioTrack.play()

// 获取精确时间戳
val timestamp = AudioTimestamp()
if (audioTrack.getTimestamp(timestamp)) {
    // timestamp.framePosition: 已经"从扬声器出来"的帧数
    // timestamp.nanoTime:       对应的系统时间(CLOCK_MONOTONIC)

    val playedUs = timestamp.framePosition * 1_000_000L / 44100L
    val systemTimeUs = timestamp.nanoTime / 1000L

    // 从这个锚点可以推算当前的播放位置
    val nowUs = System.nanoTime() / 1000L
    val currentPositionUs = playedUs + (nowUs - systemTimeUs)
}

timestamp.framePosition 来自内核的音频硬件计数器,精度在 1ms 以内,是真正的"已经从扬声器播出去的帧数",不是写入 ring buffer 的帧数——这个区别很重要,因为 AudioFlinger 的 ring buffer 本身就有 20~60ms 的延迟。

AudioFlinger 的延迟对同步的影响

一个常见误解是:AudioTrack.write() 写入数据后,声音立刻就出来了。

实际上:

AudioTrack.write(pcmData)
    │ 写入 ring buffer(延迟 ≈ 0ms)
    ▼
AudioFlinger MixerThread
    │ 混音多个应用的音频(20~40ms 周期)
    ▼
Audio HAL → DSP
    │ DSP 处理延迟(5~15ms)
    ▼
扬声器/耳机输出
总延迟: 约 40~80ms

这就是为什么同步时要用 getTimestamp() 而不是用写入的字节数来计算时间——前者反映的是真实的"播出去"时刻,后者包含了 ring buffer 里还没有播出的部分。


卡顿检测与丢帧分析

识别音视频同步问题

# 实时查看 NuPlayer 丢帧情况
adb logcat | grep "dropping video"
# 输出示例:
# D NuPlayerRenderer: [video] late by 55.3ms, dropping frame

# 查看音视频时钟偏差
adb logcat | grep "AV"
# D NuPlayerRenderer: AV sync late by 12ms

# 查看整体渲染统计
adb shell dumpsys media.player | grep -A 20 "NuPlayer"

SurfaceFlinger 帧率分析

# 查看每个图层的帧统计
adb shell dumpsys SurfaceFlinger | grep -A 5 "Layer"

# 帧时间分布(jank 分析)
adb shell dumpsys gfxinfo <package_name>

# 输出包含:
# Total frames rendered: 1234
# Janky frames: 23 (1.86%)  ← jank 率
# 90th percentile: 8ms
# 95th percentile: 12ms
# 99th percentile: 32ms     ← P99 超过 16ms 说明有掉帧

Perfetto 精确分析

# 抓取包含音视频同步的完整 trace
adb shell perfetto \
  -c - --txt \
  -o /data/local/tmp/av_trace.pb << 'EOF'
buffers { size_kb: 102400 }
data_sources {
  config {
    name: "linux.ftrace"
    ftrace_config {
      ftrace_events: "sched/sched_switch"
      ftrace_events: "power/suspend_resume"
      atrace_categories: "am"
      atrace_categories: "audio"
      atrace_categories: "video"
      atrace_categories: "gfx"
      atrace_categories: "input"
    }
  }
}
duration_ms: 10000
EOF

adb pull /data/local/tmp/av_trace.pb ./
# 在 https://ui.perfetto.dev 中打开,搜索 NuPlayer 相关 trace event

在 Perfetto 中能清楚看到:

  • 每一帧的解码时间
  • VSYNC 信号的到来时刻
  • SurfaceFlinger 的合成耗时
  • 音频写入和实际播出的时间差

实战:直播低延迟优化

直播场景是音视频同步最极端的考验:观众希望延迟尽可能低(< 3 秒甚至 < 1 秒),但又不能为了低延迟牺牲画质和流畅度。

默认 NuPlayer 的问题

NuPlayer 是为点播场景设计的,有较激进的缓冲策略(2~5 秒缓冲水位)。对于直播:

问题 1: 积累的缓冲 = 积累的延迟
问题 2: 缓冲不足时卡顿,而非平滑降级
问题 3: 音视频时钟偏差容忍窗口对直播来说太宽松

用 ExoPlayer 实现低延迟直播

ExoPlayer(Media3)提供了专门的低延迟直播支持:

// 低延迟直播播放器配置
val player = ExoPlayer.Builder(context)
    .setLoadControl(
        DefaultLoadControl.Builder()
            // 直播场景:最小缓冲压缩到 500ms
            .setBufferDurationsMs(
                /* minBufferMs = */ 500,
                /* maxBufferMs = */ 2000,
                /* bufferForPlaybackMs = */ 500,
                /* bufferForPlaybackAfterRebufferMs = */ 1000
            )
            .build()
    )
    .build()

// 低延迟直播媒体配置
val mediaItem = MediaItem.Builder()
    .setUri(hlsLiveUrl)
    .setLiveConfiguration(
        MediaItem.LiveConfiguration.Builder()
            // 目标延迟:1 秒
            .setTargetOffsetMs(1000)
            // 允许的延迟范围:0.5s ~ 2s
            .setMinOffsetMs(500)
            .setMaxOffsetMs(2000)
            // 通过调整播放速率(0.97x ~ 1.03x)来维持目标延迟
            .setMinPlaybackSpeed(0.97f)
            .setMaxPlaybackSpeed(1.03f)
            .build()
    )
    .build()

player.setMediaItem(mediaItem)
player.prepare()
player.play()

// 监听延迟变化
player.addListener(object : Player.Listener {
    override fun onPlaybackStateChanged(state: Int) {
        if (state == Player.STATE_READY) {
            val currentLiveOffsetMs = player.currentLiveOffset
            Log.d("LivePlayer", "当前延迟: ${currentLiveOffsetMs}ms")
        }
    }
})

ExoPlayer 的低延迟直播机制:当直播流延迟大于目标值时,将播放速率微调至 1.03x;当延迟小于目标值时,降速至 0.97x。这种微小的速率调整用户几乎感受不到(人耳对 3% 的速率变化不敏感),但能有效追赶或放缓直播流。

音视频同步的校准

自定义播放器时,如果发现音画不同步,可以通过以下方式校准:

// ExoPlayer:设置音视频时间偏移
player.setVideoFrameMetadataListener { presentationTimeUs, releaseTimeNs, format, mediaFormat ->
    // presentationTimeUs: 这帧的 PTS
    // releaseTimeNs: 系统决定渲染这帧的时刻(纳秒)
    // 可以在这里插入自定义同步逻辑
}

// 手动设置音视频偏移(正值:视频滞后于音频)
player.setVideoFrameMetadataListener(...)

// MediaPlayer:AudioSync offset(通过 AudioTrack)
val audioTrack = ... // 获取当前 AudioTrack 引用
audioTrack.setLatency(customLatencyUs) // 非公开 API,不推荐

对于大多数应用,不需要手动校准——Android 默认的同步机制已经处理得足够好。手动干预通常是在做专业直播或视频制作工具时才需要。


高刷新率与可变刷新率

120Hz / 144Hz 下的同步挑战

高刷新率屏幕(120Hz / 144Hz)已经在旗舰手机上普及,VSYNC 周期缩短到 8.33ms / 6.94ms。这对视频播放带来了新的挑战:

30fps 视频在 120Hz 屏幕上

视频帧周期: 33.33ms
VSYNC 周期: 8.33ms
比例: 4:1

→ 每帧视频需要在 4 个 VSYNC 中重复显示
→ 理想情况:每帧显示 4 个 VSYNC(33.33ms)
→ 问题:33.33ms / 8.33ms = 3.9996(不是整数!)
→ 长期下来,某些帧会交替显示 4 个或 3 个 VSYNC
→ 肉眼可见的轻微抖动(judder)

解决方案:自适应刷新率(JANK 消除)

Android 11 引入的 setFrameRate() API 允许应用声明视频的帧率,系统动态调整屏幕刷新率以避免抖动:

// 告诉系统这个 Surface 的内容帧率是 24fps
surface.setFrameRate(
    24.0f,                    // 目标帧率
    Surface.FRAME_RATE_COMPATIBILITY_FIXED_SOURCE,  // 内容固定帧率
    Surface.CHANGE_FRAME_RATE_ALWAYS               // 总是切换
)

系统会把屏幕刷新率切换到 24fps 的整数倍(如 48Hz 或 96Hz),消除抖动。这也是为什么在部分手机上播放 24fps 电影时屏幕刷新率会自动变化。

VRR(可变刷新率)

Snapdragon 8 Gen 2+ 和部分联发科 SoC 支持 LTPO(低温多晶氧化物)可变刷新率技术,可在 1Hz ~ 120Hz 之间动态调整:

静止画面 → 1Hz(省电)
普通滚动 → 60Hz
游戏 / 视频 → 根据内容帧率切换
高帧率内容 → 120Hz / 144Hz

SurfaceFlingerDisplayManager 共同管理这个动态刷新率,对于视频播放,setFrameRate() 声明会触发刷新率的自动切换。


总结:整个视频链路的全景回顾

至此,我们已经走完了 Android 视频子系统的完整旅程。

Camera2 API 捕获一帧图像,到 Camera HAL3 与 ISP 的硬件交互,再到 MediaCodec / Codec2 的硬件编解码,经过 NuPlayer 的 Source/Decoder/Renderer 三驾马车,最终通过 SurfaceFlinger + VSYNC 让像素点亮在屏幕上——每一个环节都是精密机器的一个齿轮。

音视频同步的核心原则只有一条:以音频时钟为绝对基准,视频帧主动向音频时钟对齐。

具体实现上的关键点:

  1. PTS 全程跟随:时间戳从容器解析开始,一路跟着数据包传递到渲染层,不在中途重算
  2. 音频时钟精度AudioTrack.getTimestamp()framePosition 来自内核硬件计数器,比轮询系统时钟准确得多
  3. 丢帧而非卡顿:落后超过 40ms 的视频帧直接丢弃,保证实时性
  4. 等待而非撕裂:超前超过 30ms 的视频帧延迟投递,等待音频追上来
  5. VSYNC 驱动渲染:所有图像更新都与 VSYNC 信号对齐,避免画面撕裂
  6. HWC 零拷贝:全屏视频走 Hardware Composer Overlay,不经过 GPU 合成

你可以做的事:

  • 在自己的设备上运行 adb logcat | grep "dropping video",看看播放时有多少丢帧
  • 用 Perfetto 分析一次视频播放的完整 trace,定位可能的卡顿点
  • 如果开发直播应用,评估使用 ExoPlayer 的 LiveConfiguration 来控制延迟目标
  • 对于高帧率内容,调用 Surface.setFrameRate() 让系统优化刷新率匹配

参考资料