【面试】Flutter面试题集合
Navigator 是在 Flutter 中负责管理维护页面堆栈的导航器。MaterialApp 在需要的时候,会自动为我们创建Navigator。,会使用context来向上遍历Element树,找到 MaterialApp 提供的再调用其push/pop方法完成导航操作。
Flutter 中 Widget 的生命周期




Dart的一些重要概念?
- 面向对象:在Dart中,一切都是对象,所有的对象都是继承自
Object - Dart是强类型语言,但可以用
var或dynamic来声明一个变量,Dart会自动推断其数据类型,var实际上是编译期的“语法糖”。dynamic表示动态类型, 被编译后,实际是一个object类型,在编译期间不进行任何的类型检查,而是在运行期进行类型检查。 - 空类型安全,使用类型后面+
?定义可空类型,一个变量只要定义时是不可空的,那么在运行期就一定能保证是非空的,由编译器保证 - Dart支持顶层方法,如
main方法,可以在方法内部创建方法 - Dart支持顶层变量,也支持类变量或对象变量
- Dart没有 public protected private 等关键字,如果某个变量以下划线(
_)开头,代表这个变量在库中是私有的
Dart 是值传递还是引用传递?
- Dart是面向对象的编程语言,传递效果和Java一样,dart是值传递。我们每次调用函数,传递过去的都是对象的内存地址,而不是这个对象的复制。
- 首先分清楚Java中的值传递和引用传递,对于基本数据类型,实参传给形参的是变量的副本,互不干扰,对于非基本类型的引用对象,作为实参传给形参是传递的引用对象的地址值的副本,此时实参和形参的地址值指向同一个引用对象的堆内存地址,因此形参修改了指向的引用对象的内容,也就是修改了实参所指向的对象内容。Dart一切都是对象,因此Dart也是传递的对象的地址值。
Widget 和 element 和 RenderObject 之间的关系?
- Widget 是用户界面的一部分,并且是不可变的。
- Element 是在树中特定位置 Widget 的实例。
- RenderObject 是渲染树中的一个对象,它的层次结构是渲染库的核心。
它们对应 3 棵树,Widget 和 Element 是一一对应,Element 和 RenderObject 不是一一对应。
Widget 会被 inflate(填充)到 Element,并由 Element 管理底层渲染树。Widget 并不会直接管理状态及渲染,而是通过 State 这个对象来管理状态。Flutter 创建 Element 的可见树,相对于 Widget 来说,是可变的。通常在界面开发中,我们不直接操作 Element,而是由框架层实现内部逻辑。例如,在一个 UI 视图树中,可能包含多个 TextWidget(Widget 被使用多次),但是从内部视图树的视角看,这些 TextWidget 都被填充到各自独立的 Element 中。Element 会持有 renderObject 和 widget 的实例。请记住,Widget 只是一个配置,而 RenderObject 负责管理布局、绘制等操作。
在第一次创建 Widget 时,会相应地创建一个 Element,然后将该元素插入树中。如果之后 Widget 发生变化,系统会将其与旧的 Widget 进行比较,并相应地更新 Element。重要的是,Element 不会被重建,只是更新其状态。
Flutter 中的 Widget、State、Context 的核心概念?是为了解决什么问题?
- Widget: 在Flutter中,几乎所有东西都是
Widget。将一个Widget想象为一个可视化的组件(或与应用可视化方面交互的组件),当你需要构建与布局直接或间接相关的任何内容时,你正在使用Widget。 - Widget树: Widget 以树结构进行组织。包含其他 Widget 的 widget 被称为父 Widget (或 widget 容器)。包含在父 widget 中的 widget 被称为子 Widget。
- Context: 在
StatefulWidget和StatelessWidget中会分别创建对应的StatefulElement和StatelessElement,在这两个Element中会调用Widget build() => state.build(this);和Widget build() => widget.build(this);方法,因此widget的build方法中的Context参数其实是对应的Element对象本身,它是Context的实现类 - State: 定义了
StatefulWidget实例的行为,它包含了用于”交互/干预“Widget信息的行为和布局。应用于State的任何更改都会强制重建Widget。
这些状态的引入,主要是为了解决多个部件之间的交互和部件自身状态的维护。
一个Widget内部会创建一个Element对象,Element对象本身又会作为Context参数传给Widget的build函数,因此Widget、Element和Context之间是一一对应的关系。
mixin extends implement 之间的关系?
将类 A mixins 到 B,B 可以使用 A 的属性和方法,B 就具备了 A 的功能,但是需要强调的是:
- mixins 的对象是类
- mixins 绝不是继承,也不是接口,而是一种全新的特性
- 可以mixins多个类
- mixins的使用需要满足一定条件
mixins 要用到的关键字 with
怎么来理解 with 呢?很简单:
- 继承 -> extends
- mixins -> with
继承和 mixins 是一样的,是语言的特性,with 和 extends 是关键字。
继承(关键字 extends)、混入 mixins (关键字 with)、接口实现(关键字 implements)。这三者可以同时存在,前后顺序是 extends -> mixins -> implements。
Flutter 中的继承是单继承,子类重写超类的方法要用@Override,子类调用超类的方法要用super。
mixin类似多继承但又不同于多继承,Dart自创大法,以多继承的方式使用多个类中的重复代码。
使用mixins的条件是什么?
因为mixins使用的条件,随着Dart版本一直在变,这里讲的是Dart2.1中使用mixins的条件:
- mixins 类只能继承自 object
- mixins 类不能有构造函数
- 一个类可以 mixins 多个 mixins 类,不破坏 Flutter 的单继承
mixin 怎么指定异常类型?
on关键字可用于指定异常类型。 on只能用于被mixins标记的类,例如mixins X on A,意思是要mixins X的话,得先接口实现或者继承A。这里A可以是类,也可以是接口,但是在mixins的时候用法有区别。
on 一个类:
class A {
void a(){
print("a");
}
}
mixin X on A {
void x(){
print("x");
}
}
class mixinsX extends A with X {
}
on 的是一个接口: 得首先实现这个接口,然后再用mixin
class A {
void a(){
print("a");
}
}
mixin X on A {
void x(){
print("x");
}
}
class implA implements A {
void a() {}
}
class mixinsX2 extends implA with X {
}
接口实现 (implements)
Flutter是没有interface的,但是Flutter中的每个类都是一个隐式的接口,这个接口包含类里的所有成员变量,以及定义的方法。
如果有一个类 A,你想让类 B 拥有 A 的API,但又不想拥有 A 里的实现,那么你就应该把 A 当做接口,类B implements 类 A。
所以在Flutter中:
class就是interface- 当
class被当做interface用时,class中的方法就是接口的方法,需要在子类里重新实现,在子类实现的时候要加@override - 当
class被当做interface用时,class中的成员变量也需要在子类里重新实现。在成员变量前加@override - 实现接口可以有多个
感觉 Dart 中的接口实现就是拿骡子当马用。。
Flutter main future mirotask 的执行顺序?
普通代码都是同步执行的,结束后会开始检查microtask中是否有任务,若有则执行,执行完继续检查microtask,直到microtask列队为空。最后会去执行event队列(future)。
Future 和 Isolate 有什么区别?
future 是异步编程,调用本身立即返回,并在稍后的某个时候执行完成时再获得返回结果。在普通代码中可以使用 await 等待一个异步调用结束。
isolate是并发编程,Dart没有并发时的共享状态,所有Dart代码都在isolate中运行,包括最初的main()。每个isolate都有它自己的堆内存,意味着其中所有内存数据,包括全局数据,都仅对该isolate可见,它们之间的通信只能通过传递消息的机制完成,消息则通过端口(port)收发。isolate只是一个概念,具体取决于如何实现,比如在 Dart VM 中一个 isolate 可能会是一个线程,在Web中可能会是一个Web Worker。
Stream 与 Future是什么关系?
Stream 和 Future 是 Dart 异步处理的核心 API。
Future 表示稍后获得的一个数据,所有异步的操作的返回值都用 Future 来表示。但是 Future 只能表示一次异步获得的数据。
而 Stream 表示多次异步获得的数据。比如界面上的按钮可能会被用户点击多次,所以按钮上的点击事件(onClick)就是一个 Stream 。
简单地说,Future 将返回一个值,而 Stream 将返回多次值。Dart 中统一使用 Stream 处理异步事件流。Stream 和一般的集合类似,都是一组数据,只不过一个是异步推送,一个是同步拉取。
Stream 两种订阅模式?
Stream有两种订阅模式:单订阅(single) 和 多订阅(broadcast)。
单订阅就是只能有一个订阅者,而广播是可以有多个订阅者。这就有点类似于消息服务(Message Service)的处理模式。单订阅类似于点对点,在订阅者出现之前会持有数据,在订阅者出现之后就才转交给它。而广播类似于发布订阅模式,可以同时有多个订阅者,当有数据时就会传递给所有的订阅者,而不管当前是否已有订阅者存在。
Stream 默认处于单订阅模式,所以同一个 stream 上的 listen 和其它大多数方法只能调用一次,调用第二次就会报错。但 Stream 可以通过 transform() 方法(返回另一个 Stream)进行连续调用。通过 Stream.asBroadcastStream() 可以将一个单订阅模式的 Stream 转换成一个多订阅模式的 Stream,isBroadcast 属性可以判断当前 Stream 所处的模式。
Widget的两种类型是什么?
- StatelessWidget: 一旦创建就不关心任何变化,在下次构建之前都不会改变。它们除了依赖于自身的配置信息(在父节点构建时提供)外不再依赖于任何其他信息。比如典型的
Text、Row、Column、Container等,都是StatelessWidget。它的生命周期相当简单:初始化、通过build()渲染。 - StatefulWidget: 在生命周期内,该类Widget所持有的数据可能会发生变化,这样的数据被称为
State,这些拥有动态内部数据的Widget被称为StatefulWidget。比如复选框、Button等。State会与Context相关联,并且此关联是永久性的,State对象将永远不会改变其Context,即使可以在树结构周围移动,也仍将与该context相关联。当state与context关联时,state被视为已挂载。StatefulWidget由两部分组成,在初始化时必须要在createState()时初始化一个与之相关的State对象。
State 对象的初始化流程?
- initState() : 一旦
State对象被创建,initState方法是第一个(构造函数之后)被调用的方法。可通过重写来执行额外的初始化,如初始化动画、控制器等。重写该方法时,应该首先调用super.initState()。在initState中,无法真正使用context,因为框架还没有完全将其与state关联。initState在该State对象的生命周期内将不会再次调用。 - didChangeDependencies(): 这是第二个被调用的方法。在这一阶段,
context已经可用。如果你的Widget链接到了一个InheritedWidget并且/或者你需要初始化一些listeners(基于context),通常会重写该方法。 - build(BuildContext context): 此方法在
didChangeDependencies()、didUpdateWidget()之后被调用。每次State对象更新(或当InheritedWidget有新的通知时)都会调用该方法!我们一般都在build中来编写真正的功能代码。为了强制重建,可以在需要的时候调用setState((){...})方法。 - dispose(): 此方法在Widget被移除时调用。可重写该方法来执行一些清理操作(如解除listeners),并在此之后立即调用
super.dispose()。
Widget 唯一标识Key有那几种?
在flutter中,每个widget都是被唯一标识的。这个唯一标识在build或rendering阶段由框架定义。该标识对应于可选的Key参数,如果省略,Flutter将会自动生成一个。
在flutter中,主要有两种类型的Key:
- GlobalKey(确保生成的Key在整个应用中唯一,是很昂贵的,允许element在树周围移动或变更父节点而不会丢失状态)
- LocalKey,LocalKey 包括:ValueKey、UniqueKey、ObjectKey
什么是Navigator? MaterialApp做了什么?
Navigator 是在 Flutter 中负责管理维护页面堆栈的导航器。MaterialApp 在需要的时候,会自动为我们创建Navigator。Navigator.of(context),会使用context来向上遍历Element树,找到 MaterialApp 提供的_NavigatorState再调用其push/pop方法完成导航操作。
widget 树的 root 节点
- Flutter 应用的根组件是一个 RenderView,它的默认大小是当前设备屏幕大小。
renderView是一个RenderObject,它是渲染树的根,而renderViewElement是renderView对应的Element对象。
main() --> runApp -->
WidgetsFlutterBinding.ensureInitialized()
..scheduleAttachRootWidget(app)
..scheduleWarmUpFrame();
其中 WidgetsFlutterBinding 混入了 RenderBinding -> initInstances() 初始化--> initRenderView() 创建 RenderView作为整个渲染树的根节点,传入的app随后被挂载的RenderView上面,当下一帧vsync信号来临时开始渲染。
dart 中 … 是什么
级联符号 .. 可以让你连续操作相同的对象,不单可以连续地调用函数,还可以连续地访问方法,这样做可以避免创建临时变量,从而写出更流畅的代码。
Zone
Dart 中可通过 Zone 表示指定代码执行的环境,类似一个沙盒概念,在 Flutter 中 C++ 运行 Dart 也是在 _runMainZoned 内执行 runZoned 方法启动,而我们也可以通过 Zone ,在运行环境内捕获全局异常等信息:
runZoned(() {
runApp(FlutterReduxApp());
}, onError: (Object obj, StackTrace stack) {
print(obj);
print(stack);
});
同时你可以给 runZoned 注册方法,在需要时执行回调,如下代码所示,这样的在一个 Zone 内任何地方,只要能获取 onData 这个 ZoneUnaryCallback,就都可以调用到 handleData
///最终需要处理的地方
handleData(result) {
print("VVVVVVVVVVVVVVVVVVVVVVVVVVV");
print(result);
}
///返回得到一个 ZoneUnaryCallback
var onData = Zone.current.registerUnaryCallback<dynamic, int>(handleData);
///执行 ZoneUnaryCallback 返回数据
Zone.current.runUnary(onData, 2);
// 异步逻辑可以通过 scheduleMicrotask 可以插入异步执行方法:
Zone.current.scheduleMicrotask((){
//todo something
});
Future
Future 简单了说就是对 Zone 的封装使用。
比如 Future.microtask 中主要是执行了 Zone 的 scheduleMicrotask ,而 result._complete 最后调用的是 _zone.runUnary 等等。
factory Future.microtask(FutureOr<T> computation()) {
_Future<T> result = new _Future<T>();
scheduleMicrotask(() {
try {
result._complete(computation());
} catch (e, s) {
_completeWithErrorCallback(result, e, s);
}
});
return result;
}
Dart 中可通过 async/await 或者 Future 定义异步操作,而事实上 async/await 也只是语法糖,最终还是通过编译器转为 Future。
Stream
Stream 也是有对 Zone 的另外一种封装使用。
Dart 中另外一种异步操作, async* / yield 或者 Stream 可定义 Stream 异步, async* / yield 也只是语法糖,最终还是通过编译器转为 Stream。 Stream 还支持同步操作。
1)、Stream 中主要有 Stream 、 StreamController 、StreamSink 和 StreamSubscription 四个关键对象,大致可以总结为:
StreamController:如类名描述,用于整个 Stream 过程的控制,提供各类接口用于创建各种事件流。StreamSink:一般作为事件的入口,提供如add,addStream等。Stream:事件源本身,一般可用于监听事件或者对事件进行转换,如listen、where。StreamSubscription:事件订阅后的对象,表面上用于管理订阅过等各类操作,如cancel、pause,同时在内部也是事件的中转关键。
2)、一般通过 StreamController 创建 Stream;通过 StreamSink 添加事件;通过 Stream 监听事件;通过 StreamSubscription 管理订阅。
3)、Stream 中支持各种变化,比如 map 、expand 、where 、take 等操作,同时支持转换为 Future 。
Dart 单线程模型

