图像显示框架十——BufferQueue的工作流程(基于Android 15源码分析)

1.背景

前面介绍了如何去创建一个Surface,也讲解了一些操作Surface的知识,接下来我们来看如何利用这个Surface来进行绘图

在此开始讲解buffer queue的工作流程,看看图形数据是怎么流转的?图形的缓存区申请和消费流程是怎么样的?有哪些核心类?等等问题在接下来的文章中陆续进行讲解

这篇文章中,先介绍一些基本概念的东西,帮助后续内容打下基础

  • 生产者和消费者模型
  • 关于图形缓冲区队列的核心类
  • BufferState介绍
  • BufferSlot介绍
  • 一些buffer数组的介绍

2.生产者与消费者的模型

在Android 15上面生产者与消费者模型流程图如下,不一定准确,这个后续再深入学习修改,主要是与SurfaceFlinger进行事务通信

3.关于图形缓冲区队列的核心类

如下是先给出一个涉及到的相关类的关系图,不一定正确或者完整,很多细节也没有呈现出来,只是大概描述各元素间的关系,便于我们看到全貌。

Android图形系统BufferQueue类关系结构

=========================================

一级结构:核心接口层

├── IGraphicBufferProducer (生产者接口)

│ ├── 实现类: BufferQueueProducer

│ ├── 关键方法: dequeueBuffer(), queueBuffer(), cancelBuffer()

│ └── 关联: ← 被Surface调用

├── IGraphicBufferConsumer (消费者接口)

│ ├── 实现类: BufferQueueConsumer

│ ├── 关键方法: acquireBuffer(), releaseBuffer()

│ └── 关联: → 被SurfaceFlinger调用

└── ConsumerListener (消费者监听器接口)

└── 实现类: BLASTBufferItemConsumer

二级结构:BufferQueue核心层

├── BufferQueueCore (缓冲区队列核心)

│ ├── 成员:

│ │ ├── mSlots: BufferSlot[64] (缓冲区槽位数组)

│ │ ├── mFreeSlots: Vector<int> (空闲槽位)

│ │ ├── mFreeBuffers: List<int> (空闲缓冲区)

│ │ ├── mQueue: Fifo<BufferItem> (缓冲区队列)

│ │ └── mActiveBuffers: Set<int> (活动缓冲区)

│ ├── 关联: ← 被BufferQueueProducer持有

│ └── 关联: ← 被BufferQueueConsumer持有

├── BufferQueueProducer (生产者实现)

│ ├── 成员:

│ │ ├── mCore: sp<BufferQueueCore> (核心引用)

│ │ ├── mConnectedApi: int (连接API类型)

│ │ └── mStickyTransform: uint32_t (粘性变换)

│ ├── 实现: IGraphicBufferProducer接口

│ └── 关联: → 操作BufferQueueCore

└── BufferQueueConsumer (消费者实现)

├── 成员:

│ ├── mCore: sp<BufferQueueCore> (核心引用)

│ ├── mConsumerListener: wp<ConsumerListener>

│ └── mFrameAvailableListener: sp<FrameAvailableListener>

├── 实现: IGraphicBufferConsumer接口

└── 关联: → 操作BufferQueueCore

三级结构:BLAST架构层

├── BLASTBufferQueue (BLAST缓冲区队列)

│ ├── 成员:

│ │ ├── mProducer: sp<IGraphicBufferProducer>

│ │ ├── mConsumer: sp<IGraphicBufferConsumer>

│ │ ├── mBufferItemConsumer: sp<BLASTBufferItemConsumer>

│ │ └── mSurfaceControl: sp<SurfaceControl>

│ ├── 关联: → 创建BufferQueueProducer

│ ├── 关联: → 创建BufferQueueConsumer

│ └── 关联: → 创建BBQSurface

└── BLASTBufferItemConsumer (BLAST缓冲区项消费者)

├── 成员:

│ ├── mConsumer: sp<IGraphicBufferConsumer>

│ └── mBLASTBufferQueue: wp<BLASTBufferQueue>

├── 实现: ConsumerListener接口

└── 功能: 监听帧可用事件,打包Transaction

四级结构:Surface应用层

├── Surface (表面)

│ ├── 成员:

│ │ ├── mGraphicBufferProducer: sp<IGraphicBufferProducer>

