想象你去一家餐厅吃饭。翻开菜单,“红烧肉”三个字并不会让你立刻吃饱——那只是一行描述。你点单后,服务员写下一张订单;后厨看到订单,才真正架锅开炒。Flutter 的界面渲染,走的正是这条“菜单→订单→后厨”的路:它的菜单叫 Widget,订单叫 Element,后厨叫 RenderObject。更妙的是,这家“餐厅”不靠外包——别人家的跨平台 App 要借用安卓、iOS 的“原生厨房”,而 Flutter 自带一个叫 Skia 的中央厨房,任何菜都能自己炒,端出来的菜在每个平台上都一样漂亮。本文就用“后厨”的视角,带你拆解 Flutter 的三棵树模型与 Skia 渲染管线,看清一帧画面是怎么从代码变成像素的。
理解 Flutter 的渲染原理,是你定位性能瓶颈、做跨平台技术选型的底气,也是从“会用框架”走向“懂框架”的关键一跃。
可视化演示
展示Widget/Element/RenderObject三树模型、Build-Layout-Paint-Composite渲染管线、图层合成及Skia光栅化流程,含六边网格背景和发光动画。
本节配有交互动效(进入视口后自动加载,约 3KB)。
架构全景
一家餐厅要想正常运转,必须有三样东西:菜谱(告诉客人能吃什么)、点单记录(把菜谱和实际运营连起来)、后厨灶台(真正把菜做出来)。Flutter 的架构也是如此。当你编写界面时,其实在写一份“菜谱”——这就是 Widget 树;Flutter 拿到菜谱后会帮你“下单”——生成 Element 树;而后厨收到订单开始炒菜——构建 RenderObject 树,最终由 Skia 引擎端出成品。三者各司其职,缺一不可。
与 React Native 等框架“借用平台原生控件”的思路不同,Flutter 选择了一条更重的路:自己实现整条渲染链。从 Dart 代码到屏幕像素的每一步——构建、布局、绘制、合成、光栅化——都由 Flutter 引擎自己掌控。这在架构上称为“自绘 UI”,好处是极致的可控性与一致性:不依赖任何平台的组件表现,所有界面元素都由自家引擎绘制。
一次交互触发后,Flutter 需要在一个 vsync(垂直同步)周期内完成以下工作:先根据状态重建 Widget 树,与已有的 Element 树做 diff;接着让 RenderObject 树完成布局,计算每个节点的尺寸与位置;然后生成绘制指令;最后将这些指令交给 Skia 栅格化,输出到屏幕。这一整套流程用公式表示如下。
餐厅的“菜单→订单→后厨”三个系统,对应 Flutter 的“配置→实例→渲染”三棵树。你只需要关心怎么设计菜单(写 Widget),剩下下单、炒菜、上桌的流程,Flutter 这套完整的“餐厅系统”会为你全自动完成。理解了这个全景,后面的每一棵树就都有了坐标。
- Frame(一帧)屏幕上显示的一幅完整画面,每一帧都要走完整个渲染流程。
- Build(构建)重建 Widget 配置,并同步到 Element 树,决定界面“应该长什么样”。
- Layout(布局)遍历 RenderObject 树,根据父级约束计算每个节点的尺寸与位置。
- Paint(绘制)把“画什么”记录成一条条绘制指令,等待后续执行。
- Composite(合成)把各层级的绘制结果按层级关系合并成一张场景图。
- Rasterize(光栅化)Skia 把场景图翻译成 GPU 能执行的命令,最终变成屏幕像素。
Widget树
你翻开菜单,看到的是菜名、食材、价格——仅仅几行字,却能描述一道完整的菜。Widget 也是这样:它是界面的“描述文件”,用类型(Type)、属性(Props)和子组件(Children)三要素,告诉 Flutter “这里应该有一张绿色的圆角按钮,里面写着'登录'”。它不参与任何实际的绘制工作,只是一个“蓝图”。
注意一个关键特性:菜单印出来之后,纸上的字不会自己改变。Widget 同样是不可变的(immutable)。你无法“修改”一个 Widget 的颜色,只能重新印刷一份新的 Widget 来描述新界面。这听起来很浪费,但 Widget 只是几个字段的轻量对象,重建一万个也比做一次真实布局便宜得多——这就叫“轻量描述”。
Flutter 的界面就是一棵由 Widget 组成的组合树。开发者通过嵌套组合来搭建 UI:StatelessWidget 包着 Text,Container 包着 Icon,层层嵌套形成完整的界面结构。由于不可变,每次 setState 都会触发一次 Widget 树重建,但这只是“重新印菜单”,真正的开销在后续阶段——Flutter 会通过对比新旧 Widget 来决定哪些部分需要重新渲染。Widget 的描述公式如下。
回到餐厅:菜单上的文字永远静止,客人看到的永远是“最新版”的菜单。Widget 的哲学正是如此——把描述与实现彻底分离,让“印菜单”这件事变得极快,把宝贵的时间留给真正费力的“炒菜”环节。这也是理解 Flutter 性能模型的第一块基石。
- Type(类型)决定界面的种类,比如按钮、文本、图片。类型相同,节点才可能被复用。
- Props(属性)颜色、字号、间距等配置参数。属性一变,描述就变。
- Children(子组件)一个 Widget 可以包含子 Widget,形成嵌套的树形结构。
- ImmutableConfig(不可变配置)对象一旦创建就不能修改。界面更新 = 重建配置,而不是原地产改。
Element树
餐厅里菜单印了一百份,但一位服务员只会为每桌客人写一张订单。订单把“纸上的菜名”和“后厨的食材”连接起来。Element 就是 Flutter 里的订单:它是 Widget 在树上实际存活的那个“实例记录”,持有这个 Widget 对应的状态信息,并负责决定“该不该让后厨重新炒这道菜”。
当界面刷新产生一本新菜单(新 Widget 树)时,服务员不会把后厨的灶台全部推翻,而是先把新旧菜单逐项对照:这道菜类型没变?那就在旧订单上改改备注(update);这道菜是新加的?那就补一张新订单(mount);这道菜下架了?那就撕掉订单(unmount)。这个对照过程叫 Diff,由 Element 树完成。
三棵树中,Element 树是“中间人”。Element 是 Widget 的实例化产物:每个 Widget 对应一个 Element,第一次挂载时通过 mount 创建,之后 Widget 更新时通过 update 复用旧的 Element。Element 还保存着父节点、子节点、渲染对象等关联信息,是建立树形结构的真正骨架。判断复用的条件,是 runtimeType 与 key 是否一致——这也是为什么 key 在列表优化中如此重要。
服务员讲究“能复用就不重做”,Element 也一样。一张薄薄的订单纸承载了连接的智慧:向上是菜单(Widget),向下是灶台(RenderObject)。理解了 Element,你就理解了 Flutter 如何用最小的力气完成界面的每一次改版。
- update(更新)旧 Element 还活着,只把内部引用的 Widget 换成新版,做最小更新。
- mount(挂载)没有可复用的旧 Element,创建全新 Element,并继续生成对应的 RenderObject。
- Type(运行时类型)Widget 的真实类型。类型变了,说明组件性质改变,必须重建。
- key(键)在同一层级中区分兄弟 Widget 的身份标识,帮助 Diff 精准“认人”。
RenderObject树
订单到了后厨,真正的厨师登场。RenderObject 就是后厨里的大厨:它不关心菜单上写了几行字,只关心两件事——这道菜要做多大(布局),以及这道菜怎么炒(绘制)。它是三棵树里唯一“真刀真枪干活”的角色,也是状态持有最多的地方。
大厨做菜要听老板的约束:桌子的空间就这么大,你得在这个范围内摆盘。RenderObject 也一样,它的 layout 方法接收父节点传来的 Constraints(约束),结合自身属性,计算出自己的 Size 和 Offset。计算完成后,父节点才能确定子节点的位置——整个布局过程是深度优先的递归遍历,一层层从根传到叶子,最终算出整棵树的几何信息。
RenderObject 由 Element 的 createRenderObject() 创建,在 updateRenderObject() 中被校准。布局完成后进入绘制阶段:每个 RenderObject 的 paint 方法把“怎么画”记录到 PaintContext 中,生成绘制指令列表(DisplayList)。注意,这里并没有真正画到屏幕,只是“写菜谱”——真正下锅的是 Skia。
大厨按订单备料、按桌面约束摆盘、按经验写下炒菜步骤——RenderObject 正是那个“按规矩干活”的大厨。它把 Widget 的抽象描述翻译成具体的布局数字和绘制指令,至此,一份菜单才真正变成了可以入口的菜。
- Constraints(约束)父节点传给子节点的“紧箍咒”,限定最大/最小宽高范围。
- Size 与 Offset(尺寸与偏移)布局的产出,决定节点占多大区域、位于父节点哪个位置。
- paint(Canvas)(绘制)把颜色、圆角、文本等内容记录为绘制指令,写入 DisplayList。
- DisplayList(绘制指令列表)一串与平台无关的“画什么、怎么画”命令,等待光栅化执行。
Skia引擎
大多数跨平台餐厅的厨师是“外聘”的:React Native 这类框架需要调用安卓自带的“川菜师傅”、iOS 自带的“粤菜师傅”,两边师傅手艺难免有差别。而 Flutter 的餐厅不一样——它自己养了一支叫 Skia 的黄金厨师团队。Skia 是 C++ 写的开源 2D 图形引擎,Chrome 和 Android 系统都在用它,如今专职为 Flutter 服务。
因为厨师是自己的,Flutter 不需要通过“电话线”(Bridge)去请求原生控件。无论是 Android 还是 iOS,Skia 都使用同一套绘制 API、同一个绘制流程。你在 Dart 层写的 Container、Text,最终都会被翻译成 Skia 能理解的绘制指令,由 Skia 亲自操刀画出每一个像素。这正是“一次编写,处处一致”的底气所在——像素级控制是架构自带的属性。
绘制指令怎么变成屏幕像素?Skia 接收 DisplayList 后,会把它转换为 GPU 指令,交给显卡的渲染管线执行(光栅化),或者在没有 GPU 时用 CPU 兜底绘制。从 Canvas 绘制调用到最终像素,公式可以表示为如下链条。
有了 Skia 这支“自建后厨”,Flutter 的菜无论端到哪个平台的餐桌上,色香味都百分百一致。这也解释了为什么 Flutter 能自信地说:UI 的像素级一致性,从架构上就写死了。
- Canvas.draw(绘制调用)RenderObject 通过画布对象发起绘制,指定形状与样式。
- Path(路径)圆角矩形、贝塞尔曲线等几何形状的数学描述。
- GPU Commands(GPU 指令)Skia 把绘制调用批量编排成显卡管线能并行处理的命令。
- Pixels(像素)光栅化后每个屏幕点上的 RGBA 颜色值,最终被人眼看到。
渲染管线
一次完整的“上菜”流程是怎样的?客人点菜(setState 触发重建)→ 服务员下单(Widget 到 Element)→ 后厨备料炒菜(RenderObject 布局与绘制)→ 摆盘(图层合成)→ 上桌(光栅化)。这五个环节串成一条流水线,每个环节都有可能成为瓶颈。
Flutter 的渲染管线由 Scheduler 调度:系统的垂直同步信号(vsync)每 16.67 毫秒(60fps)响一次,像餐厅固定的“出菜铃”。铃声一响,整条流水线就得转一轮——如果某个环节超时,这一帧就挂了,屏幕就会卡一下。理解这条流水线,是诊断一切性能问题的前提。
五个阶段中,Build 和 Layout 运行在 Dart 层;Paint 生成 DisplayList 后,Skia 的合成器会把多个图层(Layer tree)合并成一张 Scene 图;最后由光栅化线程把 Scene 交给 GPU。由于 GPU 与 CPU 异步,Flutter 还能做并行优化。整个预算公式如下。
餐厅的效率取决于整条流水线中最慢的环节。理解渲染管线,就是为了在界面卡顿时,能一眼定位瓶颈出在“下单太慢”(Widget 重建过多)、“炒菜太久”(布局绘制复杂),还是“上桌拥堵”(光栅化过载)——这就是 Flutter 性能优化的第一性原理。
- T_frame(单帧耗时)一帧从触发到上屏的全部时间。
- 五阶段之和构建、布局、绘制、合成、光栅化五段时间的累加。
- 16.67ms(帧预算)每秒 60 帧时,1 秒 1000ms 除以 60 约等于 16.67ms,是每帧的生死线。
RN对比
假设隔壁还有一家叫 React Native 的餐厅。这家餐厅的菜单用 JS 写,但后厨是外包的:它需要一名“传菜员”(Bridge)把菜谱翻译成电话,打给安卓原生餐厅或 iOS 原生餐厅,请对方用自己的厨具做菜。传菜员效率有限,而且两家原生餐厅的手艺还不一样。
Flutter 餐厅完全不打电话。它的后厨 Skia 就在店里,菜单(Widget)直接进自家后厨开做,中间没有传菜员、没有翻译损耗。这就是两者渲染机制最根本的差异:RN 的 UI 最终由“平台原生组件”渲染,Flutter 的 UI 最终由“自绘引擎”渲染。RN 胜在能快速接入平台原生能力,Flutter 胜在渲染一致与性能可控。
更深的差异在于通信模型。RN 中,JS 线程与 UI 线程之间通过 Bridge 异步传递序列化消息,高频交互容易出现通信瓶颈;Flutter 的 UI 代码(Dart)与渲染引擎在同一个进程内紧密协作,不存在跨语言桥接,绘制指令直达 Skia。两种方案的对比公式如下。
一家是“外包后厨+传菜员”,一家是“自有后厨+自家厨师”。理解了这两套“餐厅运营模式”,你在做技术选型时就能一眼看穿:你的场景更需要原生生态的灵活性,还是像素级的确定性。
- Bridge(桥接)RN 中 JS 与原生端之间的异步消息通道,负责序列化传输指令。
- Native Widgets(原生组件)Android 的 TextView、iOS 的 UILabel 等系统控件,由各平台自绘。
- Skia(RenderObject)Flutter 无需桥接,RenderObject 的绘制指令直接交给 Skia 引擎。
AI 应用场景
跨平台品牌视觉一致性保障
阶段:前端渲染(自绘 UI 落地)
解决的问题:电商 App 的品牌色、圆角、动效在 iOS 与 Android 上经常出现“双子机”偏差。原生开发需要维护两套代码对齐视觉,RN 受原生组件渲染差异影响难以像素级一致。Flutter 用 Skia 自绘,所有平台用同一套渲染指令,彻底消除偏差。
具体案例:某银行 App 用 Flutter 重写理财产品详情页后,设计验收直接对拍 iOS/Android 截图,RGBA 色值误差为 0,UI 走查耗时从 3 天缩短到 2 小时。
高频刷新行情界面的性能优化
阶段:实时数据可视化(布局与绘制性能优化)
解决的问题:股票行情、直播弹幕等高频刷新场景,传统 WebView 或 RN 桥接通信容易掉帧。Flutter 渲染管线全程自控,可在 16.67ms 帧预算内完成从构建到光栅化,配合 CustomPainter 能把绘制指令压缩成极轻的 DisplayList。
具体案例:某量化交易客户端用 Flutter 的 CustomPainter 自绘 K 线图,通过 RenderObject 树深度优化布局次数,Skia 光栅化直通 GPU,实现 120fps 的流畅滚动。
AI 生成 UI 的实时预览渲染
阶段:AIGC 前端代码生成
解决的问题:LLM 生成的 UI 描述需要跨平台渲染验证,RN 受限于平台组件映射表,而 Flutter 的三棵树模型天然适合“配置即界面”——LLM 输出 Widget 树描述,Flutter 直接实例化并交给 Skia 渲染,实现低延迟预览。
具体案例:团队用 LLM 生成 Flutter Widget 树 JSON,配合 Skia 渲染引擎在预览画布上实时出图,实现了“文生界面”的秒级迭代闭环。
Python 演示
class Widget: """界面配置描述, 相当于餐厅菜单""" def __init__(self, kind, props=None): self.kind = kind self.props = props or {} def __repr__(self): return "Widget(%s)" % self.kind class Element: """Widget 的实例记录, 相当于餐厅订单""" def __init__(self, widget): self.widget = widget self.render_object = None class RenderObject: """真正干活的渲染对象, 相当于后厨大厨""" def __init__(self, kind): self.kind = kind self.size = (0, 0) def layout(self, constraints): self.size = constraints return self.size def build(old_elements, widgets): """Widget -> Element: 类型相同则复用, 否则重建""" new_elements = [] for index, widget in enumerate(widgets): if index < len(old_elements) and old_elements[index].widget.kind == widget.kind: old = old_elements[index] old.widget = widget new_elements.append(old) else: new_elements.append(Element(widget)) return new_elements def create_render_object(elements): for element in elements: if element.render_object is None: element.render_object = RenderObject(element.widget.kind) return elements def paint_display_list(elements): return ["draw_%s" % element.widget.kind for element in elements] def render_frame(widgets, constraints=(100, 40)): """模拟一帧: 构建 -> 布局 -> 绘制""" elements = build([], widgets) elements = create_render_object(elements) for element in elements: element.render_object.layout(constraints) return elements, paint_display_list(elements) if __name__ == "__main__": widgets = [Widget("Button", {"text": "登录"}), Widget("Text", {"text": "欢迎"})] elements, commands = render_frame(widgets) print("首次渲染命令:", commands) widgets[1] = Widget("Text", {"text": "欢迎回来", "color": "red"}) elements = build(elements, widgets) print("复用后元素数:", len(elements)) print("复用后渲染命令:", paint_display_list(elements))
class Widget:菜单描述:只保存类型与属性,不参与绘制。class Element:订单记录:持有 Widget 引用与对应的 RenderObject。def layout(self, constraints):后厨按约束计算尺寸,产出布局结果。old.widget = widget类型相同时复用旧 Element,只换新配置,这就是 diff 复用的核心。if element.render_object is None:首次挂载才创建 RenderObject,后续帧直接复用。"draw_%s" % element.widget.kind生成与平台无关的绘制指令(模拟 DisplayList)。render_frame(widgets, constraints)模拟一帧的构建、布局、绘制三阶段串行流程。widgets[1] = Widget("Text", ...)第二次帧只改了 Text 的属性,Button 的 Element 被复用。
常见误区
误区我以为 Widget 树就是最终渲染到屏幕上的组件树,修改 Widget 的颜色就等于直接改了界面上的像素。
正解Widget 只是不可变的配置描述(菜单),从来不参与绘制。真正负责渲染的是由 Element 挂载生成的 RenderObject,最终由 Skia 引擎输出像素。菜单不会自己变成菜,是后厨把菜单做成了菜。
为何常见:从使用者的角度,setState 后界面立即可见,容易把“描述”当成“实物”;加上文档常以“Widget 是 UI 的基础”一言概之,忽略了中间层。
误区我听说 setState 会重建整棵 Widget 树,那每个动画帧都重建,Flutter 不就卡死了吗?
正解Widget 是极轻的配置对象,重建整棵树成本极低;而且重建后还要经过 Element 树 diff,同类型节点会被复用更新,只有真正变化的节点才会触发 RenderObject 的布局与重绘。这是“重印菜单”和“重新炒菜”的区别,重印便宜得多。
为何常见:把“重建 Widget 树”误解为“重绘整个界面”,混淆了三棵树的分工,忽略了 diff 复用的存在。
误区跨平台框架都是通过桥接调用安卓/iOS 的原生控件来渲染的,Flutter 应该也是吧。
正解这是 RN 的机制,Flutter 完全不同。Flutter 自带 Skia 引擎直接自绘所有 UI,RenderObject 的绘制指令直达 Skia 和 GPU,根本不经过平台原生控件。好比 RN 是外包后厨+传菜员,Flutter 是自有后厨+自家厨师。
为何常见:多数人先认识 RN,再用“统一跨平台框架”的既有印象套到 Flutter 上,忽略了架构层面的根本差异。
误区我觉得渲染就是 Widget 树从头到尾画一遍,反正最后屏幕上出来了就行,不需要分阶段。
正解渲染分五个阶段:Build、Layout、Paint 生成的是“绘制指令”(DisplayList),并没有真正上屏;合成阶段把图层合并成场景图;最后光栅化才把指令变成 GPU 像素。理解阶段划分,才能定位性能瓶颈在哪一环。
为何常见:用户只看到“从代码到屏幕”的最终结果,内部阶段的中间产物(指令、图层)对小白而言是黑盒。
误区既然三棵树节点一一对应,那它们就是同一棵树的三个影子,结构、生命周期都一样。
正解三棵树结构大致镜像,但角色和生命周期完全不同:Widget 每帧可能重建,Element 尽量复用存活,RenderObject 承担重量级布局与绘制。对应菜谱、订单、后厨,三者物理上不是一回事,生命周期也不同步。
为何常见:名字都叫“树”,且节点大体对应,容易把角色混为一谈;再加上文档示例常画成并列的三棵树,加深了误解。
面试题
Q1. Flutter 中真正负责布局与绘制(生成渲染指令)的是哪棵树?
- A.Widget 树
- B.Element 树
- C.RenderObject 树← 正确答案
- D.原生组件树
答案:C。Widget 只是配置描述,Element 负责 diff 协调,RenderObject 才真正执行布局与绘制。好比菜单负责描述、订单负责记录、后厨大厨负责炒菜。
Q2. Widget 的核心特性是什么?
- A.可变且状态丰富
- B.不可变轻量配置← 正确答案
- C.直接参与像素绘制
- D.依赖平台原生组件
答案:B。Widget 是不可变(immutable)的轻量配置对象,只描述界面长什么样,不绘制。它像菜单上的文字,可以反复印刷,但不会自己变成菜。
Q3. Element 树在渲染管线中承担什么职能?
- A.计算布局尺寸
- B.生成 GPU 指令
- C.做新旧 Widget 的 diff 协调← 正确答案
- D.将场景光栅化为像素
答案:C。Element 是 Widget 实例化后的“订单记录”,职责是对比新旧 Widget,决定复用 update 还是重建 mount。布局是 RenderObject 的事,光栅化是 Skia 的事。
Q4. 渲染管线中,Paint 阶段产出的是什么?
- A.最终屏幕像素
- B.绘制指令列表← 正确答案
- C.已合成的场景图
- D.布局尺寸数据
答案:B。Paint 阶段把“怎么画”记录成 DisplayList 绘制指令列表,并没有真正上屏。像素输出要等到 Rasterize 光栅化阶段由 GPU 完成。
Q5. Flutter 与 React Native 渲染机制的本质差异是什么?
- A.Flutter 用桥接调用原生组件
- B.RN 用 Skia 自绘全部界面
- C.Flutter 自绘 UI,RN 桥接原生组件← 正确答案
- D.两者都由 iOS 原生渲染
答案:C。本质差异是“外包后厨”与“自有后厨”:Flutter 用 Skia 自绘,RN 通过 Bridge 桥接原生组件。前者像素级一致、无桥接损耗,后者依赖平台能力。
Q6. 为什么 Flutter 中重建整棵 Widget 树依然高效?
- A.因为 Widget 极轻且 Element 树可复用← 正确答案
- B.因为渲染完全由原生完成
- C.因为 Widget 树被缓存无需重建
- D.因为光栅化过程被完全跳过
答案:A。Widget 只是轻量配置,重建便宜;Element 树通过 diff 复用同类型节点,避免 RenderObject 反复销毁重建。重印菜单便宜,不是每次都把后厨推倒重来。
Q7. 帧预算 16.67ms 对应的是多少帧率?
- A.30fps
- B.60fps← 正确答案
- C.90fps
- D.120fps
答案:B。每秒 60 帧时,1000ms ÷ 60 ≈ 16.67ms,这是每帧渲染各阶段的总预算上限。超过这个预算就会掉帧,用户会感到卡顿。
Q8. 以下哪些属于 Flutter 渲染管线的阶段?
- A.Build 构建← 正确答案
- B.Layout 布局← 正确答案
- C.Bridge 桥接
- D.Rasterize 光栅化← 正确答案
答案:ABD。渲染管线五阶段是 Build、Layout、Paint、Composite、Rasterize。Bridge 是 React Native 的通信机制,不属于 Flutter 渲染管线。
Q9. 关于三棵树模型与更新机制,下列说法正确的有哪些?
- A.Widget 是不可变配置,每帧可重建← 正确答案
- B.Element 负责同步 Widget 与 RenderObject← 正确答案
- C.RenderObject 持有布局尺寸与绘制数据← 正确答案
- D.三棵树结构相同且生命周期完全一致
答案:ABC。三棵树分工不同、生命周期不同,D 错误。Widget 是菜谱(配置),Element 是订单(协调),RenderObject 是后厨(布局绘制)。同类型 Widget 更新时 Element 被复用而非重建。
Q10. 关于 Skia 引擎,下列说法正确的有哪些?
- A.Skia 是 C++ 编写的开源 2D 图形引擎← 正确答案
- B.Skia 只能运行在 Android 平台上
- C.Flutter 依靠 Skia 实现跨平台自绘← 正确答案
- D.Skia 直接把 Widget 变成原生控件
答案:AC。Skia 跨平台、跨系统使用,B 错。Flutter 的绘制指令由 Skia 编译为 GPU 命令输出像素,而非变成原生控件,D 错。它像自有后厨,无论哪个平台的餐桌都能端出一致的菜。