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 |
状态流转图是这样的:
嗯,这里要注意: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。
追查后发现:
- 相机HAL层在拍照时,会额外dequeue一个Buffer用于编码
- 这个Buffer被dequeue后,因为编码器还没准备好,一直没queue回来
- 导致预览用的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