公告

微信

欢迎大家私信交流

Skip to content

面试

flutter基础

flutter中的key

widget是否复用

stf 和 stl 组件

flutter渲染

渲染流程

渲染原理

简单来说,Flutter 的渲染原理可以概括为:通过组合(Composition)而非继承(Inheritance)的方式构建视图,并使用自建的渲染引擎(Skia)直接在画布(Canvas)上进行绘制,从而实现对每一像素的精准控制,达到高效的渲染性能。

下面我们分步骤拆解这个过程。

核心三棵树:Widget -> Element -> RenderObject

Flutter 的渲染流程围绕着三棵核心的树形结构展开,它们协同工作,构成了响应式UI框架的基石。

  1. Widget 树

    • 是什么:Widget 是您用代码编写的UI描述,是不可变的(Immutable)。它们就像蓝图,配置了Element和RenderObject应该如何被创建和更新。
    • 特点:轻量级、不可变。当状态改变时,整个 Widget 树会重建,但这不是性能问题,因为重建的是“蓝图”本身,而不是底层的渲染实体。
    • 例子Container, Text, Row, Column 等。
  2. Element 树

    • 是什么:Element 是 Widget 在 UI 树中的实例化对象。它负责管理 Widget 的生命周期,并将 Widget 的配置信息与底层的 RenderObject 连接起来。它是 Widget 和 RenderObject 之间的“粘合剂”。
    • 特点:可变的、稳定的。当 Widget 树重建时,Flutter 会使用 diff 算法对比新旧 Widget,然后尽可能地复用已有的 Element,只更新发生变化的 Element。这是 Flutter 高效的关键之一。
    • 工作方式Element 会检查新的 Widget 是否与旧的 Widget 是同一类型(runtimeTypekey 相同)。如果是,则更新现有的 Element 和 RenderObject;如果不是,则销毁旧的并创建新的。
  3. RenderObject 树

    • 是什么:RenderObject 是真正负责布局(Layout)和绘制(Painting) 的对象。它保存了元素的几何信息(如大小、位置)。
    • 特点:重量级、持久化。布局和计算是非常昂贵的操作,因此 RenderObject 树会尽量避免重建和重新计算。
    • 核心方法
      • layout():执行布局约束(由父节点传递下来的 Constraints),计算自身大小(Size),并递归地对子节点进行布局。
      • paint():在给定的画布(Canvas)上,根据布局阶段计算出的位置和大小,将自己绘制出来。

三棵树的关系图解:

// 你的代码
Widget build(BuildContext context) {
  return Container( // Widget
    color: Colors.blue,
    child: Text('Hello'),
  );
}

// 底层对应
Widget Tree:    Container Widget -> Text Widget
                    |               |
Element Tree:   Container Element -> Text Element
                    |               |
RenderObject树:  RenderFlex    ->  RenderParagraph

当你改变 Text 的字符串时,Text Widget 会重建,对应的 Element 会被复用,RenderParagraph 会被标记为“脏”(dirty),需要重新绘制。而 Container 的 Element 和 RenderObject 可能完全不受影响。


完整的渲染管线(Rendering Pipeline)

一个完整的 UI 更新(例如,因为 setState 触发了 Widget 树重建)会经历以下阶段:

  1. 动画(Animate)

    • 渲染管线首先处理所有的动画(Ticker)。动画的每一帧都会触发一次新的构建和渲染。
  2. 构建(Build)

    • setState() 被调用,标记了某个 State 为“脏”。
    • Flutter 会重新执行 build 方法,生成新的 Widget 树。
    • Flutter 会将新的 Widget 树与旧的 Widget 树进行对比(Diff),并相应地更新 Element 树(复用、更新、创建或销毁 Element)。
    • 注意:这个阶段只更新 Element 和 RenderObject 的配置,不进行布局和绘制
  3. 布局(Layout)

    • 布局过程是一个从根 RenderObject 开始的递归过程。
    • 约束向下传递(Constraints go down):父节点向子节点传递布局约束(例如,最大/最小宽度/高度)。
    • 尺寸向上传递(Sizes go up):子节点根据约束决定自己的尺寸,然后告诉父节点。
    • 如果一个 RenderObject 在构建阶段被标记为“需要布局”(dirty),或者它的约束发生了变化,它就会重新执行 layout 方法。
    • 布局完成后,每个 RenderObject 都拥有了一个确定的位置和大小。
  4. 绘制(Paint)

    • 绘制过程也是一个递归过程,但顺序通常是自上而下。
    • 如果一个 RenderObject 在布局阶段被标记为“需要绘制”(dirty),它就会重新执行 paint 方法。
    • paint 方法会接收一个 Canvas 对象,RenderObject 在这个画布上调用绘图指令(例如,画矩形、写文字、显示图片)来绘制自己。
    • 绘制过程会生成一个或多个 Layer(图层)。这些图层最终会被合成(Composited)到屏幕上。
  5. 合成(Compositing)

    • 这是渲染管线的最后一步。Flutter 的渲染引擎(Skia)会将所有由 paint 方法生成的图层合成为一张完整的画面。
    • 对于需要硬件加速的图形操作(如变换、透明度),Flutter 会使用不同的图层来处理,以提高效率。

