[Android] Graphic Buffer 的申请

前言:

MediaCodec 支持 texture mode,即MediaCodec解码video完毕后把 yuv 数据填入 GPU 共享出来的 graphic buffer 里面,app 会把 video 的 yuv数据 和 ui 的数据通过通过软件渲染组件(opengl等)发送给GPU 进行一并渲染。这样做的效率较低,但是稳定性较好。且性能取决于GPU的性能。

另外一种是 surface mode,就是常用的模式,这种模式下 app 不再获取 yuv 数据,而是只负责输送 es 数据并决定 render 还是 discard 解码后的数据,且是根据 bufferid 来判断的,根本没有机会接触解码后的 yuv 数据,所有的一切都在平台完成。这种办法效率搞且功耗也很低,因为不需要GPU的参与。大部分情况下都是这种模式。

本文简单理清一下 Graphic Buffer 的框架,以对 texture mode 中的 Graphic Buffer 申请流程有一个大致的概念。




Graphic Buffer 的申请

始于 ANativeWindow 类

frameworks/native/libs/nativewindow/include/system/window.h


struct ANativeWindow
{
#ifdef __cplusplusANativeWindow(): flags(0), minSwapInterval(0), maxSwapInterval(0), xdpi(0), ydpi(0){common.magic = ANDROID_NATIVE_WINDOW_MAGIC;common.version = sizeof(ANativeWindow);memset(common.reserved, 0, sizeof(common.reserved));}/* Implement the methods that sp<ANativeWindow> expects so that itcan be used to automatically refcount ANativeWindow's. */void incStrong(const void* /*id*/) const {common.incRef(const_cast<android_native_base_t*>(&common));}void decStrong(const void* /*id*/) const {common.decRef(const_cast<android_native_base_t*>(&common));}
#endifstruct android_native_base_t common;/* flags describing some attributes of this surface or its updater */const uint32_t flags;/* min swap interval supported by this updated */const int   minSwapInterval;/* max swap interval supported by this updated */const int   maxSwapInterval;/* horizontal and vertical resolution in DPI */const float xdpi;const float ydpi;/* Some storage reserved for the OEM's driver. */intptr_t    oem[4];/** Set the swap interval for this surface.** Returns 0 on success or -errno on error.*/int     (*setSwapInterval)(struct ANativeWindow* window,int interval);/** Hook called by EGL to acquire a buffer. After this call, the buffer* is not locked, so its content cannot be modified. This call may block if* no buffers are available.** The window holds a reference to the buffer between dequeueBuffer and* either queueBuffer or cancelBuffer, so clients only need their own* reference if they might use the buffer after queueing or canceling it.* Holding a reference to a buffer after queueing or canceling it is only* allowed if a specific buffer count has been set.** Returns 0 on success or -errno on error.** XXX: This function is deprecated.  It will continue to work for some* time for binary compatibility, but the new dequeueBuffer function that* outputs a fence file descriptor should be used in its place.*/int     (*dequeueBuffer_DEPRECATED)(struct ANativeWindow* window,struct ANativeWindowBuffer** buffer);/** hook called by EGL to lock a buffer. This MUST be called before modifying* the content of a buffer. The buffer must have been acquired with* dequeueBuffer first.** Returns 0 on success or -errno on error.** XXX: This function is deprecated.  It will continue to work for some* time for binary compatibility, but it is essentially a no-op, and calls* to it should be removed.*/int     (*lockBuffer_DEPRECATED)(struct ANativeWindow* window,struct ANativeWindowBuffer* buffer);/** Hook called by EGL when modifications to the render buffer are done.* This unlocks and post the buffer.** The window holds a reference to the buffer between dequeueBuffer and* either queueBuffer or cancelBuffer, so clients only need their own* reference if they might use the buffer after queueing or canceling it.* Holding a reference to a buffer after queueing or canceling it is only* allowed if a specific buffer count has been set.** Buffers MUST be queued in the same order than they were dequeued.** Returns 0 on success or -errno on error.** XXX: This function is deprecated.  It will continue to work for some* time for binary compatibility, but the new queueBuffer function that* takes a fence file descriptor should be used in its place (pass a value* of -1 for the fence file descriptor if there is no valid one to pass).*/int     (*queueBuffer_DEPRECATED)(struct ANativeWindow* window,struct ANativeWindowBuffer* buffer);/** hook used to retrieve information about the native window.** Returns 0 on success or -errno on error.*/int     (*query)(const struct ANativeWindow* window,int what, int* value);/** hook used to perform various operations on the surface.* (*perform)() is a generic mechanism to add functionality to* ANativeWindow while keeping backward binary compatibility.** DO NOT CALL THIS HOOK DIRECTLY.  Instead, use the helper functions* defined below.** (*perform)() returns -ENOENT if the 'what' parameter is not supported* by the surface's implementation.** See above for a list of valid operations, such as* NATIVE_WINDOW_SET_USAGE or NATIVE_WINDOW_CONNECT*/int     (*perform)(struct ANativeWindow* window,int operation, ... );/** Hook used to cancel a buffer that has been dequeued.* No synchronization is performed between dequeue() and cancel(), so* either external synchronization is needed, or these functions must be* called from the same thread.** The window holds a reference to the buffer between dequeueBuffer and* either queueBuffer or cancelBuffer, so clients only need their own* reference if they might use the buffer after queueing or canceling it.* Holding a reference to a buffer after queueing or canceling it is only* allowed if a specific buffer count has been set.** XXX: This function is deprecated.  It will continue to work for some* time for binary compatibility, but the new cancelBuffer function that* takes a fence file descriptor should be used in its place (pass a value* of -1 for the fence file descriptor if there is no valid one to pass).*/int     (*cancelBuffer_DEPRECATED)(struct ANativeWindow* window,struct ANativeWindowBuffer* buffer);/** Hook called by EGL to acquire a buffer. This call may block if no* buffers are available.** The window holds a reference to the buffer between dequeueBuffer and* either queueBuffer or cancelBuffer, so clients only need their own* reference if they might use the buffer after queueing or canceling it.* Holding a reference to a buffer after queueing or canceling it is only* allowed if a specific buffer count has been set.** The libsync fence file descriptor returned in the int pointed to by the* fenceFd argument will refer to the fence that must signal before the* dequeued buffer may be written to.  A value of -1 indicates that the* caller may access the buffer immediately without waiting on a fence.  If* a valid file descriptor is returned (i.e. any value except -1) then the* caller is responsible for closing the file descriptor.** Returns 0 on success or -errno on error.*/int     (*dequeueBuffer)(struct ANativeWindow* window,struct ANativeWindowBuffer** buffer, int* fenceFd);/** Hook called by EGL when modifications to the render buffer are done.* This unlocks and post the buffer.** The window holds a reference to the buffer between dequeueBuffer and* either queueBuffer or cancelBuffer, so clients only need their own* reference if they might use the buffer after queueing or canceling it.* Holding a reference to a buffer after queueing or canceling it is only* allowed if a specific buffer count has been set.** The fenceFd argument specifies a libsync fence file descriptor for a* fence that must signal before the buffer can be accessed.  If the buffer* can be accessed immediately then a value of -1 should be used.  The* caller must not use the file descriptor after it is passed to* queueBuffer, and the ANativeWindow implementation is responsible for* closing it.** Returns 0 on success or -errno on error.*/int     (*queueBuffer)(struct ANativeWindow* window,struct ANativeWindowBuffer* buffer, int fenceFd);/** Hook used to cancel a buffer that has been dequeued.* No synchronization is performed between dequeue() and cancel(), so* either external synchronization is needed, or these functions must be* called from the same thread.** The window holds a reference to the buffer between dequeueBuffer and* either queueBuffer or cancelBuffer, so clients only need their own* reference if they might use the buffer after queueing or canceling it.* Holding a reference to a buffer after queueing or canceling it is only* allowed if a specific buffer count has been set.** The fenceFd argument specifies a libsync fence file decsriptor for a* fence that must signal before the buffer can be accessed.  If the buffer* can be accessed immediately then a value of -1 should be used.** Note that if the client has not waited on the fence that was returned* from dequeueBuffer, that same fence should be passed to cancelBuffer to* ensure that future uses of the buffer are preceded by a wait on that* fence.  The caller must not use the file descriptor after it is passed* to cancelBuffer, and the ANativeWindow implementation is responsible for* closing it.** Returns 0 on success or -errno on error.*/int     (*cancelBuffer)(struct ANativeWindow* window,struct ANativeWindowBuffer* buffer, int fenceFd);
};

