公告

微信

欢迎大家私信交流

Skip to content

面试

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。在以下情况会触发:
    • initStatedidChangeDependencies 调用后。
    • 手动调用 setState
    • 父组件触发 didUpdateWidget 后。
  • didUpdateWidget: 当父组件重建,且新旧 Widget 的 runtimeTypekey 相同时调用。常用于在配置变化时更新 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框架的基石。

  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

GetBuilder Obx

在 Flutter 的 GetX 插件中,GetBuilderObx 是两种最常用的状态管理刷新机制。它们的核心区别在于:一个是手动触发的被动式管理,另一个是自动追踪的响应式管理。

1. GetBuilder:手动刷新机制

GetBuilder 属于“简单状态管理”。它的运行逻辑类似于原生的 ChangeNotifier

  • 工作原理
  1. 你需要在 GetxController 中定义普通的变量。
  2. 当数据改变后,必须手动调用 update() 方法。
  3. 调用 update() 后,GetX 会遍历所有监听该控制器的 GetBuilder 组件,并触发它们的 rebuild(重新构建)。
  • 特点
  • 低内存占用:它不使用 Dart 的 Stream 或观察者模式,只是简单的回调通知,因此性能最高,开销最小。
  • 非响应式:即使变量值没变,只要调用了 update(),对应的 GetBuilder 就会刷新。
  • 适用场景:大型列表更新、对性能要求极高的界面、或多个变量改变后只需一次刷新的逻辑。
2. Obx:响应式刷新机制

Obx(及 GetX 组件)属于“响应式状态管理”。它基于观察者模式。

  • 工作原理
  1. 定义响应式变量:使用 .obs 声明变量(例如 var count = 0.obs;),这会将变量包装成一个 Rx 对象。
  2. 自动订阅:当 Obx 内部的代码块读取该变量的值时(调用了其 value),Obx 会自动将自己注册为该变量的观察者。
  3. 自动触发:一旦该响应式变量的值发生变化(新旧值不相等),它会立即通知所有订阅了自己的 Obx 组件进行局部刷新。
  • 特点

  • 无需手动刷新:开发者不需要写 update()

  • 精准颗粒度:只有真正用到该变量的 Obx 才会刷新,且只有值变动时才刷新。

  • 内存开销稍大:因为每个 .obs 变量都是一个流(Stream),在处理极其大量的响应式变量时,内存消耗会比 GetBuilder 高。

  • 适用场景:表单实时验证、用户信息同步、逻辑较简单的快速开发。

核心对比总结
特性GetBuilderObx
状态类型普通变量 (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 创建的任务,默认都会被加入到事件队列。

代码示例与执行顺序

dart
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

关键点:

  • 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

其他面试题

一些面试题

上次更新于: