温馨提示×

温馨提示×

您好,登录后才能下订单哦!

密码登录×
登录注册×
其他方式登录
点击 登录注册 即表示同意《亿速云用户服务条款》

BufferQueue的设计思想和内部实现方法是什么

发布时间:2022-01-11 17:13:25 来源:亿速云 阅读:139 作者:iii 栏目:移动开发

这篇文章主要介绍“BufferQueue的设计思想和内部实现方法是什么”的相关知识,小编通过实际案例向大家展示操作过程,操作方法简单快捷,实用性强,希望这篇“BufferQueue的设计思想和内部实现方法是什么”文章能帮助大家解决问题。

1. 背景

对业务开发来说,无法接触到BufferQueue,甚至不知道BufferQueue是什么东西。对系统来说,BufferQueue是很重要的传递数据的组件,Android显示系统依赖于BufferQueue,只要显示内容到“屏幕”(此处指抽象的屏幕,有时候还可以包含编码器),就一定需要用到BufferQueue,可以说在显示/播放器相关的领域中,BufferQueue无处不在。即使直接调用Opengl ES来绘制,底层依然需要BufferQueue才能显示到屏幕上。

弄明白BufferQueue,不仅可以增强对Android系统的了解,还可以弄明白/排查相关的问题,如为什么Mediacodec调用dequeueBuffer老是返回-1?为什么普通View的draw方法直接绘制内容即可,SurfaceView在draw完毕后还需要unlockCanvasAndPost?

注:本文分析的代码来自于Android6.0.1。

2. BufferQueue内部运作方式

BufferQueue是Android显示系统的核心,它的设计哲学是生产者-消费者模型,只要往BufferQueue中填充数据,则认为是生产者,只要从BufferQueue中获取数据,则认为是消费者。有时候同一个类,在不同的场景下既可能是生产者也有可能是消费者。如SurfaceFlinger,在合成并显示UI内容时,UI元素作为生产者生产内容,SurfaceFlinger作为消费者消费这些内容。而在截屏时,SurfaceFlinger又作为生产者将当前合成显示的UI内容填充到另一个BufferQueue,截屏应用此时作为消费者从BufferQueue中获取数据并生产截图。

以下是常见的BufferQueue使用步骤:

  1. 初始化一个BufferQueue

  2. 图形数据的生产者通过BufferQueue申请一块GraphicBuffer,对应图中的dequeueBuffer方法

  3. 申请到GraphicBuffer后,获取GraphicBuffer,通过函数requestBuffer获取

  4. 获取到GraphicBuffer后,通过各种形式往GraphicBuffer中填充图形数据后,然后将GraphicBuffer入队到BufferQueue中,对应上图中的queueBuffer方法

  5. 在新的GraphicBuffer入队BufferQueue时,BufferQueue会通过回调通知图形数据的消费者,有新的图形数据被生产出来了

  6. 然后消费者从BufferQueue中出队一个GraphicBuffer,对应图中的acquireBuffer方法

  7. 待消费者消费完图形数据后,将空的GraphicBuffer还给BufferQueue以便重复利用,此时对应上图中的releaseBuffer方法

  8. 此时BufferQueue再通过回调通知图形数据的生产者有空的GraphicBuffer了,图形数据的生产者又可以从BufferQueue中获取一个空的GraphicBuffer来填充数据

  9. 一直循环2-8步骤,这样就有条不紊的完成了图形数据的生产-消费

当然图形数据的生产者可以不用等待BufferQueue的回调再生产数据,而是一直生产数据然后入队到BufferQueue,直到BufferQueue满为止。图形数据的消费者也可以不用等BufferQueue的回调通知,每次都从BufferQueue中尝试获取数据,获取失败则尝试,只是这样效率比较低,需要不断的轮训BufferQueue(因为BufferQueue有同步阻塞和非同步阻塞两种机种,在非同步阻塞机制下获取数据失败不会阻塞该线程直到有数据才唤醒该线程,而是直接返回-1)。

同时使用BufferQueue的生产者和消费者往往处在不同的进程,BufferQueue内部使用共享内存和Binder在不同的进程传递数据,减少数据拷贝提高效率。