上述代码中有很多函数指针,比如

    int     (*dequeueBuffer)(struct ANativeWindow* window,struct ANativeWindowBuffer** buffer, int* fenceFd);

这些指针的具体实现在其子类中有实现和设置,这里是一种 C 的 hook 写法,而不是 c++ 的虚函数写法,可能出于兼容性考虑。

\frameworks\native\libs\nativewindow\ANativeWindow.cpp 里面并不是 ANativeWindow的实现,而是对其封装,比如:

int ANativeWindow_dequeueBuffer(ANativeWindow* window, ANativeWindowBuffer** buffer, int* fenceFd) {return window->dequeueBuffer(window, buffer, fenceFd);
}

可以看到,这里调用了 ANativeWindow的dequeueBuffer函数,而这个函数在 ANativeWindow 结构里是一个函数指针,而这个函数指针又指向哪里呢?


Hook的实现者Surface

\frameworks\native\libs\gui\include\gui\Surface.h

\frameworks\native\libs\gui\Surface.cpp

class Surface: public ANativeObjectBase<ANativeWindow, Surface, RefBase>
{}

Surface 是 ANativeWindow的子类,但是考虑到 ANativeWindow通过 函数指针的方式实现多态,所以Surface里面一定有设置Hook的地方,那就是在构造函数里。