为什么这种设计是高效的?

  1. 声明式UI与响应式编程:你只需要描述“当前状态下的UI应该是什么样子”,而不需要关心如何从状态A更新到状态B。框架(Flutter)会自动、高效地帮你完成更新。

  2. 逻辑与渲染分离:轻量级的 Widget 负责业务逻辑和配置,重量级的 RenderObject 负责昂贵的布局和绘制。两者通过稳定的 Element 树连接,实现了高效的更新。

  3. 精细的重建:通过 Diff 算法和“脏”标记机制,Flutter 能够将 UI 变化的影响范围降到最低。改变一个文本,通常不会导致整个页面重新布局和绘制。

  4. 绕过原生控件,直接渲染:与 React Native 等框架不同,Flutter 不依赖平台的原生控件。它自己管理 RenderObject 树,并通过 Skia 引擎直接向画布(Canvas)绘制。这消除了桥接(Bridge)带来的性能开销,并保证了 UI 在不同平台上的一致性和高性能。

Flutter 的渲染原理可以概括为:

开发者编写不可变的 Widget -> Flutter 通过 Diff 算法更新可复用的 Element -> Element 驱动持有布局和绘制信息的 RenderObject -> RenderObject 通过 layoutpaint 方法,利用 Skia 引擎直接在画布上绘制出最终界面。

这套机制确保了 Flutter 能够以每秒 60 帧(甚至 120 帧)的流畅度运行,并提供极其灵活和强大的 UI 构建能力。

setState流程

buildContext作用

Widget、Element、RenderObject

热重载实现原理

skia渲染逻辑

flutter状态管理

getx

provider

bloc

flutter框架进阶

flutter中的engine层、framework层

flutter原生通信

dart语法基础

dart进阶

dart异步实现机制

总结:异步IO+事件循环

事件队列 微任务队列 垃圾回收

1. Flutter 的消息队列与事件队列

Flutter 作为一个单线程模型的应用,其异步能力依赖于 事件循环(Event Loop) 机制。这个机制中包含了两个核心队列:

  • 微任务队列(Microtask Queue)
  • 事件队列(Event Queue)

事件循环(Event Loop)模型

Dart(以及 Flutter)的运行模型是一个单线程的、带有事件循环的模型。这个线程通常被称为 UI 线程主线程。它的工作流程可以用下图表示:

启动App
  |
  v
执行main函数
  |
  v
启动事件循环
  |
  v
[ 事件循环开始 ]
  |
  |-----> 检查微任务队列 -----> 有任务? -----> 执行微任务(直到队列为空)
  |                              ^
  |                              | 无
  |                              |
  |                              v
  |-----> 检查事件队列    -----> 有任务? -----> 取出第一个事件并执行
  |                              ^
  |                              | 无
  |                              |
  |                              |(应用可以退出)
  |------------------------------

a 微任务队列

  • 优先级:最高。在当前事件处理和其他任何事件之前执行。
  • 用途:用于非常紧急的、需要在这个事件循环周期内完成的短小任务。通常用于库内部的清理工作或状态同步,在业务代码中应谨慎使用
  • 调度方法scheduleMicrotask(() { ... })Future.microtask(() { ... })

