面试
flutter基础
flutter中的key
widget是否复用
1. Key 是什么?
- 定义:Key 是 Widget、Element 和 SemanticsNode 的唯一标识符。
- 核心作用:帮助 Flutter 在 Widget 树发生变化时(如交换位置、删除),准确地匹配 Widget 与 Element,从而实现 Element 的复用或状态的保留。
2. 为什么要用 Key?(核心案例)
- 无状态组件 (StatelessWidget):交换两个方块位置时,由于没有状态,Flutter 仅根据类型匹配就能正常更新,不需要 Key。
- 有状态组件 (StatefulWidget):
- 问题:如果不加 Key,交换两个方块后,颜色(State)竟然没有交换!
- 原因:Flutter 的
canUpdate机制默认只检查runtimeType。在没有 Key 时,Element 认为 Widget 类型没变,于是原地复用了旧的 Element。但State是存储在 Element 中的,结果就是 Widget 换了,但“灵魂”(State/颜色)没动。 - 解决:给组件加上唯一的 Key。这样
canUpdate会返回 false,促使 Flutter 重新匹配或通过 Diff 算法找到正确的 Element 对应关系。
3. Key 的种类
Key 主要分为两大类:
LocalKey (局部键):在相同父级下必须唯一。
- ValueKey:以一个值(如字符串、数字)作为标识。
- ObjectKey:以一个对象作为标识。
- UniqueKey:每次构建都生成唯一的 Key(会导致组件无法复用,强制重新创建)。
- PageStorageKey:用于保存页面滚动位置等状态。
GlobalKey (全局键):在整个 App 中必须唯一。
- 作用:可以跨树访问某个 Element 或 State(例如在外部调用 FormState 的校验方法)。
- 代价:开销较大,除非必要否则不建议滥用。
4. 总结
- 当你需要改变相同类型 Widget 的顺序,或者维护 StatefulWidget 的状态时,必须使用 Key。
- Key 应该加在 Widget 树中最顶层发生位置变化的那个组件上。
stf 和 stl 组件
一、 核心生命周期方法(按调用顺序)
- createState: 当组件插入树中时首先调用,用于创建关联的
State对象。此时mounted属性被设为true。 - initState:
State对象初始化时调用(仅一次)。通常用于:- 初始化变量。
- 订阅数据流(Stream)或通知。
- 注意:若要在此时弹出对话框,需使用
WidgetsBinding.instance.addPostFrameCallback。
- didChangeDependencies: 在
initState之后立即调用,或当所依赖的InheritedWidget(如 Theme、Localization)发生变化时调用。 - build: 最常用的方法,用于构建 UI。在以下情况会触发:
initState或didChangeDependencies调用后。- 手动调用
setState。 - 父组件触发
didUpdateWidget后。
- didUpdateWidget: 当父组件重建,且新旧 Widget 的
runtimeType和key相同时调用。常用于在配置变化时更新State。 - deactivate: 当组件被从树中移除时调用(可能是暂时移除,如在树中移动位置)。
- dispose: 组件被永久销毁时调用。必须在此处:
- 取消订阅、关闭控制器(如
AnimationController)。 - 释放资源。
- 取消订阅、关闭控制器(如
二、 几个关键概念
- mounted: 一个布尔值。如果为
true,表示State对象当前正在组件树中。在调用setState前,建议判断if (mounted)以避免内存泄漏或报错。 - dirty (脏状态): 表示组件已被标记为需要重新构建。执行
setState后,组件会进入dirty状态,并在下一帧触发build。 - clean (干净状态): 表示组件当前 UI 与状态同步,不需要重新构建。
三、 总结
理解生命周期的核心在于知道 “什么时候该做什么事”:initState 做初始化,build 只做渲染,dispose 做清理,setState 触发更新。
flutter渲染
渲染流程
1. 图像显示基础
- 显示原理:CPU 计算数据 -> GPU 渲染 -> 放入帧缓冲区 -> 视频控制器根据 VSync(垂直同步信号) 读取并显示。
- Flutter 流程:UI 线程(Dart 构建 Widget)-> GPU 线程(图层合成)-> Skia 引擎(加工为 GPU 数据)-> GPU 渲染。所有操作需在两个 VSync 信号间完成,否则会卡顿。
- 渲染引擎 (Skia):Flutter 默认的 2D 绘图引擎。Android 原生内置;iOS 则需打包在 SDK 中(导致 iOS 包体积稍大)。它保证了多端渲染效果的高度一致。
2. 核心渲染过程(四阶段)
页面由 Widget 树 映射为 RenderObject 树,后续处理分为:
布局 (Layout):
- 深度优先遍历:父节点向下传递布局约束(Constraints),子节点向上传递尺寸信息(Size)。
- 布局边界 (Relayout Boundary):设置边界后,边界内的布局变化不会影响边界外,提升性能。
绘制 (Painting):
- 深度优先遍历:将 RenderObject 绘制到图层(Layer)上。
- 重绘边界 (Repaint Boundary):将经常变动的组件(如 ScrollView)独立到一个图层,避免无关组件跟随重绘,减少性能损耗。
合成 (Compositing):
- 由于图层可能非常多,Flutter 会将多个图层合并、简化,计算出最终的显示效果,避免重复绘制,提高效率。
渲染 (Rasterizing):
- 将合成后的几何数据交给 Skia 加工成像素数据,最终由 GPU 渲染到屏幕上。
总结
Flutter 的高性能源于其自研的渲染管线:通过三棵树机制简化逻辑,利用布局/重绘边界优化局部更新,最后通过 Skia + GPU 实现高效绘制。
渲染原理
简单来说,Flutter 的渲染原理可以概括为:通过组合(Composition)而非继承(Inheritance)的方式构建视图,并使用自建的渲染引擎(Skia)直接在画布(Canvas)上进行绘制,从而实现对每一像素的精准控制,达到高效的渲染性能。
下面我们分步骤拆解这个过程。
核心三棵树:Widget -> Element -> RenderObject
Flutter 的渲染流程围绕着三棵核心的树形结构展开,它们协同工作,构成了响应式UI框架的基石。
Widget 树:
- 是什么:Widget 是您用代码编写的UI描述,是不可变的(Immutable)。它们就像蓝图,配置了Element和RenderObject应该如何被创建和更新。
- 特点:轻量级、不可变。当状态改变时,整个 Widget 树会重建,但这不是性能问题,因为重建的是“蓝图”本身,而不是底层的渲染实体。
- 例子:
Container,Text,Row,Column等。
Element 树:
- 是什么:Element 是 Widget 在 UI 树中的实例化对象。它负责管理 Widget 的生命周期,并将 Widget 的配置信息与底层的 RenderObject 连接起来。它是 Widget 和 RenderObject 之间的“粘合剂”。
- 特点:可变的、稳定的。当 Widget 树重建时,Flutter 会使用
diff算法对比新旧 Widget,然后尽可能地复用已有的 Element,只更新发生变化的 Element。这是 Flutter 高效的关键之一。 - 工作方式:
Element会检查新的 Widget 是否与旧的 Widget 是同一类型(runtimeType和key相同)。如果是,则更新现有的 Element 和 RenderObject;如果不是,则销毁旧的并创建新的。
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 树重建)会经历以下阶段:
动画(Animate):
- 渲染管线首先处理所有的动画(
Ticker)。动画的每一帧都会触发一次新的构建和渲染。
- 渲染管线首先处理所有的动画(
构建(Build):
setState()被调用,标记了某个 State 为“脏”。- Flutter 会重新执行
build方法,生成新的 Widget 树。 - Flutter 会将新的 Widget 树与旧的 Widget 树进行对比(Diff),并相应地更新 Element 树(复用、更新、创建或销毁 Element)。
- 注意:这个阶段只更新 Element 和 RenderObject 的配置,不进行布局和绘制。
布局(Layout):
- 布局过程是一个从根
RenderObject开始的递归过程。 - 约束向下传递(Constraints go down):父节点向子节点传递布局约束(例如,最大/最小宽度/高度)。
- 尺寸向上传递(Sizes go up):子节点根据约束决定自己的尺寸,然后告诉父节点。
- 如果一个 RenderObject 在构建阶段被标记为“需要布局”(dirty),或者它的约束发生了变化,它就会重新执行
layout方法。 - 布局完成后,每个 RenderObject 都拥有了一个确定的位置和大小。
- 布局过程是一个从根
绘制(Paint):
- 绘制过程也是一个递归过程,但顺序通常是自上而下。
- 如果一个 RenderObject 在布局阶段被标记为“需要绘制”(dirty),它就会重新执行
paint方法。 paint方法会接收一个Canvas对象,RenderObject 在这个画布上调用绘图指令(例如,画矩形、写文字、显示图片)来绘制自己。- 绘制过程会生成一个或多个
Layer(图层)。这些图层最终会被合成(Composited)到屏幕上。
合成(Compositing):
- 这是渲染管线的最后一步。Flutter 的渲染引擎(Skia)会将所有由
paint方法生成的图层合成为一张完整的画面。 - 对于需要硬件加速的图形操作(如变换、透明度),Flutter 会使用不同的图层来处理,以提高效率。
- 这是渲染管线的最后一步。Flutter 的渲染引擎(Skia)会将所有由
为什么这种设计是高效的?
声明式UI与响应式编程:你只需要描述“当前状态下的UI应该是什么样子”,而不需要关心如何从状态A更新到状态B。框架(Flutter)会自动、高效地帮你完成更新。
逻辑与渲染分离:轻量级的 Widget 负责业务逻辑和配置,重量级的 RenderObject 负责昂贵的布局和绘制。两者通过稳定的 Element 树连接,实现了高效的更新。
精细的重建:通过 Diff 算法和“脏”标记机制,Flutter 能够将 UI 变化的影响范围降到最低。改变一个文本,通常不会导致整个页面重新布局和绘制。
绕过原生控件,直接渲染:与 React Native 等框架不同,Flutter 不依赖平台的原生控件。它自己管理 RenderObject 树,并通过 Skia 引擎直接向画布(Canvas)绘制。这消除了桥接(Bridge)带来的性能开销,并保证了 UI 在不同平台上的一致性和高性能。
Flutter 的渲染原理可以概括为:
开发者编写不可变的 Widget -> Flutter 通过 Diff 算法更新可复用的 Element -> Element 驱动持有布局和绘制信息的 RenderObject -> RenderObject 通过 layout 和 paint 方法,利用 Skia 引擎直接在画布上绘制出最终界面。
这套机制确保了 Flutter 能够以每秒 60 帧(甚至 120 帧)的流畅度运行,并提供极其灵活和强大的 UI 构建能力。
setState流程
buildContext作用
Widget、Element、RenderObject
热重载实现原理
skia渲染逻辑
flutter状态管理
getx
GetBuilder Obx
在 Flutter 的 GetX 插件中,GetBuilder 和 Obx 是两种最常用的状态管理刷新机制。它们的核心区别在于:一个是手动触发的被动式管理,另一个是自动追踪的响应式管理。
1. GetBuilder:手动刷新机制
GetBuilder 属于“简单状态管理”。它的运行逻辑类似于原生的 ChangeNotifier。
- 工作原理:
- 你需要在
GetxController中定义普通的变量。 - 当数据改变后,必须手动调用
update()方法。 - 调用
update()后,GetX 会遍历所有监听该控制器的GetBuilder组件,并触发它们的rebuild(重新构建)。
- 特点:
- 低内存占用:它不使用 Dart 的
Stream或观察者模式,只是简单的回调通知,因此性能最高,开销最小。 - 非响应式:即使变量值没变,只要调用了
update(),对应的GetBuilder就会刷新。 - 适用场景:大型列表更新、对性能要求极高的界面、或多个变量改变后只需一次刷新的逻辑。
2. Obx:响应式刷新机制
Obx(及 GetX 组件)属于“响应式状态管理”。它基于观察者模式。
- 工作原理:
- 定义响应式变量:使用
.obs声明变量(例如var count = 0.obs;),这会将变量包装成一个Rx对象。 - 自动订阅:当
Obx内部的代码块读取该变量的值时(调用了其value),Obx会自动将自己注册为该变量的观察者。 - 自动触发:一旦该响应式变量的值发生变化(新旧值不相等),它会立即通知所有订阅了自己的
Obx组件进行局部刷新。
特点:
无需手动刷新:开发者不需要写
update()。精准颗粒度:只有真正用到该变量的
Obx才会刷新,且只有值变动时才刷新。内存开销稍大:因为每个
.obs变量都是一个流(Stream),在处理极其大量的响应式变量时,内存消耗会比GetBuilder高。适用场景:表单实时验证、用户信息同步、逻辑较简单的快速开发。
核心对比总结
| 特性 | GetBuilder | Obx |
|---|---|---|
| 状态类型 | 普通变量 (int, String...) | 响应式变量 (RxInt, RxString...) |
| 刷新方式 | 手动调用 update() | 自动检测变量 value 变化 |
| 性能开销 | 极低(类似方法回调) | 稍高(基于观察者/流) |
| 颗粒度 | 依赖 update() 指定的 ID | 自动识别代码中使用的变量 |
| 上手难度 | 略繁琐(需手动维护) | 极简(随写随用) |
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创建的任务,默认都会被加入到事件队列。
代码示例与执行顺序
void main() {
print('1. main start');
// 事件队列
Future(() => print('3. Event Queue Task'));
// 微任务队列
scheduleMicrotask(() => print('4. Microtask 1'));
// 通过 Future 创建的微任务
Future.microtask(() => print('5. Microtask 2'));
// 立即执行的 Future(仍然会在当前事件之后)
Future(() => print('6. Immediate Event Queue Task'))
.then((_) => print('7. Then callback'));
print('2. main end');
// 事件循环启动,开始处理队列中的任务
}输出顺序:
1. main start
2. main end
4. Microtask 1
5. Microtask 2
3. Event Queue Task
6. Immediate Event Queue Task
7. Then callback关键点:
then和await之后的代码,其回调会被包装成微任务,因此执行优先级高于普通事件。- 永远不要在主线程执行耗时操作(如大量计算、同步网络请求)。这会阻塞事件循环,导致页面卡顿甚至应用无响应(ANR)。耗时任务应使用 Isolate 在后台执行。
Dart 使用一种分代垃圾回收(Generational Garbage Collection) 机制,这与 Java, C# 等语言类似。它的设计目标是实现低延迟,避免因GC导致UI卡顿。
分代假说
GC 基于两个“假说”:
- 弱分代假说:绝大多数对象都是“朝生夕死”的(例如,在
build方法中创建的临时对象)。 - 强分代假说:存活得越久的对象,越不可能被回收。
基于此,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 或其他大型对象的引用,这可能导致它们无法被回收。
- 避免创建大量小对象:虽然在新生代GC很快,但频繁触发GC和对象晋升也会带来开销。在
总结:三者如何协同工作
- 你编写代码:包括同步代码和异步代码(
Future,async/await)。 - 任务入队:你的异步任务被分配到微任务队列或事件队列中。
- 事件循环驱动:事件循环 按优先级(微任务 -> 事件)从队列中取出任务执行。
- 内存分配与回收:任务执行过程中创建的对象在堆内存中分配。Dart VM 的垃圾回收器会默默地在后台工作,自动回收不再使用的内存,确保应用不会因内存泄漏而崩溃。
- 核心目标:这套机制的最终目标是确保 UI 线程的流畅。通过高效的异步模型和低延迟的GC,Flutter 能够实现 60fps/120fps 的丝滑用户体验。
业务
ios开发者账号维护
打包/上架
平常是怎么打包的?如果我一个原生的app里面引入了flutter的话我应该怎么打包?
支付
支付踩坑
购买漏包如何解决?