Surface::Surface(const sp<IGraphicBufferProducer>& bufferProducer, bool controlledByApp): mGraphicBufferProducer(bufferProducer),mCrop(Rect::EMPTY_RECT),mBufferAge(0),mGenerationNumber(0),mSharedBufferMode(false),mAutoRefresh(false),mSharedBufferSlot(BufferItem::INVALID_BUFFER_SLOT),mSharedBufferHasBeenQueued(false),mQueriedSupportedTimestamps(false),mFrameTimestampsSupportsPresent(false),mEnableFrameTimestamps(false),mFrameEventHistory(std::make_unique<ProducerFrameEventHistory>()) {// Initialize the ANativeWindow function pointers.ANativeWindow::setSwapInterval  = hook_setSwapInterval;ANativeWindow::dequeueBuffer    = hook_dequeueBuffer;ANativeWindow::cancelBuffer     = hook_cancelBuffer;ANativeWindow::queueBuffer      = hook_queueBuffer;ANativeWindow::query            = hook_query;ANativeWindow::perform          = hook_perform;ANativeWindow::dequeueBuffer_DEPRECATED = hook_dequeueBuffer_DEPRECATED;ANativeWindow::cancelBuffer_DEPRECATED  = hook_cancelBuffer_DEPRECATED;ANativeWindow::lockBuffer_DEPRECATED    = hook_lockBuffer_DEPRECATED;ANativeWindow::queueBuffer_DEPRECATED   = hook_queueBuffer_DEPRECATED;

至此,所有调用 ANativeWindow_xxx 的入口都会被导入 Surface 里的相应接口中,进入 hook_xxx。

int Surface::hook_dequeueBuffer(ANativeWindow* window,ANativeWindowBuffer** buffer, int* fenceFd) {Surface* c = getSelf(window);return c->dequeueBuffer(buffer, fenceFd);
}

进而在进入 Surface 的 xxx 函数。从 xxx 函数开始,和 graphic service 打交道就正式开始了。

int Surface::dequeueBuffer(android_native_buffer_t** buffer, int* fenceFd) {ATRACE_CALL();status_t result = mGraphicBufferProducer->dequeueBuffer(&buf, &fence, reqWidth, reqHeight,reqFormat, reqUsage, &mBufferAge,enableFrameTimestamps ? &frameTimestamps: nullptr);}

Graphic Buffer 服务

Surface 的 所有和 Graphic Buffer 相关的接口都会使用到成员 mGraphicBufferProducer

sp<IGraphicBufferProducer> mGraphicBufferProducer;

这个成员的类型一看就是一个 binder Interface,继续看 IGraphicBufferProducer。

\frameworks\native\libs\gui\include\gui\IGraphicBufferProducer.h

class IGraphicBufferProducer : public IInterface
{
public:using HGraphicBufferProducerV1_0 =::android::hardware::graphics::bufferqueue::V1_0::IGraphicBufferProducer;using HGraphicBufferProducerV2_0 =::android::hardware::graphics::bufferqueue::V2_0::IGraphicBufferProducer;。。。virtual status_t dequeueBuffer(int* slot, sp<Fence>* fence, uint32_t w, uint32_t h,PixelFormat format, uint64_t usage, uint64_t* outBufferAge,FrameEventHistoryDelta* outTimestamps) = 0;。。。}

至此,一个binder 模型已经形成。

Surface::mGraphicBufferProducer as Bp   <--IGraphicBufferProducer BINDER-->  ? as Bn

那么这里的Bn是什么呢?只需要在源码里搜一下哪些类继承IGraphicBufferProducer 即可。其中一个常用的类就是 BufferQueueProducer。

\frameworks\native\libs\gui\include\gui\BufferQueueProducer.h

class BufferQueueProducer : public BnGraphicBufferProducer,private IBinder::DeathRecipient {。。。virtual status_t queueBuffer(int slot,const QueueBufferInput& input, QueueBufferOutput* output);
。。。}

继承自BnGraphicBufferProducer,Bn开头,一看就知道是server端。

status_t BufferQueueProducer::dequeueBuffer(int* outSlot, sp<android::Fence>* outFence,uint32_t width, uint32_t height, PixelFormat format,uint64_t usage, uint64_t* outBufferAge,FrameEventHistoryDelta* outTimestamps) {sp<GraphicBuffer> graphicBuffer = new GraphicBuffer(width, height, format, BQ_LAYER_COUNT, usage,{mConsumerName.string(), mConsumerName.size()});status_t error = graphicBuffer->initCheck();}

在上面的代码里有 new GraphicBuffer 和 initCheck() 这两个动作,这里就是在创建管理对象并进行初始化检查。


内存管理

\frameworks\native\libs\ui\include\ui\GraphicBuffer.h

class GraphicBuffer: public ANativeObjectBase<ANativeWindowBuffer, GraphicBuffer, RefBase>,public Flattenable<GraphicBuffer>
{GraphicBuffer(uint32_t inWidth, uint32_t inHeight, PixelFormat inFormat,uint32_t inUsage, std::string requestorName = "<Unknown>");}

\frameworks\native\libs\ui\GraphicBuffer.cpp

GraphicBuffer::GraphicBuffer(uint32_t inWidth, uint32_t inHeight, PixelFormat inFormat,uint32_t inLayerCount, uint64_t inUsage, std::string requestorName): GraphicBuffer() {mInitCheck = initWithSize(inWidth, inHeight, inFormat, inLayerCount, inUsage,std::move(requestorName));
}
status_t GraphicBuffer::initWithSize(uint32_t inWidth, uint32_t inHeight,PixelFormat inFormat, uint32_t inLayerCount, uint64_t inUsage,std::string requestorName)
{GraphicBufferAllocator& allocator = GraphicBufferAllocator::get();uint32_t outStride = 0;status_t err = allocator.allocate(inWidth, inHeight, inFormat, inLayerCount,inUsage, &handle, &outStride, mId,std::move(requestorName));if (err == NO_ERROR) {mBufferMapper.getTransportSize(handle, &mTransportNumFds, &mTransportNumInts);width = static_cast<int>(inWidth);height = static_cast<int>(inHeight);format = inFormat;layerCount = inLayerCount;usage = inUsage;usage_deprecated = int(usage);stride = static_cast<int>(outStride);}return err;
}

上面创建 Graphic Buffer 的过程中会有一个 initWithSize , 这是初始化的过程,在初始化过程里又又 allocator.allocate,可见是一个内存分配的过程。


内存分配 GraphicBufferAllocator

\frameworks\native\libs\ui\include\ui\GraphicBufferAllocator.h

\frameworks\native\libs\ui\GraphicBufferAllocator.cpp

class GraphicBufferAllocator : public Singleton<GraphicBufferAllocator>
{}

allocate 方法如下

status_t GraphicBufferAllocator::allocate(uint32_t width, uint32_t height,PixelFormat format, uint32_t layerCount, uint64_t usage,buffer_handle_t* handle, uint32_t* stride,uint64_t /*graphicBufferId*/, std::string requestorName)
{。。。status_t error =mAllocator->allocate(width, height, format, layerCount, usage, 1, stride, handle);
。。。}

mAllocator 是什么 ? 

sp<hardware::graphics::allocator::V2_0::IAllocator> mAllocator;

又是一个 Interface ,这次是 hardware 的,因此其实现是由硬件完成的。具体细节参考 /hardware/interfaces/graphics/allocator/2.0/

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/diannao/59806.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

TDengine 签约蘑菇物联,改造通用设备工业互联网平台

在当前工业互联网迅猛发展的背景下&#xff0c;企业面临着日益增长的数据处理需求和智能化转型的挑战。通用工业设备的高能耗问题愈发突出&#xff0c;尤其是由这些设备组成的公辅能源车间&#xff0c;亟需更高效的解决方案来提升设备运行效率&#xff0c;降低能源消耗。为此&a…

Python怎么查看编码

在读取中文的情况下&#xff0c;通常会遇到一些编码的问题&#xff0c;但是首先需要了解目前的编码方式是什么&#xff0c;然后再用decode或者encode去编码和解码&#xff0c;下面是使用chardet库来查看编码方式的。 import chardet path "E:/t.csv" #path "E…

sqoop Oracle 导入到hive 日期时间消失

sqoop脚本&#xff1a; sqoop import -D mapred.job.queue.namehighway \ -D mapreduce.map.memory.mb4096 \ -D mapreduce.map.java.opts-Xmx3072m \ --connect "jdbc:oracle:thin://localhost:61521/LZY2" \ --username LZSHARE \ --password 123456 \ --query &q…

[产品管理-59]:项目组合中产品或项目的类型分类

目录 一、概述 1、突破型项目&#xff1a;全新产品&#xff0c;颠覆性产品 2、平台型项目&#xff1a;平台产品 3、衍生型项目&#xff1a;衍生出来的新产品&#xff0c;不同于现有产品&#xff0c;但与现有产品有关联 4、支持性项目&#xff1a;现有产品的改进&#xff0…

WPF中的ResizeMode

在 WPF (Windows Presentation Foundation) 中&#xff0c;ResizeMode 属性用于指定窗口是否可以被用户调整大小&#xff0c;以及如何调整大小。ResizeMode 属性可以设置为以下几个值之一&#xff1a; NoResize&#xff1a;窗口不能被用户调整大小&#xff0c;但可以被程序代码…

第2章立项-2.4如何进行立项评审?

2.4 如何进行立项评审&#xff1f; 2.4 如何进行立项评审&#xff1f;2.4.1 立项沟通不充分会带来的问题2.4.2 让大家都参与到立项评审中发表意见 2.4 如何进行立项评审&#xff1f; 研发项目不仅仅和研发相关&#xff0c;而且跟采购、工艺、制造、销售、市场都相关。如果相关…

设计模式-七个基本原则之一-迪米特法则 + 案例

迪米特法则:(LoD) 面向对象七个基本原则之一 只与直接的朋友通信&#xff1a;对象应只与自己直接关联的对象通信&#xff0c;例如&#xff1a;方法参数、返回值、创建的对象。避免“链式调用”&#xff1a;尽量避免通过多个对象链进行调用。例如&#xff0c;a.getB().getC().do…

VUE单页面 路由

1. 安装 Vue 和设置项目 首先&#xff0c;确保你已经安装了 Node.js 和 npm&#xff08;或 Yarn&#xff09;&#xff0c;然后使用 Vue CLI 创建一个新的项目。 安装 Vue CLI 如果你没有安装 Vue CLI&#xff0c;可以通过以下命令来安装&#xff1a; npm install -g vue/cl…

基于vue3实现的聊天机器人前端(附代码)

<template><div class"container"><!-- 页面头部 --><header><h1>跟它说说话吧&#xff01;</h1><p>一个活泼的伙伴&#xff0c;为你提供情感支持&#xff01;</p></header><!-- 聊天容器 --><div c…

【安卓13 源码】Input子系统(4)- InputReader 数据处理

1. 多指触控协议 多指触控协议有 2 种&#xff1a; > A类&#xff1a; 处理无关联的接触&#xff1a; 用于直接发送原始数据&#xff1b; > B类&#xff1a; 处理跟踪识别类的接触&#xff1a; 通过事件slot发送相关联的独立接触更新。 B协议可以使用一个ID来标识触点&…

Uniapp的学习

uniapp的内容和vue网页开发会有很多区别&#xff0c;但是都是基于vue开发的&#xff0c;大多数业务还是在vue打交道&#xff0c;但是这些uniapp的特殊的知识点也是要掌握好的。 基本配置 创建uniapp项目 npx degit dcloudio/uni-preset-vue#vite-ts 项目名 &#xff1a;用于…

Windows Server 怎么关闭IE增强安全配置(关闭IE弹窗)

首先第一步打开IE浏览器&#xff0c;根据下图所示&#xff0c;访问网页时会弹出警告窗口。 打开【控制面板】图标。查看方式改为小图标&#xff0c;打开【管理工具】 第五步进入【管理工具】页面后&#xff0c;找到并双击【服务器管理器】选项。 第六步在弹出的窗口中&#…

鸿萌数据迁移服务: 企业服务器整机在线热迁移, 实现不停机业务转移

天津鸿萌科贸发展有限公司从事数据安全服务二十余年&#xff0c;致力于为各领域客户提供专业的数据存储、数据恢复、数据备份、数据迁移等解决方案与服务&#xff0c;并针对企业面临的数据安全风险&#xff0c;提供专业的相关数据安全培训。 鸿萌数据迁移业务为众多企业顺利高效…

vue2组件封装和UI组件的二次封装,方法,属性,ref的传递

封装组件使用v-model 使用方法props接受value值&#xff0c;当值发生变化的时候再通过this.$emit("input", newValue)&#xff0c;则实现了简单组件的v-model封装,如果不使用第三方UI可以接受到的值使用watch或者计算属性保存&#xff0c;然后再通过事件派发自己保存…

无网络安装ionic和运行

npm 安装本地文件 之前使用npm安装包都是在有网络环境下&#xff0c;因为需要部署无互联网环境开发&#xff0c;才知道如何使用npm安装包安装本地文件/文件夹 >npm install <folder>如果 <folder> 位于项目的根目录中&#xff0c;它的依赖项将被安装&#xff…

ChatGPT 新体验:AI 搜索功能与订阅支付指南

就在凌晨&#xff0c;在 ChatGPT 迎来两周岁生日之际&#xff0c;OpenAI 重磅发布了 ChatGPT 的全新人工智能搜索体验。 期待已久的时刻终于到来&#xff0c; ChatGPT 正式转型成为一款革命性的 AI 搜索引擎&#xff01; 先来看看 ChatGPT 搜索&#xff1a;这次不是简单的加个…

测试开发面试题记录

1. TCP与UDP的区别及应用场景 TCP (传输控制协议)&#xff1a; 特点&#xff1a;面向连接&#xff0c;可靠性高&#xff0c;数据顺序保证&#xff0c;流量控制和拥塞控制。应用场景&#xff1a;文件传输&#xff08;FTP&#xff09;&#xff0c;电子邮件&#xff08;SMTP&#…

[JAVAEE] 面试题(四) - 多线程下使用ArrayList涉及到的线程安全问题及解决

目录 一. 多线程下使用ArrayList 1.1. 自行判断加锁 1.2 使用Collections.synchronizedList()套壳加锁 1.3 CopyOnWriteArrayList类 二. 总结 一. 多线程下使用ArrayList 多线程下使用ArrayList会涉及到线程安全问题, 例如: public static void main(String[] args) thro…

canal1.1.7使用canal-adapter进行mysql同步数据

重要的事情说前面&#xff0c;canal1.1.8需要jdk11以上&#xff0c;大家自行选择&#xff0c;我这由于项目原因只能使用1.1.7兼容版的 文章参考地址&#xff1a; canal 使用详解_canal使用-CSDN博客 使用canal.deployer-1.1.7和canal.adapter-1.1.7实现mysql数据同步_mysql更…

DevExpress中文教程 - 如何使用AI模型检查HTML编辑中的语法?

DevExpress .NET MAUI多平台应用UI组件库提供了用于Android和iOS移动开发的高性能UI组件&#xff0c;该组件库包括数据网格、图表、调度程序、数据编辑器、CollectionView和选项卡组件等。 目前许多开发人员正在寻找多种方法将AI添加到解决方案中&#xff08;这通常比想象的要…