Dart 在单线程中是以消息循环机制来运行的,其中包含两个任务队列,一个是“微任务队列” microtask queue,另一个叫做“事件队列” event queue。从图中可以发现,微任务队列的执行优先级高于事件队列。
现在我们来介绍一下Dart线程运行过程,如上图中所示,入口函数 main() 执行完后,消息循环机制便启动了。首先会按照先进先出的顺序逐个执行微任务队列中的任务,事件任务执行完毕后程序便会退出,但是,在事件任务执行的过程中也可以插入新的微任务和事件任务,在这种情况下,整个线程的执行过程便是一直在循环,不会退出,而 Flutter 中,主线程的执行过程正是如此,永不终止。
在Dart中,所有的外部事件任务都在事件队列中,如 IO、计时器、点击、以及绘制事件等,而微任务通常来源于Dart内部,并且微任务非常少,之所以如此,是因为微任务队列优先级高,如果微任务太多,执行时间总和就越久,事件队列任务的延迟也就越久,对于GUI应用来说最直观的表现就是比较卡,所以必须得保证微任务队列不会太长。
值得注意的是,我们可以通过 Future.microtask(…) 方法向微任务队列插入一个任务。
微任务队列有点类似于 Android 中 Choreographer 提交绘制任务之前发送的同步屏障消息(阻塞普通消息,优先执行异步消息),它们都是为系统的某些UI任务执行提供绿色通道,提高更高优先级的执行。
在事件循环中,当某个任务发生异常并没有被捕获时,程序并不会退出,而直接导致的结果是当前任务的后续代码就不会被执行了,也就是说一个任务中的异常是不会影响其他任务执行的。
Dart 多任务如何并行的?
刚才也说了,既然 Dart 不存在多线程,那如何进行多任务并行?
- Dart 当中提供了一个 类似于新线程,但是不共享内存的独立运行的 worker - isolate。
那他们是如何交互的?
- 在dart中,一个
isolate对象其实就是一个isolate执行环境的引用,一般来说我们都是通过当前的isolate去控制其他的isolate完成彼此之间的交互,而当我们想要创建一个新的Isolate可以使用Isolate.spawn方法获取返回的一个新的isolate对象,两个isolate之间使用SendPort相互发送消息,而isolate中也存在了一个与之对应的ReceivePort接受消息用来处理,但是我们需要注意的是,ReceivePort和SendPort在每个isolate都有一对,只有同一个isolate中的ReceivePort才能接受到当前类的SendPort发送的消息并且处理。
Flutter与Native的通信机制
- 应用中的 Flutter 部分通过 Platform Channel 向其宿主 (非 Dart 部分) 发送消息。
- 宿主监听 Platform Channel 并接收消息。然后,它使用原生编程语言来调用任意数量的相关平台 API,并将响应发送回 Flutter 。
Flutter 端在调用方法的时候 MethodChannel 会负责响应,从平台一侧来讲,Android 系统上使用 MethodChannelAndroid、 iOS 系统使用 MethodChanneliOS 来接收和返回来自 MethodChannel 的方法调用。
Platform Channel 的三种分类:

