2.1 架构分层与一帧的旅程


2.1 架构分层与一帧的旅程

本节摘要:本节是全章的物理底图。先纵切三层——Framework(Dart 写的界面世界)、Engine(C++ 写的图形核心)、Embedder(各平台宿主壳),再横切时间——一帧从垂直同步信号出发,经构建、布局、绘制、合成、光栅化六道工序在预算内上屏。读完你将能对任何一次卡顿给出第一手的定位假设。

三层各管一段:从你的代码到屏幕像素

第 1 章说过宿主与画布的分工,这里把引擎内部也摊开。Flutter 的运行时自上而下分三层,每层的语言、职责、出错特征都不同:

Framework 层(Dart):你日常打交道的全部——Widget、手势、动画、主题、状态管理都在这一层,它是纯 Dart 写的,随你的应用一起打包。性能问题里"重建过多""布局爆炸"一类,病根在这层,治法也在这层:改你的代码或用法。

Engine 层(C++):图形核心。文本排版与光栅化、图形后端(Impeller,以及仍在逐步退场的 Skia)、合成器、Dart 运行时(含 Isolate 调度与垃圾回收)都在这里。这一层你改不了,但它的行为可观测——DevTools 里的时间线大多是这层和 Framework 层对话的记录。

Embedder 层(平台语言):第 1 章说的宿主壳。Android 上是一段接入 Activity 生命周期的代码,iOS 上接入 UIApplication,桌面接入各自的窗口系统。平台通道的"平台侧"就住在这层,第 7 章会正式打开它。

分层的价值在于故障定位有方向:界面显示错——查 Framework(你的 Widget 树);所有内容都渲染但整体卡顿掉帧——查管线各工序耗时;应用起不来、窗口出不来——查 Embedder 与宿主配置;文字全是豆腐块、图形撕裂——才是 Engine 侧的问题。

图 3 三层架构与一帧的六道工序

图 3 三层架构与一帧的六道工序

线程模型:管线跑在哪些线程上

管线的六道工序不是挤在一个线程里跑的,线程归属本身就是诊断的坐标系:

  • 平台主线程(宿主壳的线程):系统事件先到这,平台通道的原生侧回调也住这(第 7 章会强调它的纪律);
  • UI 线程(Dart):构建与布局两道工序,你的 build 代码在这执行——第 2.4 节说的"界面 Isolate"就是它;
  • 光栅线程:合成与光栅化,把绘制指令变成像素;图片解码等重活也在关联的工作线程;
  • GPU 进程:真正的图形硬件调用,应用开发者无需触碰。

线索串起来就是:你的代码只直接占用平台主线程(经通道)与 UI 线程(build 与逻辑);光栅线程的账虽然常被你的写法间接放大(图层多、重绘范围大),但你从不直接在它上面写代码——这也是"光栅线程红先查结构、UI 线程红先查代码"口诀的由来。

一帧的排班表:16 毫秒怎么花

屏幕以固定节奏刷新(60Hz 即约 16.7 毫秒一帧,高刷屏预算更紧),每次刷新前引擎收到垂直同步信号,管线开跑。工序顺序固定,谁超时谁背锅:

构建:框架从标记为"脏"的位置出发执行 build,产出新的 Widget 配置树。这一步只做描述,不碰像素。build 里写循环、算大数、同步读盘,超时账全记在这道工序头上。

布局:约束从根节点向下传递("你最大多大"),尺寸从叶向上汇报("我就要这么大"),一趟遍历完成。第 3 章的布局规则就是这道工序的外显。布局是递归的,嵌套越深越贵,这也是"列表项别嵌几十层"的由来。

绘制:需要重绘的部分把"画圆、画字、画阴影"这类指令记录到图层上。注意它记录的是指令而非像素,真正的像素在最后一道工序产生。

合成与光栅化:Engine 把图层组装成场景,交给图形后端光栅化成位图。Impeller 的改进就发生在这里——它在管线构建期就预编译好着色器,砍掉了 Skia 时代首次遇到某种效果时现场编译着色器造成的小卡顿(当年动画第一次播放顿一下,多半是它)。

诊断时用得上的一句话账法:UI 线程超时看构建与布局,光栅线程超时看绘制指令量与图层结构。DevTools 的性能视图正是按这两个线程分开记账的,第 6 章会实操。

动手:亲眼看见一帧超时

下面的页面故意在 build 里同步睡掉 50 毫秒,跑在真机上即可复现一次可测量的掉帧:

import 'dart:io'; import 'package:flutter/material.dart'; class JankDemo extends StatelessWidget { const JankDemo({super.key}); void burnFrame() { // 反面教材:在 build 路径上同步阻塞,只做演示 sleep(const Duration(milliseconds: 50)); } @override Widget build(BuildContext context) { return Scaffold( body: Center( child: ElevatedButton( onPressed: () { burnFrame(); ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('这一帧超时了')), ); }, child: const Text('制造一次卡顿'), ), ), ); } }

按下按钮的瞬间,界面明显一顿——50 毫秒的活儿挤进 16 毫秒的预算,本帧作废,下一帧补画。打开 DevTools 的 Performance 页,时间线上会有一段醒目的红色帧条。修法预告:把这类耗时逻辑挪出 build(第 2.4 节的 Isolate),或者至少拆成多帧分摊。此刻先记住结论:卡顿不是"手机慢",是某道工序超了预算,而工序是可以点名批评的

本节要点回顾

  • 三层职责:Framework 管界面逻辑,Engine 管图形与运行时,Embedder 管宿主接入;
  • 定位问题的路径:显示错查 Framework,掉帧查管线工序,起不来查 Embedder;
  • 一帧六道工序固定排班:同步信号、构建、布局、绘制、合成、光栅化;
  • 布局一趟完成靠"约束向下、尺寸向上",嵌套深度直接换算成布局耗时;
  • Impeller 用预编译着色器消除首次效果卡顿,是渲染后端换代的核心收益。

管线的"构建"工序里出现了 Widget 树的影子。下一节把三棵树摊开,解释为什么重建一棵树很便宜、而重算几何才昂贵。


作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U