和BufferQueue有关的几个类分别是:

  1. BufferBufferCore:BufferQueue的实际实现

  2. BufferSlot:用来存储GraphicBuffer

  3. BufferState:表示GraphicBuffer的状态

  4. IGraphicBufferProducer:BufferQueue的生产者接口,实现类是BufferQueueProducer

  5. IGraphicBufferConsumer:BufferQueue的消费者接口,实现类是BufferQueueConsumer

  6. GraphicBuffer:表示一个Buffer,可以填充图像数据

  7. ANativeWindow_Buffer:GraphicBuffer的父类

  8. ConsumerBase:实现了ConsumerListener接口,在数据入队列时会被调用到,用来通知消费者

BufferQueue中用BufferSlot来存储GraphicBuffer,使用数组来存储一系列BufferSlot,数组默认大小为64。

GraphicBuffer用BufferState来表示其状态,有以下状态:

  1. FREE:表示该Buffer没有被生产者-消费者所使用,该Buffer的所有权属于BufferQueue

  2. DEQUEUED:表示该Buffer被生产者获取了,该Buffer的所有权属于生产者

  3. QUEUED:表示该Buffer被生产者填充了数据,并且入队到BufferQueue了,该Buffer的所有权属于BufferQueue

  4. ACQUIRED:表示该Buffer被消费者获取了,该Buffer的所有权属于消费者

为什么需要这些状态呢? 假设不需要这些状态,实现一个简单的BufferQueue,假设是如下实现:

BufferQueue{
   vector<GraphicBuffer> slots;
   void push(GraphicBuffer slot){
       slots.push(slot);
   }

   GraphicBuffer pull(){
       return slots.pull();
   }
}

生产者生产完数据后,通过调用BufferQueue的push函数将数据插入到vector中。消费者调用BufferQueue的pull函数出队一个Buffer数据。

上述实现的问题在于,生产者每次都需要自行创建GraphicBuffer,而消费者每次消费完数据后的GraphicBuffer就被释放了,GraphicBuffer没有得到循环利用。而在Android中,由于BufferQueue的生产者-消费者往往处于不同的进程,GraphicBuffer内部是需要通过共享内存来连接生成者-消费者进程的,每次创建GraphicBuffer,即意味着需要创建共享内存,效率较低。

而BufferQueue中用BufferState来表示GraphicBuffer的状态则解决了这个问题。每个GraphicBuffer都有当前的状态,通过维护GraphicBuffer的状态,完成GraphicBuffer的复用。

由于BufferQueue内部实现是BufferQueueCore,下文均用BufferQueueCore代替BufferQueue。先介绍下BufferQueueCore内部相应的数据结构,再介绍BufferQueue的状态扭转过程和生产-消费过程。

以下是Buffer的入队/出队操作和BufferState的状态扭转的过程,这里只介绍非同步阻塞模式。

2.1 BufferQueueCore内部数据结构

核心数据结构如下:

BufferQueueDefs::SlotsType mSlots:用数组存放的Slot,数组默认大小为BufferQueueDefs::NUM_BUFFER_SLOTS,具体是64,代表所有的Slot
std::set<int> mFreeSlots:当前所有的状态为FREE的Slot,这些Slot没有关联上具体的GraphicBuffer,后续用的时候还需要关联上GraphicBuffer
std::list<int> mFreeBuffers:当前所有的状态为FREE的Slot,这些Slot已经关联上具体的GraphicBuffer,可以直接使用
Fifo mQueue:一个先进先出队列,保存了生产者生产的数据

在BufferQueueCore初始化时,由于此时队列中没有入队任何数据,按照上面的介绍,此时mFreeSlots应该包含所有的Slot,元素大小和mSlots一致,初始化代码如下:

for (int slot = 0; slot < BufferQueueDefs::NUM_BUFFER_SLOTS; ++slot) {
        mFreeSlots.insert(slot);
    }
2.2 生产者dequeueBuffer

当生产者可以生产图形数据时,首先向BufferQueue中申请一块GraphicBuffer。调用函数BufferQueueProducer.dequeueBuffer,如果当前BufferQueue中有可用的GraphicBuffer,则返回其对用的索引;如果不存在,则返回-1,代码在BufferQueueProducer,流程如下:

status_t BufferQueueProducer::dequeueBuffer(int *outSlot,
        sp<android::Fence> *outFence, bool async,
        uint32_t width, uint32_t height, PixelFormat format, uint32_t usage) {

             //1. 寻找可用的Slot,可用指Buffer状态为FREE
             status_t status = waitForFreeSlotThenRelock("dequeueBuffer", async,
                    &found, &returnFlags);
            if (status != NO_ERROR) {
                return status;
            }
            //2.找到可用的Slot,将Buffer状态设置为DEQUEUED,由于步骤1找到的Slot状态为FREE,因此这一步完成了FREE到DEQUEUED的状态切换
            *outSlot = found;
            ATRACE_BUFFER_INDEX(found);
            attachedByConsumer = mSlots[found].mAttachedByConsumer;
            mSlots[found].mBufferState = BufferSlot::DEQUEUED;
            //3. 找到的Slot如果需要申请GraphicBuffer,则申请GraphicBuffer,这里采用了懒加载机制,如果内存没有申请,申请内存放在生产者来处理
            if (returnFlags & BUFFER_NEEDS_REALLOCATION) {
                status_t error;
                sp<GraphicBuffer> graphicBuffer(mCore->mAllocator->createGraphicBuffer(width, height, format, usage, &error));
                graphicBuffer->setGenerationNumber(mCore->mGenerationNumber);
                mSlots[*outSlot].mGraphicBuffer = graphicBuffer;
            }
}

关键在于寻找可用Slot,waitForFreeSlotThenRelock的流程如下:

status_t BufferQueueProducer::waitForFreeSlotThenRelock(const char* caller,
        bool async, int* found, status_t* returnFlags) const {

    //1. mQueue 是否太多
    bool tooManyBuffers = mCore->mQueue.size()> static_cast<size_t>(maxBufferCount);
        if (tooManyBuffers) {

        } else {
            // 2. 先查找mFreeBuffers中是否有可用的,由2.1介绍可知,mFreeBuffers中的元素关联了GraphicBuffer,直接可用
            if (!mCore->mFreeBuffers.empty()) {
                auto slot = mCore->mFreeBuffers.begin();
                *found = *slot;
                mCore->mFreeBuffers.erase(slot);
            } else if (mCore->mAllowAllocation && !mCore->mFreeSlots.empty()) {
                // 3. 再查找mFreeSlots中是否有可用的,由2.1可知,初始化时会填充满这个列表,因此第一次调用一定不会为空。同时用这个列表中的元素需要关联上GraphicBuffer才可以直接使用,关联的过程由外层函数来实现
                auto slot = mCore->mFreeSlots.begin();
                // Only return free slots up to the max buffer count
                if (*slot < maxBufferCount) {
                    *found = *slot;
                    mCore->mFreeSlots.erase(slot);
                }
            }
        }

         tryAgain = (*found == BufferQueueCore::INVALID_BUFFER_SLOT) ||
                   tooManyBuffers;
        //4. 如果找不到可用的Slot或者Buffer太多(同步阻塞模式下),则可能需要等
        if (tryAgain) {
            if (mCore->mDequeueBufferCannotBlock &&
                    (acquiredCount <= mCore->mMaxAcquiredBufferCount)) {
                return WOULD_BLOCK;
            }
            mCore->mDequeueCondition.wait(mCore->mMutex);
        }
}

waitForFreeSlotThenRelock函数会尝试寻找一个可用的Slot,可用的Slot状态一定是FREE(因为是从两个FREE状态的列表中获取的),然后dequeueBuffer将状态改变为DEQUEUED,即完成了状态的扭转。

waitForFreeSlotThenRelock返回可用的Slot分为两种:

  1. 从mFreeBuffers中获取到的,mFreeBuffers中的元素关联了GraphicBuffer,直接可用

  2. 从mFreeSlots中获取到的,没有关联上GraphicBuffer,因此需要申请GraphicBuffer并和Slot关联上,通过createGraphicBuffer申请一个GraphicBuffer,然后赋值给Slot的mGraphicBuffer完成关联

小结dequeueBuffer:尝试找到一个Slot,并完成Slot与GraphicBuffer的关联(如果需要),然后将Slot的状态由FREE扭转成DEQUEUED,返回Slot在BufferQueueCore中mSlots对应的索引。

2.3 生产者requestBuffer

dequeueBuffer函数获取到了可用Slot的索引后,通过requestBuffer获取到对应的GraphicBuffer。流程如下:

status_t BufferQueueProducer::requestBuffer(int slot, sp<GraphicBuffer>* buf) {

    // 1. 判断slot参数是否合法
    if (slot < 0 || slot >= BufferQueueDefs::NUM_BUFFER_SLOTS) {
        BQ_LOGE("requestBuffer: slot index %d out of range [0, %d)",
                slot, BufferQueueDefs::NUM_BUFFER_SLOTS);
        return BAD_VALUE;
    } else if (mSlots[slot].mBufferState != BufferSlot::DEQUEUED) {
        BQ_LOGE("requestBuffer: slot %d is not owned by the producer "
                "(state = %d)", slot, mSlots[slot].mBufferState);
        return BAD_VALUE;
    }

    //2. 将mRequestBufferCalled置为true
    mSlots[slot].mRequestBufferCalled = true;
    *buf = mSlots[slot].mGraphicBuffer;
    return NO_ERROR;
}

这一步不是必须的,业务层可以直接通过Slot的索引获取到对应的GraphicBuffer。

2.4 生产者queueBuffer

上文dequeueBuffer获取到一个Slot后,就可以在Slot对应的GraphicBuffer上完成图像数据的生产了,可以是View的主线程Draw过程,也可以是SurfaceView的子线程绘制过程,甚至可以是MediaCodec的解码过程。

填充完图像数据后,需要将Slot入队BufferQueueCore(数据写完了,可以传给生产者-消费者队列,让消费者来消费了),入队调用queueBuffer函数。queueBuffer的流程如下:

status_t BufferQueueProducer::queueBuffer(int slot,
        const QueueBufferInput &input, QueueBufferOutput *output) {

        // 1. 先判断传入的Slot是否合法
        if (slot < 0 || slot >= maxBufferCount) {
            BQ_LOGE("queueBuffer: slot index %d out of range [0, %d)",
                    slot, maxBufferCount);
            return BAD_VALUE;
        }

        //2. 将Buffer状态扭转成QUEUED,此步完成了Buffer的状态由DEQUEUED到QUEUED的过程
        mSlots[slot].mFence = fence;
        mSlots[slot].mBufferState = BufferSlot::QUEUED;
        ++mCore->mFrameCounter;
        mSlots[slot].mFrameNumber = mCore->mFrameCounter;

        //3. 入队mQueue
        if (mCore->mQueue.empty()) {
            mCore->mQueue.push_back(item);
            frameAvailableListener = mCore->mConsumerListener;
        } 

        // 4. 回调frameAvailableListener,告知消费者有数据入队了
        if (frameAvailableListener != NULL) {
            frameAvailableListener->onFrameAvailable(item);
        } else if (frameReplacedListener != NULL) {
            frameReplacedListener->onFrameReplaced(item);
        }
}

从上面的注释可以看到,queueBuffer的主要步骤如下:

  1. 将Buffer状态扭转成QUEUED,此步完成了Buffer的状态由DEQUEUED到QUEUED的过程

  2. 将Buffer入队到BufferQueueCore的mQueue队列中

  3. 回调frameAvailableListener,告知消费者有数据入队,可以来消费数据了,frameAvailableListener是消费者注册的回调

小结queueBuffer:将Slot的状态扭转成QUEUED,并添加到mQueue中,最后通知消费者有数据入队。

2.5 消费者acquireBuffer

在消费者接收到onFrameAvailable回调时或者消费者主动想要消费数据,调用acquireBuffer尝试向BufferQueueCore获取一个数据以供消费。消费者的代码在BufferQueueConsumer中,acquireBuffer流程如下:

status_t BufferQueueConsumer::acquireBuffer(BufferItem* outBuffer,
        nsecs_t expectedPresent, uint64_t maxFrameNumber) {

        //1. 如果队列为空,则直接返回
        if (mCore->mQueue.empty()) {
            return NO_BUFFER_AVAILABLE;
        }

        //2. 取出mQueue队列的第一个元素,并从队列中移除
        BufferQueueCore::Fifo::iterator front(mCore->mQueue.begin());
           int slot = front->mSlot;
        *outBuffer = *front;
        mCore->mQueue.erase(front);

        //3. 处理expectedPresent的情况,这种情况可能会连续丢几个Slot的“显示”时间小于expectedPresent的情况,这种情况下这些Slot已经是“过时”的,直接走下文的releaseBuffer消费流程,代码比较长,忽略了
              

        //4. 更新Slot的状态为ACQUIRED
        if (mCore->stillTracking(front)) {
            mSlots[slot].mAcquireCalled = true;
            mSlots[slot].mNeedsCleanupOnRelease = false;
            mSlots[slot].mBufferState = BufferSlot::ACQUIRED;
            mSlots[slot].mFence = Fence::NO_FENCE;
        }

        //5. 如果步骤3有直接releaseBuffer的过程,则回调生产者,有数据被消费了
        if (listener != NULL) {
            for (int i = 0; i < numDroppedBuffers; ++i) {
                listener->onBufferReleased();
            }
        }

}

从上面的注释可以看到,acquireBuffer的主要步骤如下:

  1. 从mQueue队列中取出并移除一个元素

  2. 改变Slot对应的状态为ACQUIRED

  3. 如果有丢帧逻辑,回调告知生产者有数据被消费,生产者可以准备生产数据了

小结acquireBuffer:将Slot的状态扭转成ACQUIRED,并从mQueue中移除,最后通知生产者有数据出队。

2.6 消费者releaseBuffer

消费者获取到Slot后开始消费数据(典型的消费如SurfaceFlinger的UI合成),消费完毕后,需要告知BufferQueueCore这个Slot被消费者消费完毕了,可以给生产者重新生产数据,releaseBuffer流程如下:

status_t BufferQueueConsumer::releaseBuffer(int slot, uint64_t frameNumber,
        const sp<Fence>& releaseFence, EGLDisplay eglDisplay,EGLSyncKHR eglFence) {

         //1. 检查Slot是否合法
        if (slot < 0 || slot >= BufferQueueDefs::NUM_BUFFER_SLOTS ||         
            return BAD_VALUE;
        }

        //2. 容错处理:如果要处理的Slot存在于mQueue中,那么说明这个Slot的来源不合法,并不是从2.5的acquireBuffer获取的Slot,拒绝处理
        BufferQueueCore::Fifo::iterator current(mCore->mQueue.begin());
        while (current != mCore->mQueue.end()) {
            if (current->mSlot == slot) {
                return BAD_VALUE;
            }
            ++current;
        } 

         // 3. 将Slot的状态扭转为FREE,之前是ACQUIRED,并将该Slot添加到BufferQueueCore的mFreeBuffers列表中(mFreeBuffers的定义参考2.1的介绍)
         if (mSlots[slot].mBufferState == BufferSlot::ACQUIRED) {
                mSlots[slot].mEglDisplay = eglDisplay;
                mSlots[slot].mEglFence = eglFence;
                mSlots[slot].mFence = releaseFence;
                mSlots[slot].mBufferState = BufferSlot::FREE;
                mCore->mFreeBuffers.push_back(slot);
                listener = mCore->mConnectedProducerListener;
                BQ_LOGV("releaseBuffer: releasing slot %d", slot);
            }

           // 4. 回调生产者,有数据被消费了
           if (listener != NULL) {
               listener->onBufferReleased();
           }
}

从上面的注释可以看到,releaseBuffer的主要步骤如下:

  1. 将Slot的状态扭转为FREE

  2. 将被消费的Slot添加到mFreeBuffers供后续的生产者dequeueBuffer使用

  3. 回调告知生产者有数据被消费,生产者可以准备生产数据了

小结releaseBuffer:将Slot的状态扭转成FREE,并添加到BufferQueueCore mFreeBuffers队列中,最后通知生产者有数据出队。

总结下状态变化的过程:

BufferQueue的设计思想和内部实现方法是什么

关于“BufferQueue的设计思想和内部实现方法是什么”的内容就介绍到这里了,感谢大家的阅读。如果想了解更多行业相关的知识,可以关注亿速云行业资讯频道,小编每天都会为大家更新不同的知识点。

向AI问一下细节

免责声明:本站发布的内容(图片、视频和文字)以原创、转载和分享为主,文章观点不代表本网站立场,如果涉及侵权请联系站长邮箱:is@yisu.com进行举报,并提供相关证据,一经查实,将立刻删除涉嫌侵权内容。

AI