这三种方式,无论是传递方法还是事件,本质上都是传递的数据。
具体使用哪种通信方式,要看使用场景,同一种功能可以通过不同的方式来实现。比如获取手机电量,网络变化等可以通过 EventChannel,也能用 MethodChannel。比如Flutter调用Native拍照可以用MethodChannel。 如果要通过Flutter控制一个原生的视频播放组件,则最好通过MethodChannel;如果要视频播放期间获取播放进度改变、当前网速变化等信息,则最好通过EventChannel。
同时 Platform Channel 并非是线程安全的 。
在 Flutter 中嵌入原生 Android View
Platform view 允许将原生视图嵌入到 Flutter 应用中,所以您可以通过 Dart 将变换、裁剪和不透明度等效果应用到原生视图。
Flutter 支持两种集成模式:虚拟显示模式 (Virtual displays) 和混合集成模式 (Hybrid composition) 。
我们应根据具体情况来决定使用哪种模式:
-
混合集成模式:会将原生的
android.view.View附加到视图层次结构中。因此,键盘处理和无障碍功能是开箱即用的。在 Android 10 之前,此模式可能会大大降低 Flutter UI 的帧吞吐量 (FPS)。minSdkVersion最低为19 -
虚拟显示模式 会将
android.view.View实例渲染为纹理,因此它不会嵌入到 Android Activity 的视图层次结构中。某些平台交互(例如键盘处理和辅助功能)可能无法正常工作。minSdkVersion最低为20。
Flutter 中通过 PlatformView 可以嵌套原生 View 到 Flutter UI 中,这里面其实是使用了 Presentation + VirtualDisplay + Surface 等实现的,大致原理就是:
- 使用了类似副屏显示的技术,VirtualDisplay 类代表一个虚拟显示器,调用 DisplayManager 的 createVirtualDisplay() 方法,将虚拟显示器的内容渲染在一个 Surface 控件上,然后将 Surface 的 id 通知给 Dart,让 engine 绘制时,在内存中找到对应的 Surface 画面内存数据,然后绘制出来。
Flutter 和 React Native 区别
Flutter 和 React Native 不同主要在于 Flutter UI 是直接通过 skia 渲染的 ,而 React Native 是将 js 中的控件转化为原生控件,通过原生去渲染的。
一些基础概念
- Flutter 中 Widget 不可变,每次保持在一帧,如果发生改变是通过 State 实现跨帧状态保存,而真实完成布局和绘制数组的是 RenderObject ,Element 充当两者的桥梁, State 就是保存在 Element 中。
- Flutter 中的 BuildContext 只是接口,而 Element 实现了它。
- Flutter 中 setState 其实是调用了 markNeedsBuild ,该方法内部标记此 Element 为 Dirty ,然后在下一帧 WidgetsBinding.drawFrame 才会被绘制,这可以看出 setState 并不是立即生效的。
- Flutter 中 RenderObject 在 attch/layout 之后会通过 markNeedsPaint(); 使得页面重绘。
- 当 RenderObject 的 isRepaintBoundary 为 true 时,那么个区域形成一个 Layer,所以不是每个 RenderObject 都具有 Layer 的,因为这受 isRepaintBoundary 的影响。
- 正常情况 RenderObject 的布局相关方法调用顺序是 : layout ->performResize ->performLayout ->markNeedsPaint , 但是用户一般不会直接调用 layout,而是通过 markNeedsLayout
- Flutter 中一般 json 数据从 String 转为 Object 的过程中都需要先经过 Map 类型。
- Flutter 中 InheritedWidget 一般用于状态共享,如
Theme 、Localizations 、 MediaQuery等,都是通过它实现共享状态,这样我们可以通过context去获取共享的状态,比如ThemeData theme = Theme.of(context); - 在 Element 的 inheritFromWidgetOfExactType 方法实现里,有一个
Map<Type, InheritedElement> _inheritedWidgets的对象。_inheritedWidgets一般情况下是空的,只有当父控件是InheritedWidget或者本身是InheritedWidgets时才会有被初始化,而当父控件是InheritedWidget时,这个Map会被一级一级往下传递与合并 。所以当我们通过context调用inheritFromWidgetOfExactType时,就可以往上查找到父控件的Widget。
Flutter 中默认主要通过 runtimeType 和 key 判断更新:
static bool canUpdate(Widget oldWidget, Widget newWidget) {
return oldWidget.runtimeType == newWidget.runtimeType
&& oldWidget.key == newWidget.key;
}
}
Flutter 的 Debug 下是 JIT 模式,release下是 AOT 模式。
- 在应用程序开发过程中使用JIT编译,因为它可以通过动态编译代码来实现热重新加载和快速开发周期。
- 完成开发并准备发布后,将使用AOT编译。然后将代码AOT编译为本机代码,从而实现应用程序的快速启动和高性能执行。Dart 是极少数可以同时编译 AOT 和 JIT 的语言之一。
Flutter 中可以通过 mixins AutomaticKeepAliveClientMixin ,然后重写 wantKeepAlive 保持住页面,记得在被保持住的页面 build 中调用 super.build 。(因为 mixins 特性)。
Flutter 手势事件主要是通过竞技判断的:
- 主要有 hitTest 把所有需要处理的控件对应的 RenderObject , 从 child 到 parent 全部组合成列表,从最里面一直添加到最外层。
- 然后从队列头的 child 开始 for 循环执行 handleEvent 方法,执行 handleEvent 的过程不会被拦截打断。
- 一般情况下 Down 事件不会决出胜利者,大部分时候是在 MOVE 或者 UP 的时候才会决出胜利者。
- 竞技场关闭时只有一个的就直接胜出响应,没有胜利者就拿排在队列第一个强制胜利响应。
- 同时还有
didExceedDeadline处理按住时的 Down 事件额外处理,同时手势处理一般在GestureRecognizer的子类进行。
Flutter 中 ListView 滑动其实都是通过改变 ViewPort 中的 child 布局来实现显示的。
更多推荐


所有评论(0)