6、BufferQueue详解:生产者-消费者模型、Buffer状态机与dequeueBuffer超时

说到Android图形系统,BufferQueue绝对是个绕不开的核心组件。我最早接触它的时候,是在做相机预览卡顿优化——那时候我还不清楚,为什么有时候应用层明明调用了dequeueBuffer,却会卡住几百毫秒。后来追到源码里一看,才明白这背后是一套精巧的状态机在运作。

说白了,BufferQueue就是一个「生产者-消费者」的缓冲区队列。生产者往队列里放数据,消费者从队列里取数据。但跟普通队列不一样的是,这里的「数据」是图形缓冲区——也就是一块块显存。

核心要点:BufferQueue管理的是GraphicBuffer的流转,而不是数据拷贝。每个Buffer在生命周期内会经历4种状态:FREE → DEQUEUED → QUEUED → ACQUIRED → FREE。

6.1 生产者-消费者模型

先看一个最简单的场景。SurfaceFlinger是消费者,某个App是生产者。App通过Surface往BufferQueue里提交帧,SurfaceFlinger从BufferQueue里取帧去做合成。

我习惯把BufferQueue比作一个「环形缓冲区」——但比环形缓冲区复杂得多。它内部维护了一个Buffer数组,每个Buffer都有独立的生命周期。

// 核心数据结构(简化版)
class BufferQueue {
    Vector<BufferItem> mSlots;  // Buffer槽位数组
    int mDequeueCount;          // 当前已DEQUEUED的Buffer数
    int mMaxDequeueCount;       // 最大允许DEQUEUED数(通常=总Buffer数-1)
    
    status_t dequeueBuffer(...);
    status_t queueBuffer(...);
    status_t acquireBuffer(...);
    status_t releaseBuffer(...);
};

这里有个关键点:生产者不能无限制地dequeue。mMaxDequeueCount限制了同时处于DEQUEUED状态的Buffer数量。为什么?因为如果所有Buffer都被生产者拿走了,消费者就没东西可消费了。

我的经验:在Android 10之前,默认的Buffer数量是3个。后来为了降低延迟,很多厂商改成了4个。但Buffer越多,内存占用越大,这是个trade-off。

6.2 Buffer状态机

每个Buffer在任意时刻都处于以下4种状态之一:

状态 含义 谁持有 典型操作
FREE 空闲,可被生产者获取 BufferQueue dequeueBuffer
DEQUEUED 生产者正在写入数据 生产者 queueBuffer / cancelBuffer
QUEUED 生产者已提交,等待消费者消费 BufferQueue acquireBuffer
ACQUIRED 消费者正在读取数据 消费者 releaseBuffer

状态流转图是这样的:

FREE DEQUEUED QUEUED ACQUIRED dequeueBuffer queueBuffer acquireBuffer releaseBuffer cancelBuffer Buffer 状态机流转图

嗯,这里要注意:cancelBuffer 是个容易被忽略的操作。如果生产者dequeue了一个Buffer,但后来发现不需要了(比如丢帧了),必须调用cancelBuffer把它还回去。否则这个Buffer就一直卡在DEQUEUED状态,其他Buffer也拿不到。

我曾经踩过的坑:有个第三方App在快速滑动时频繁丢帧,但没调用cancelBuffer。结果BufferQueue里所有Buffer都被DEQUEUED了,SurfaceFlinger拿不到新帧,直接导致界面卡死。后来我们在BufferQueue里加了超时检测,才定位到这个问题。

6.3 dequeueBuffer超时问题

这是实际开发中最常遇到的性能问题之一。dequeueBuffer为什么会超时?

先看源码逻辑:

status_t BufferQueueProducer::dequeueBuffer(
        int* outSlot, sp<Fence>* outFence,
        uint32_t width, uint32_t height,
        PixelFormat format, uint64_t usage,
        uint64_t* outBufferAge,
        FrameEventHistoryDelta* outTimestamps) {
    
    // 1. 尝试获取一个FREE状态的Buffer
    int found = -1;
    for (int i = 0; i < mSlots.size(); i++) {
        if (mSlots[i].mBufferState == BufferState::FREE) {
            found = i;
            break;
        }
    }
    
    // 2. 如果没有FREE的Buffer,等待消费者释放
    if (found == -1) {
        // 这里会阻塞,等待消费者releaseBuffer
        status_t result = waitForFreeSlotThenRelock(..., &found);
        if (result != NO_ERROR) {
            return result;
        }
    }
    
    // 3. 设置状态为DEQUEUED
    mSlots[found].mBufferState = BufferState::DEQUEUED;
    *outSlot = found;
    return NO_ERROR;
}

超时的根本原因只有一个:没有FREE状态的Buffer可用。但具体场景有几种:

  • 消费者处理太慢:SurfaceFlinger合成一帧的时间超过了16ms,导致ACQUIRED的Buffer迟迟不释放
  • 生产者dequeue太多:应用层同时dequeue了多个Buffer,但没有及时queue回来
  • Buffer数量配置不足:比如只有2个Buffer,但生产者和消费者各占一个,第三个请求就只能等
  • 死锁:生产者等消费者释放,消费者等生产者提交——但双方都在等对方先动

排查思路:我一般先看dumpsys SurfaceFlinger的输出,重点关注每个Buffer的状态。如果发现大量Buffer处于ACQUIRED状态,那问题大概率在SurfaceFlinger侧。如果全是DEQUEUED,那问题在应用侧。

6.4 实际优化案例

说个我去年遇到的真实案例。某款手机在微信视频通话时,画面会间歇性卡顿。用systrace抓了一看,dequeueBuffer的等待时间经常超过100ms。

追查后发现:

  1. 相机HAL层在拍照时,会额外dequeue一个Buffer用于编码
  2. 这个Buffer被dequeue后,因为编码器还没准备好,一直没queue回来
  3. 导致预览用的Buffer只剩2个,偶尔出现全部被占用的状况

解决方案其实不复杂:增加BufferPool的大小,从3个改成5个。同时给编码器用的Buffer单独分配,不跟预览共用。

我的建议:如果你在优化相机或视频类应用,可以尝试把Buffer数量从3改成4或5。但别改太多——每个Buffer都是显存,1280x720的YUV420格式就要1.3MB,4个就是5MB多。

6.5 调试工具与方法

最后分享几个我常用的调试手段:

  • dumpsys SurfaceFlinger:查看所有Layer的Buffer状态,最直接
  • systrace:在dequeueBuffer/queueBuffer/acquireBuffer/releaseBuffer处打trace点,看耗时分布
  • 自定义日志:在BufferQueue的构造函数里加log,打印每次状态变更
  • GPU渲染分析:用RenderDoc或Snapdragon Profiler看GPU侧的Buffer使用情况

说实话,BufferQueue的问题大多数时候不是代码逻辑错了,而是时序问题。生产者太快、消费者太慢、或者中间某个环节卡了一下,都会导致Buffer流转不畅。理解了状态机,你就能快速定位到底是哪个环节出了问题。


公众号:蓝海资料掘金营,微信deep3321