│ │ ├── mSurfaceControlHandle: sp<IBinder>

│ │ └── mGenerationNumber: int

│ ├── 功能: ANativeWindow的实现

│ └── 关联: → 调用IGraphicBufferProducer

└── BBQSurface (BBQ表面)

├── 继承: 自Surface

└── 成员: mBbq: sp<BLASTBufferQueue>

五级结构:缓冲区对象

├── ANativeWindowBuffer (原生窗口缓冲区)

│ ├── 成员: width, height, stride, format, handle, usage

│ └── 功能: 缓冲区描述结构体

├── GraphicBuffer (图形缓冲区)

│ ├── 继承: 自ANativeWindowBuffer

│ └── 功能: 实际的内存分配和管理

└── BufferItem (缓冲区项)

├── 成员: mGraphicBuffer, mSlot, mFrameNumber, mFence

└── 功能: 在队列中传递的缓冲区信息

继承关系汇总:

────────────────────────────────────

  1. Surface 继承自 ANativeWindow

  2. BBQSurface 继承自 Surface

  3. GraphicBuffer 继承自 ANativeWindowBuffer

  4. BufferQueueProducer 实现 IGraphicBufferProducer

  5. BufferQueueConsumer 实现 IGraphicBufferConsumer

  6. BLASTBufferItemConsumer 实现 ConsumerListener

关键组合关系:

────────────────────────────────────

  1. BufferQueueProducer 1对1 持有 BufferQueueCore

  2. BufferQueueConsumer 1对1 持有 BufferQueueCore

  3. BufferQueueCore 1对64 管理 BufferSlot数组

  4. BLASTBufferQueue 1对1 创建 BufferQueueProducer

  5. BLASTBufferQueue 1对1 创建 BufferQueueConsumer

  6. BLASTBufferQueue 1对1 创建 BLASTBufferItemConsumer

  7. Surface 1对1 持有 IGraphicBufferProducer接口

工作流程方向:

────────────────────────────────────

应用 → Surface → IGraphicBufferProducer → BufferQueueProducer → BufferQueueCore

SurfaceFlinger ← IGraphicBufferConsumer ← BufferQueueConsumer ← BufferQueueCore

4.BufferSlot介绍

源码

/frameworks/native/libs/gui/include/gui/BufferSlot.h

定义

BufferSlot理解为缓冲槽,一个存放buffer及其信息的地方。这个结构体中主要有如下内容:

我们主要看一下几个成员变量:

①.图形资源相关

mGraphicBuffer 指向为此槽位分配的实际图形缓冲区。这是缓存区数据的核心载体,存储实际的像素信息。

mEglDisplay用于创建EGL同步对象的显示连接。在跨EGL/OpenGL ES环境同步时使用

②.状态管理与同步

mBufferState 缓冲区的状态机核心,标记当前buffer slot所处的状态

mFence同步栅栏,用于生产者与消费者之间的同步

  • FREE 状态:标识消费者何时完成读取(或生产者取消后的写入完成)

  • QUEUED 状态:标识生产者何时完成填充

  • DEQUEUED/ACQUIRED 状态 :栅栏已传递给对方,设为 NO_FENCE

mEglFence:EGL 同步对象,功能类似 mFence,但用于 EGL 环境(已弃用,优先使用 mFence

mFrameNumber:此槽位排队帧的编号。用于 LRU(最近最少使用)算法对缓冲区进行出列排序,这在缓冲区可能在释放栅栏信号发出前就被释放时特别有用

③.生命周期与调试控制

mRequestBufferCalled:验证生产者是否在被告知时调用了 requestBuffer()。主要用于调试和捕获生产者错误

mAcquireCalled:指示消费者是否已看到此缓冲区。用于跟踪缓冲区的消费状态

mNeedsReallocation关键标志! 指示缓存区是否在未通知生产者的情况下被重新分配。如果为true,在出列时需要设置BUFFER_NEEDS_REALLOCATION标志,防止生产者使用过时的缓存缓冲区

然后我们再看下BufferSlot的构造函数

复制代码
    BufferSlot()
    : mGraphicBuffer(nullptr),
      mEglDisplay(EGL_NO_DISPLAY),
      mBufferState(),
      mRequestBufferCalled(false),
      mFrameNumber(0),
      mEglFence(EGL_NO_SYNC_KHR),
      mFence(Fence::NO_FENCE),
      mAcquireCalled(false),
      mNeedsReallocation(false) {
    }

可以看出来这个成员变量的默认值,其中mGraphicBuffer默认是nullptr,既没有绑定GraphicBuffer,也就没有分配实际的图形缓存

5.BufferState介绍

源码:/frameworks/native/libs/gui/include/gui/BufferSlot.h

如下是BufferState的源代码

复制代码
// BufferState 追踪缓冲区槽位可能处于的状态。
struct BufferState {

    // 构造函数:所有槽位最初都是 FREE 状态(空闲)
    BufferState()
    : mDequeueCount(0), // 出队计数
      mQueueCount(0),   // 入队计数
      mAcquireCount(0), // 获取计数
      mShared(false) {  // 是否为共享缓冲区模式
    }

    uint32_t mDequeueCount;
    uint32_t mQueueCount;
    uint32_t mAcquireCount;
    bool mShared;

    /**
     * 缓冲区可能处于以下五种状态之一,对应关系如下表:
     *
     *         | mShared | mDequeueCount | mQueueCount | mAcquireCount |
     * --------|---------|---------------|-------------|---------------|
     * FREE    |  false  |       0       |      0      |       0       |
     * DEQUEUED|  false  |       1       |      0      |       0       |
     * QUEUED  |  false  |       0       |      1      |       0       |
     * ACQUIRED|  false  |       0       |      0      |       1       |
     * SHARED  |  true   |      any      |     any     |      any      |
     *
     * 1. FREE (空闲):
     *    表示缓冲区可被生产者出队(dequeue)。
     *    所有权:归 BufferQueue 所有。
     *    转换:调用 dequeueBuffer 后转为 DEQUEUED。
     *
     * 2. DEQUEUED (已出队):
     *    表示生产者已申请该缓冲区,但尚未提交(queue)或取消(cancel)。
     *    一旦相关的 release fence 触发,生产者就可以修改缓冲区内容。
     *    所有权:归生产者(Producer/App)所有。
     *    转换:通过 queueBuffer 转为 QUEUED,或通过 cancelBuffer 回到 FREE。
     *
     * 3. QUEUED (已入队):
     *    表示生产者已填充数据并提交给消费者使用。
     *    在 fence 触发前,内容可能仍在写入,此时不可访问。
     *    所有权:归 BufferQueue 所有。
     *    转换:通过 acquireBuffer 转为 ACQUIRED,或者在异步模式下由于新帧进入而回到 FREE。
     *
     * 4. ACQUIRED (已获取):
     *    表示消费者已获取该缓冲区进行渲染或合成。
     *    所有权:归消费者(Consumer/SurfaceFlinger)所有。
     *    转换:通过 releaseBuffer 释放回 FREE 状态。
     *
     * 5. SHARED (共享):
     *    用于低延迟模式(如 VR)。缓冲区可以同时处于多种状态,可以多次出队/入队/获取。
     */

    // 判断是否空闲:没有任何计数,即不属于生产者、BufferQueue或消费者
    inline bool isFree() const {
        return !isAcquired() && !isDequeued() && !isQueued();
    }

    // 是否被生产者占用(出队)
    inline bool isDequeued() const {
        return mDequeueCount > 0;
    }

    // 是否在队列中等待消费者(入队)
    inline bool isQueued() const {
        return mQueueCount > 0;
    }

    // 是否正在被消费者处理(获取)
    inline bool isAcquired() const {
        return mAcquireCount > 0;
    }

    // 是否是共享模式
    inline bool isShared() const {
        return mShared;
    }

    // 重置状态
    inline void reset() {
        *this = BufferState();
    }

    const char* string() const;

    // --- 状态转换操作 ---

    // 生产者执行 dequeue:增加出队计数
    inline void dequeue() {
        mDequeueCount++;
    }

    // 生产者分离(detach):减少出队计数(不再受此生产者管理)
    inline void detachProducer() {
        if (mDequeueCount > 0) {
            mDequeueCount--;
        }
    }

    // 生产者关联(attach):增加出队计数
    inline void attachProducer() {
        mDequeueCount++;
    }

    // 生产者执行 queue:将缓冲区从生产者移交给 BufferQueue
    inline void queue() {
        if (mDequeueCount > 0) {
            mDequeueCount--;
        }
        mQueueCount++;
    }

    // 生产者取消出队:归还给 BufferQueue,不提交数据
    inline void cancel() {
        if (mDequeueCount > 0) {
            mDequeueCount--;
        }
    }

    // 释放队列中的缓冲区(通常用于丢弃旧帧)
    inline void freeQueued() {
        if (mQueueCount > 0) {
            mQueueCount--;
        }
    }

    // 消费者执行 acquire:将缓冲区从 BufferQueue 移交给消费者
    inline void acquire() {
        if (mQueueCount > 0) {
            mQueueCount--;
        }
        mAcquireCount++;
    }

    // 强行获取不在队列中的缓冲区
    inline void acquireNotInQueue() {
        mAcquireCount++;
    }

    // 消费者执行 release:消费者用完后释放,回到 FREE 状态
    inline void release() {
        if (mAcquireCount > 0) {
            mAcquireCount--;
        }
    }

    // 消费者分离
    inline void detachConsumer() {
        if (mAcquireCount > 0) {
            mAcquireCount--;
        }
    }

    // 消费者关联
    inline void attachConsumer() {
        mAcquireCount++;
    }
};

定义

BufferState用于跟踪记录一个buffer slot(缓冲槽)所处的状态。如下这个类图描述了BufferState中定义的基本内容:

  • 用于描述缓冲区的3个uint32_t变量(mDequeueCount、mQueueCount、mAcquireCount)和一个bool变量(mShared);
  • 用于查询缓冲区状态的函数,isXXX();
  • 用于改变/设置缓冲区的函数,比如dequeue() 和 queue()

状态

BufferState用于跟踪记录一个buffer slot(缓冲槽)所处的状态。一个buffer可以处于以下5种状态之一。

如下是状态转换的流程图:

典型的工作流程如下

6.几个队列/数组大概解释

在图形缓冲区队列的逻辑中,有几处队列、数组,我们大概看一看他们代表了什么意思。

定义了一个 BufferQueue 所能管理的最大缓冲区槽位数量

/frameworks/native/libs/ui/include/ui/BufferQueueDefs.h

复制代码
        // BufferQueue will keep track of at most this value of buffers.
        // Attempts at runtime to increase the number of buffers past this
        // will fail.
        static constexpr int NUM_BUFFER_SLOTS = 64;

定义一个名为 SlotsType 的新类型,它是一个数组,这个数组最多能容纳 NUM_BUFFER_SLOTSBufferSlot对象。

/frameworks/native/libs/gui/include/gui/BufferQueueDefs.h

复制代码
    namespace BufferQueueDefs {
        typedef BufferSlot SlotsType[NUM_BUFFER_SLOTS];
    } // namespace BufferQueueDefs

BufferQueueCore中的buffer slot数组

/frameworks/native/libs/gui/include/gui/BufferQueueCore.h

复制代码
// mSlots 是一个缓冲区槽位数组,在生产者和消费者端均有镜像。
// 这种设计使得缓冲区所有权可以在生产者和消费者之间转移,而无需通过 Binder 传递庞大的 GraphicBuffer 对象,极大地提升了效率。
// 整个数组在构造时被初始化为 NULL。当调用 requestBuffer 并传入特定槽位索引时,才会为该槽位分配实际的缓冲区。
BufferQueueDefs::SlotsType mSlots; // 核心槽位数组,大小为 NUM_BUFFER_SLOTS (通常为64)

// mQueue 是一个 FIFO(先进先出)队列,用于存放已在同步模式下排队的缓冲区(状态为 QUEUED)。
// 消费者将按此队列的顺序获取缓冲区进行处理。
Fifo mQueue;

// mFreeSlots 是一个集合,包含所有当前状态为 FREE 且未关联具体 GraphicBuffer 的槽位索引。
// 这些是"空"的槽位,需要分配缓冲区后才能使用。
std::set<int> mFreeSlots;

// mFreeBuffers 是一个链表,包含所有当前状态为 FREE 但已关联有 GraphicBuffer 的槽位索引。
// 这些是立即可用的缓冲区,生产者可以直接申请使用,无需重新分配内存。
std::list<int> mFreeBuffers;

// mUnusedSlots 是一个链表,包含所有当前完全未被使用的槽位索引。
// 它们应该是 FREE 状态且没有关联缓冲区。用于管理超出当前最大缓冲区数量(maxBufferCount)的闲置槽位。
std::list<int> mUnusedSlots;

// mActiveBuffers 是一个集合,包含所有关联了缓冲区且当前状态非 FREE(即 DEQUEUED, QUEUED, ACQUIRED)的槽位索引。
// 用于快速追踪正在被生产或消费的活跃缓冲区。
std::set<int> mActiveBuffers;
  • mSlots:核心槽位数组,大小为NUM_BUFFER_SLOTS (通常是64位,如上),这个数组会被映射到BufferQueueProducer/BufferQueueConsumer类中;
  • mQueue:BufferItem类型的数组,Producer调用queueBuffer后,其实就是queue到这个数组里面,然后消费者将按此队列的顺序获取缓冲区进行处理。
  • mFreeSlots:包含所有当前状态为 FREE 且未关联具体 GraphicBuffer 的槽位(BufferSlot)索引
  • mFreeBuffers:包含所有当前状态为 FREE 但已关联有 GraphicBuffer 的槽位(BufferSlot)索引
  • mUnusedSlots:包含所有当前完全未被使用的槽位(BufferSlot)索引
  • mActiveBuffers:包含所有关联了缓冲区(GraphicBuffer)且当前状态非 FREE(即 DEQUEUED, QUEUED, ACQUIRED)的槽位(BufferSlot)索引

下面的表格清晰地展示了每个容器的职责及其在缓冲区生命周期中所管理的状态。

变量名 类型 管理槽位特征(状态 + 缓冲区) 核心作用
**mSlots**​ BufferSlot[N] 所有槽位的完整集合 核心存储 :物理上存储所有槽位(BufferSlot)及其关联的 GraphicBuffer
**mFreeSlots**​ std::set<int> FREE ​ 状态 + 未关联GraphicBuffer 空闲槽位池:存放可被分配新缓冲区的"空"槽位索引。
**mFreeBuffers**​ std::list<int> FREE ​ 状态 + 已关联GraphicBuffer 立即可用缓冲区池:存放已分配内存、可立即用于绘制的缓冲区索引,实现缓冲区复用 。
**mActiveBuffers**​ std::set<int> 状态为 DEQUEUED, QUEUED, ACQUIRED 活跃缓冲区追踪器 :快速定位所有正在使用中的缓冲区,用于状态查询和限制检查(如 mMaxDequeuedBufferCount)。
**mQueue**​ Fifo QUEUED​ 状态 生产完毕缓冲区队列:维护生产者已提交、等待消费者获取的缓冲区顺序(FIFO)。
**mUnusedSlots**​ std::list<int> FREE ​ 状态 + 未关联GraphicBuffer(且索引通常 ≥ maxBufferCount 闲置资源池 :管理超出当前所需数量的槽位,便于动态调整 maxBufferCount

一般工作流程如下:

这里可能感觉mFreeSlots与mUnusedSlots描述差不多,这里概括下他们直接的区别,mFreeSlots是 "当前可用的空闲槽位 ",而 mUnusedSlots是 "系统预留的、尚未激活的槽位"。

BufferQueueProducer中的buffer slot 数组

源码路径:

framework/native/libs/gui/BufferQueueProducer.cpp

复制代码
// BufferQueueProducer 构造函数
// 作用:初始化生产者对象,建立与 BufferQueueCore 核心组件的关联
BufferQueueProducer::BufferQueueProducer(
        const sp<BufferQueueCore>& core,        // 传入 BufferQueue 核心管理组件
        bool consumerIsSurfaceFlinger) :         // 标识消费者是否为 SurfaceFlinger
    mCore(core),                                 // 保存 BufferQueueCore 的强引用
    mSlots(core->mSlots),                        // 直接引用核心的缓冲区槽位数组(64个BufferSlot)
    mConsumerName(),                            // 初始化消费者名称为空
    mStickyTransform(0),                         // 初始化粘性变换为0(无变换)
    mConsumerIsSurfaceFlinger(consumerIsSurfaceFlinger), // 记录消费者身份标志
    mLastQueueBufferFence(Fence::NO_FENCE),      // 初始化上一次队列操作的栅栏为空
    mLastQueuedTransform(0),                     // 初始化上一次队列的变换参数
    mCallbackMutex(),                            // 初始化回调函数的互斥锁
    mNextCallbackTicket(0),                      // 初始化下一个回调票据ID
    mCurrentCallbackTicket(0),                   // 初始化当前回调票据ID
    mCallbackCondition(),                        // 初始化回调条件变量
    mDequeueTimeout(-1),                         // 初始化出列超时时间(-1表示无限等待)
    mDequeueWaitingForAllocation(false) {       // 初始化分配等待标志为false

/* QTI_BEGIN */
// 以下为高通平台特定扩展代码
#ifdef QTI_DISPLAY_EXTENSION
    if (!mQtiBQPExtn) {
        // 创建高通专用的 BufferQueueProducer 扩展实例
        mQtiBQPExtn = new libguiextension::QtiBufferQueueProducerExtension(this);
    }
#endif
/* QTI_END */
}

framework/native/include/gui/BufferQueueProducer.h

复制代码
    // This references mCore->mSlots. Lock mCore->mMutex while accessing.
    BufferQueueDefs::SlotsType& mSlots;

BLASTBufferQueue::createBufferQueue中,实例化一个BufferQueueProducer对象,其构造函数在初始化成员变量时,在会直接将前面创建好的 BufferQueueCore 和 mSlots 赋值到 的成员 变量mSlots中。

BufferQueueProducer::mSlots 是 BufferQueueCore::mSlots的映射/引用,其实就是一个东西!

BufferQueueConsumer中的buffer slot数组

framework/native/libs/gui/BufferQueueConsumer.cpp

复制代码
BufferQueueConsumer::BufferQueueConsumer(const sp<BufferQueueCore>& core) :
    mCore(core),
    mSlots(core->mSlots),
    mConsumerName() {}

framework/native/libs/gui/include/gui/BufferQueueConsumer.h

复制代码
    // This references mCore->mSlots. Lock mCore->mMutex while accessing.
    BufferQueueDefs::SlotsType& mSlots;

BLASTBufferQueue::createBufferQueue中,实例化一个BufferQueueConsumer对象,其构造函数在初始化成员变量时,在会直接将前面创建好的 BufferQueueCore 和 mSlots 赋值到 的成 员变量mSlots中。

BufferQueueConsumer::mSlots 是 BufferQueueCore::mSlots的映射/引用,其实就是一个东西!

framework/native/include/gui/BufferQueueCore.h

复制代码
class BufferQueueCore : public virtual RefBase {
    friend class BufferQueueProducer; // 声明BufferQueueProducer为友元类
    friend class BufferQueueConsumer; // 声明BufferQueueConsumer为友元类
    // ... 私有成员变量,如mSlots, mQueue, mFreeSlots等 ...
};

这意味着BufferQueueProducerBufferQueueConsumer的成员函数可以直接访问和操作BufferQueueCore的所有私有成员 ,而无需通过额外的getter/setter方法

7.总结

这篇文章主要是讲了一些零碎的概念,这些小的知识点理解后,对于后续理解 生产者 - 缓冲区队列 - 消费者 运行的逻辑十分有帮助。

下一篇中将会讲解buffer queue的运作流程&buffer是怎样在其中流转的。

相关推荐
TheNextByte12 小时前
如何将音乐从Android手机传输到电脑 [4 种方法]
android·智能手机·电脑
一起养小猫2 小时前
Flutter for OpenHarmony 实战:贪吃蛇蛇的移动逻辑详解
android·flutter
灵感菇_3 小时前
全面解析 Retrofit 网络框架
android·kotlin·网络请求·retrofit
李慕婉学姐3 小时前
【开题答辩过程】以《基于uniapp的养宠互助服务程序设计与实现》为例,不知道这个选题怎么做的,不知道这个选题怎么开题答辩的可以进来看看
android·mysql·uni-app
移幻漂流3 小时前
JNI的本质解析:Android Framework视角下的Java-Native衔接机制
android·java·开发语言
浪客川3 小时前
1972 GODOT 入门案例
android·java·godot
粤M温同学4 小时前
Android Studio 有多个module,快速修改包名
android·android studio
学海无涯书山有路4 小时前
Android LiveData + MVVM 新手入门教程(基于 XML+Java)
android·xml·java
晚霞的不甘4 小时前
Flutter for OpenHarmony:注入灵魂:购物车的数据驱动与状态管理实战
android·前端·javascript·flutter·前端框架