b 事件队列

  • 优先级:低于微任务队列。
  • 用途:处理绝大多数异步操作,如:
    • I/O 操作(文件读写、网络请求)
    • 用户输入(点击、滑动)
    • 定时器(Future.delayed, Timer
    • 绘制与布局事件
  • 调度方法:通过 Future 创建的任务,默认都会被加入到事件队列。

代码示例与执行顺序

dart
void main() {
  print('1. main start');

  // 事件队列
  Future(() => print('6. Event Queue Task'));

  // 微任务队列
  scheduleMicrotask(() => print('4. Microtask 1'));

  // 通过 Future 创建的微任务
  Future.microtask(() => print('5. Microtask 2'));

  // 立即执行的 Future(仍然会在当前事件之后)
  Future(() => print('3. Immediate Event Queue Task'))
    .then((_) => print('7. Then callback (also microtask)'));

  print('2. main end');

  // 事件循环启动,开始处理队列中的任务
}

输出顺序:

1. main start
2. main end
4. Microtask 1
5. Microtask 2
3. Immediate Event Queue Task
7. Then callback (also microtask)
6. Event Queue Task

关键点:

  • thenawait 之后的代码,其回调会被包装成微任务,因此执行优先级高于普通事件。
  • 永远不要在主线程执行耗时操作(如大量计算、同步网络请求)。这会阻塞事件循环,导致页面卡顿甚至应用无响应(ANR)。耗时任务应使用 Isolate 在后台执行。

Dart 使用一种分代垃圾回收(Generational Garbage Collection) 机制,这与 Java, C# 等语言类似。它的设计目标是实现低延迟,避免因GC导致UI卡顿。

分代假说

GC 基于两个“假说”:

  1. 弱分代假说:绝大多数对象都是“朝生夕死”的(例如,在 build 方法中创建的临时对象)。
  2. 强分代假说:存活得越久的对象,越不可能被回收。

基于此,Dart VM 将内存分为两块:

  • 新生代(Young Generation)
  • 老年代(Old Generation)

a 新生代

  • 存储对象:新创建的、存活时间短的对象。
  • 回收算法:采用 复制清除(Copying Collector)
    • 新生代空间被分为两块:活动区空闲区
    • 对象首先在活动区分配。
    • 当GC触发时,GC会遍历活动区的“活跃对象”,并将它们复制到空闲区
    • 然后,清空原来的活动区,并交换两个区的角色。
  • 特点速度极快。因为只处理“活跃对象”,不活跃的对象直接被丢弃。由于新生代空间小,且大部分对象都很快死亡,所以这个回收过程非常高效。

b 老年代

  • 存储对象:在新生代中经历过多次GC后仍然存活的对象(被称为“晋升”)。
  • 回收算法:采用 标记-清除-整理(Mark-Sweep-Compact)
    • 标记:从GC根对象(如全局变量、当前执行栈上的变量)开始,遍历所有可达对象,并标记为“活跃”。
    • 清除:遍历整个老年代内存,将所有未被标记的对象回收。
    • 整理:(可选,非每次进行)为了避免内存碎片,GC会将存活的对象移动到内存的一端。
  • 特点速度较慢。因为需要遍历整个老年代空间。但触发的频率远低于新生代GC。

GC 与 Flutter 性能

  • 避免卡顿:Dart 的 GC 是 增量式的(Incremental)并发的(Concurrent)。这意味着 GC 过程可以被分成很多小步骤,并与应用程序的代码执行交错进行,而不是一次性地“停止世界”(Stop-The-World),从而极大地减少了单次GC造成的UI卡顿。
  • 开发建议
    • 避免创建大量小对象:虽然在新生代GC很快,但频繁触发GC和对象晋升也会带来开销。在 build 方法或动画的 builder 中尤其要注意。
    • 使用 const 构造函数:对于不变的 Widget,使用 const 可以避免在每次重建时创建新的实例,既减少了GC压力,又提高了性能。
      • const Container() 优于 Container()
    • 及时销毁监听器:对于 AnimationController, ScrollController 等,在 dispose 方法中一定要调用 dispose(),以便GC可以回收它们。
    • 小心闭包:避免在长时间存在的对象中持有对 BuildContext 或其他大型对象的引用,这可能导致它们无法被回收。

总结:三者如何协同工作

  1. 你编写代码:包括同步代码和异步代码(Future, async/await)。
  2. 任务入队:你的异步任务被分配到微任务队列事件队列中。
  3. 事件循环驱动事件循环 按优先级(微任务 -> 事件)从队列中取出任务执行。
  4. 内存分配与回收:任务执行过程中创建的对象在堆内存中分配。Dart VM 的垃圾回收器会默默地在后台工作,自动回收不再使用的内存,确保应用不会因内存泄漏而崩溃。
  5. 核心目标:这套机制的最终目标是确保 UI 线程的流畅。通过高效的异步模型和低延迟的GC,Flutter 能够实现 60fps/120fps 的丝滑用户体验。

业务

ios开发者账号维护

打包/上架

平常是怎么打包的?如果我一个原生的app里面引入了flutter的话我应该怎么打包?

支付

支付踩坑

购买漏包如何解决?

IM实现

缓存

hive

sqflite

其他数据库

其他

flutter较于别的跨平台优势和劣势

pubspec.lock

其他面试题

一些面试题

上次更新于: