Flutter面试通关秘籍

566 阅读31分钟

前言

如何快速高效的掌握一门学问,建议先阅读下这篇文章关于学习的一些看法

码字不易,记得关注 + 点赞 + 收藏

一、Dart语言基础

1、Dart是什么类型的编程语言?

Dart是一种面向对象的、强类型的、静态类型的编程语言,由Google开发。 它最初是为了改善Web开发体验而设计的,现在广泛用于移动应用开发,通过Flutter框架来构建AndroidiOS应用。

Dart语言的主要特性:

  • 面向对象
    • 支持类、对象、继承、抽象类、接口、混入(mixins)等面向对象编程的特性。
  • 强类型
    • 一种类型检查机制。
    • 要求变量的类型在编译时必须明确指定,程序运行中保持不变。
    • 如需进行类型转换时,必须是显式类型转换。
  • 静态类型
    • 一种类型检查机制。
    • 变量的类型在编译时检查完成。
    • 隐式声明的变量,编译器通过类型推断来确定其类型。
  • 函数式编程
    • 是一等公民,可作为值传递
    • 支持lambda表达式闭包
  • 异步与并发
    • 支持异步与并发编程。
    • 异步使用FutureStream来处理。
    • 并发使用isolate来处理。
  • AOT与JIT编译
    • AOT编译生成机器码,适用于部署
    • JIT编译在运行时动态编译代码,适用于开发环境

2、Dart变量的声明方式有哪些?

  • var
    • 用于声明变量,属于隐式声明
    • 编译时类型检查,编译器自动推断其类型。
    • 变量初始化后其类型不能改变。
    • 使用场景:声明变量时能确定其类型。
  • dynamic
    • 一个特殊类型,其声明的变量可以存储任何类型的数据
    • 运行时类型检查,编译时不检查类型。
    • 使用场景:声明变量时能不确定其类型且需要在运行时动态改变类型。
  • final
    • 用于声明不可变的变量。
    • 初始化后其值不可重新赋值
    • 延迟初始化,可运行时初始化。
  • const
    • 用于声明编译时常量
    • 声明变量的类型在编译时就能确定。
    • 隐式声明时编译器可推断其类型。
  • late
    • 用于声明非空变量
    • 延迟初始化。
    • 远行时检查,可绕过编译时检查,若在未初始化前访问,会抛出一个LateError异常。
  • 显式声明
    • 变量名前加上类型名称。

3、Dart中基本数据类型有哪些?

  • 数值类型 (num):
    • num: 通用数值类型,其子类有intdouble
    • int:整数类型,值范围不限,受内存的大小限制。
    • double:浮点数类型,值范围不限,受内存的大小限制。
  • 布尔类型 (bool):
    • bool:值为truefalse
  • 字符串类型 (String):
    • String:用于存储文本的类型,字符串是不可变的,创建后不可更改。
  • 动态类型 (dynamic):
    • dynamic:当不确定变量类型时使用,其声明的变量在运行时可以是任何类型。
  • 空类型及可空类型 (Null、Nullable):
    • null:表示值的缺失,任何非Never类型都可赋值为null
    • 可空类型:dart2引入,用问号?后缀表示,如int?
  • 集合类型 (List、Set、Map):
    • dynamic:当不确定变量类型时使用,其声明的变量在运行时可以是任何类型。
  • 符号类型 (Symbol):
    • Symbol:表示标识符的类型,常用于编译时的元编程。
  • 函数类型 (Function):
    • Function:函数类型,可以是任何函数或方法的引用。

4、DartObject对象中的方法及作用?

  • hashCode
    • 返回对象的哈希码,用于对象的哈希表实现。
  • toString()
    • 返回对象的一个字符串表示形式。
  • runtimeType
    • 返回对象的运行时类型
  • noSuchMethod()
    • 当尝试调用对象中不存在的方法时,该方法被调用。
  • operator ==(other)
    • 比较当前对象是否与另一个对象相等。

5、Dart中的mixin和它的用途?

  • mixin是一种特殊的类,它被设计用于代码重用,尤其是多个类需要共享某些行为时。
  • mixin不能被实例化,也不能作为超类,可被其他类使用with关键字混入,以扩展该类的功能
  • mixin的用途:
    • 代码重用mixin提供了一种在多个类中重用代码的机制,可避免代码重复,使得代码更加模块化和易于维护。
    • 避免多继承问题Dart单继承语言,使用mixin可避免单继承的局限性,在不增加继承树复杂性的情况下,向类中添加多个独立的功能集。
    • 组合行为:可将多个mixin应用于一个类,以组合不同的特性。这使得类的设计更加灵活,按需添加或删除功能。

6、DartFutureStream的区别?

Future

  • 定义Future表示一个可能在将来某个时间点完成的计算或操作的结果
  • 单个结果Future最终会完成并产生一个成功或错误的结果,但不会产生多个结果。
  • 完成或错误Fluture一旦完成,要么成功并返回一个值,要么失败并抛出一个错误。
  • 等待完成:可使用then方法注册一个回调来处理结果,或使用awaitasync函数中等待Future完成。
  • 创建Future可通过多种方式工厂模式创建,如Future.delayedFuture.valueFuture.error等。
  • 消费:可通过Future.thenFuture、catchErrorFuture.whenComplete等方法消费。
  • 暂停和恢复:不支持。
  • 使用场景:主要用于处理一次性异步操作,如网络请求数据库查询文件读写等,这些操作通常会产生单个结果。

Stream

  • 定义Stream是一个异步数据的序列,它可以产生零个或多个数据项,并最终完成。
  • 多个数据项Stream可以产生多个值,直到它被关闭或者完成。
  • 订阅模式:消费者通过订阅Stream来接收数据,使用Stream.listenStreamBuilder等方法。
  • 实时处理Stream非常适合实时处理数据,因为数据一到达就可以被处理,而不需要等待整个操作完成。
  • 创建Stream可通过StreamControllerStream.fromIterableStream.periodic等方式创建。
  • 消费Stream可通过Stream.listenStreamBuilderStreamProvider等方式消费。
  • 暂停和恢复:可通过pause暂停, resume恢复。
  • 使用场景:非常适合处理连续的可能无限的数据流,如实时传感器数据日志记录网络流等。

7、Dart中如何取消正在执行中的异步任务?

取消 Future

由于Future本身没有直接的取消机制,可通过如下几种方式实现类似效果。

方式一:使用Future.cancelable

通过Future.cancelable工厂方法,该方法允许创建一个可取消的Future,如果在Future完成之前调用了cancel方法,会抛出一个CancelledError异常。

Future<int> cancellableOperation() {
  return Future.cancelable((cancellationToken) async {
    await Future.delayed(Duration(seconds: 5));
    if (!cancellationToken.isCancelled) {
      return 42;
    }
    throw CancelledError();
  }, onCancel: () {});
}

void main() async {
  final future = cancellableOperation();
  await Future.delayed(Duration(seconds: 2));
  future.cancel();
}

方式二:使用Completer

Completer类可控制Future的完成时间,因此可通过控制Completer来取消Future

  1. 创建Completer:创建一个Completer对象,并将其Future属性暴露给外部。
  2. 在异步任务中使用Completer:在异步任务中,通过CompletercompletecompleteError方法来标记任务的完成状态。
  3. 取消任务:通过调用CompletercompleteError方法来取消任务,通常会传一个错误或取消的信号。
Completer completer = Completer();

Future future = completer.future;

void cancelTask() {
  completer.completeError('Task was cancelled');
}

void doSomeTask() async {
  try {
    // 异步操作
    await Future.delayed(Duration(seconds: 3));
    completer.complete('Task completed');
  } catch (e) {
    completer.completeError(e);
  }
}

// 启动任务
doSomeTask();

// 取消任务
cancelTask();

方式三:使用Stopwatch

使用StopwatchFuture.timeout来实现超时取消。

Stopwatch stopwatch = Stopwatch()..start();

Future future = someAsyncFunction()
    .timeout(Duration(seconds: 5), onTimeout: () {
      stopwatch.stop();
      return 'Task timed out';
});

stopwatch.elapsed > Duration(seconds: 5)
    ? future.catchError((e) => print('Task cancelled'))
    : future.then((value) => print(value));

方式四:使用StreamController

对于Stream,可使用StreamController来创建和控制Stream. 通过调用close方法来实现取消操作。

StreamController<int> controller = StreamController<int>.broadcast();

Stream<int> stream = controller.stream;

void cancelStream() {
  controller.close();
}

void produceStream() async {
  for (int i = 0; i < 10; i++) {
    await Future.delayed(Duration(seconds: 1));
    controller.add(i);
  }
  controller.close();
}

// 启动Stream
produceStream();

// 取消Stream
cancelStream();

最佳实践

  • 在设计系统时,尽量避免需要取消Future的情况。确保你的异步操作可以优雅地处理中断信号。
  • 如果需要取消Future,考虑使用Future.cancelable,该方法实现的代码更易于理解和维护。
  • 对于长期运行的任务,考虑使用Stopwatch来实现超时机制。
  • 如果Future是基于Stream的,使用StreamControllerclose方法来取消未完成的操作。

8、Dartasyncawait的区别?

async

  • async关键字用于声明一个函数是异步的
  • async函数的执行不会阻塞当前的执行线程,会返回一个Future对象,表示异常操作的结果。
  • 适用于耗时操作,如网络请求数据库操作文件读写等。

await

  • await关键字用于等待一个Future完成。
  • await只能在async函数内部使用。
  • await会暂停当前执行的线程,直到Future完成并返回结果或抛出异常。
  • 适用于简化异步代码的链式调用,使异步代码看起来更像同步代码。

9、Dartisolate和它的用途?

Isolate

  • Dart语言本身是基于单线程模型设计的。
  • IsolateDart提供的用于实现并发和异步编程的重要机制之一。

Isolate基本概念

  • 独立的内存和执行环境:每个Isolate都有自己的内存空间不会共享内存。这消除了多线程编程中常见的锁竞争和死锁等问题。
  • 消息传递Isolate之间的通信是通过消息传递机制实现的,而不是共享内存。
  • 事件循环:每个Isolate都有自己的事件循环负责调度和执行异步操作

Isolate创建

创建Isolate的基本方法是使用Isolate.spawn函数,它接受一个函数作为参数,这个函数将在新创建的Isolate中执行。

import 'dart:isolate';

void main() {
  Isolate.spawn(_computeFactorial, 10);
}

void _computeFactorial(int n) {
  // 计算阶乘的逻辑...
}

Isolate通信

Isolate之间的通信是通过发送和接收消息完成的。分别使用SendPortReceivePort来发送和接收消息。

void main() {
  var receivePort = ReceivePort();
  Isolate.spawn(_computeFactorial, 10, receivePort.sendPort);

  receivePort.listen((factorial) {
    print('Factorial of 10 is: $factorial');
  });
}

void _computeFactorial(int n, SendPort sendPort) {
  int factorial = _calculateFactorial(n);
  sendPort.send(factorial);
}

int _calculateFactorial(int n) {
  // 计算阶乘的逻辑...
}

Isolate事件循环

  • 每个Isolate都有自己的事件循环,这是Isolate管理异步操作和执行任务的核心机制。
  • 事件循环负责调度和执行Isolate中的异步事件,包括定时器网络请求I/O操作等。

事件循环的组成

  • 事件队列 (Event Queue):这是个先进先出的队列,用于存储待处理的异步事件。
  • 微任务队列 (Microtask Queue):这是另一队列,用于存储微任务,如Future.microtask。微任务会在当前宏任务的末尾执行,但优先于事件队列中的下一个宏任务。
  • 宏任务 (Macro Tasks):宏任务是事件循环中的一次完整的执行周期,通常由一个事件触发,如定时器到期I/O操作完成等。

事件循环的执行流程

  • 执行宏任务:事件循环从事件队列中取出一个宏任务,并执行其对应的回调函数。
  • 清空微任务队列:当宏任务执行完成后,事件循环会清空微任务队列,执行所有微任务。
  • 重复上述过程:事件循环会不断重复上述过程,直到事件队列和微任务队列都为空,或者Isolate被显示地停止。

10、Dart是不是单线程模型? 它是如何运行的?

图解方式让记忆更为深刻及长久: image.png

开场引入

Dart确实采用了单线程模型,但这并不意味着Dart不能处理并发操作。实际上,Dart的单线程模型与事件循环和异步编程紧密相关,旨在提供一个简单而一致的编程模型,同时确保高性能和响应性。

单线程模型的细节

  1. 事件循环
  • Dart的单线程模型依赖于事件循环机制。事件循环负责监听和调度各种事件,如定时器、用户输入、网络事件等。
  1. 宏任务与微任务
  • 事件循环管理着宏任务和微任务队列。宏任务包括如脚本执行、事件处理、定时器回调等,它们是事件循环的主要工作单位。
  • 微任务(如Future的回调)则在当前宏任务结束后立即执行,但在此之前。微任务的即时执行特性使得它们适合用于需要在宏任务结束时立即响应的场景。
  1. Isolates
  • 尽管Dart在单个Isolate中是单线程的,但Isolates允许Dart程序并行执行。每个Isolate都有自己的内存空间和事件循环,它们通过消息传递而非共享内存进行通信。Isolates的设计是为了实现真正的并发和避免竞态条件。

Dart的运行机制

  1. Dart VM初始化
  • Dart程序的执行始于Dart VM的启动,它创建一个默认的Isolate,这是程序执行的起点。
  1. 事件循环初始化
  • 每个Isolate在启动时初始化其自身的事件循环,用于处理宏任务和微任务队列。
  1. 宏任务执行
  • 当宏任务开始时,它会占用整个线程,直到完成。在宏任务中,可以发起异步操作,如网络请求或Future的创建。
  1. 异步操作
  • 异步操作的回调通常被安排为微任务或下一个宏任务。例如,Future.then的回调会被添加到微任务队列。
  1. 微任务执行
  • 在当前宏任务结束后,事件循环会执行微任务队列中的所有任务。微任务队列在每个宏任务结束后被清空。
  1. 任务调度与执行
  • 事件循环不断检查宏任务队列和微任务队列,调度并执行任务,直到所有任务完成。

总结

Dart的单线程模型,加上事件循环、宏任务和微任务的机制,以及Isolates的并发能力,提供了一个既简单又强大的异步编程模型。这种模型不仅避免了多线程编程中的复杂性,如锁和竞态条件,还确保了程序的高性能和响应性。

二、Flutter框架

1、Flutter架构简述?

image.png

Framework(框架层)

开发者通过Flutter 框架层Flutter交互,该框架提供了以Dart语言编写的现代响应式框架

从上至下依次有:

  • MeterialCupertino库提供了全面的widgets层的原语组合,这套组合分别实现了MeterialiOS设计规范。
  • widgets层是一种组合的抽象。每个渲染层中的渲染对象,都在widgets层中有一个对应的类。widgets层可自由组合开发者需要的各种类。响应式编程模型就是该层种被引入。
  • Rendering(渲染层)用于提供操作布局的抽象。有了渲染层,可以构建一棵棵渲染对象的树。当动态更新这些对象时,渲染树也会自动根据变更来更新布局。
  • 基础的foundational类及一些基层之上的构建块服务,如animationpaintinggestures,它们提供了上层常用的抽象。

Engine(引擎层)

  • Flutter核心层,主要使用C++编写,并提供了Flutter应用所需要的原语。

  • 该层包括: 图形绘制(在AndroidiOS上使用Impeller,其他平台使用Skia)、文本布局文件及网络IO辅助功能支持插件架构Dart运行环境及编译环境的工具链等。

Embedder(嵌入层or平台层)

  • 嵌入层提供一个程序入口,程序由此可与操作系统进行协调,如Surface渲染辅助功能输入等服务,并且管理事件循环队列

2、Flutter中StatelessWidget和StatefulWidget的区别?

开场引入

Flutter中,Widget是构成用户界面的基本构建块,每个UI元素都可以看着是一个WidgetWidget主要分为两大类:StatelessWidgetStatefulWidget,它们的主要区别在于是否需要维护和更新状态

正式回答

接下来,从四个方面详细阐述两者的主要区别:

  1. 状态管理
    • StatelessWidget:不维护状态。一旦构建,它的表现和行为在应用运行期间不会改变。
    • StatefulWidget:维护状态。它可以通过State对象来跟踪和影响变化,允许Widget在需要时更新自身。
  2. 构建和更新
    • StatelessWidget:每次构建时,都会重新创建整个Widget,因为它不保存任何状态。
    • StatefulWidget:只有当状态或属性改变时,才会触发更新。更新的通过调用setState方法来实现的,这会通知Flutter框架重新构建Widget
  3. 性能影响
    • StatelessWidget:因为每次都会完全重建,性能上优于StatefulWidget,尤其是当Widget不需要频繁更新时。
    • StatefulWidget:虽然更新机制可以节省资源,但过多的setState调用或不当的状态管理可能导致不必要的重建,影响性能。
  4. 使用场景
    • StatelessWidget:适用于静态内容或不随时间变化的UI元素,如图标、静态文本等。
    • StatefulWidget:适用于需要响应用户输入或数据变化的动态UI元素,如按钮、滑块、列表视图等。

结束总结语

正确区分和使用StatelessWidget和StatefulWidget对于构建高效,响应迅速的Flutter应用至关重要。了解它们的区别有助于优化UI的性能,同时确保应用的用户界面能够准确响应用户操作和数据变化。

3、Flutter中Widget的生命周期?

开场引入

Flutter中,Widget的生命周期描述了从创建到销毁的整个过程,理解Widget的生命周期对于编写高效和响应式的应用程序至关重要。

正式回答

StatelessWidget的生命周期

StatelessWidget因为无状态,它的生命周期较为简单,主要关注点在于构建过程。每当StatelessWidget的属性变化时,它将被重新构建。

StatefulWidget的生命周期

  1. 创建阶段
    • createState:创建StatefulWidget对应的State对象,此方法仅被调用一次。
    • initState:在State对象被插入到渲染树中时调用,可在此进行初始化操作, 如加载数据或订阅事件。
  2. 运行阶段
    • build:构建Widget树,此方法在State对象创建后会被多次调用,当Widget发生任何变化时,build方法会被重新调用。
    • didChangeDependencies:当State对象依赖的InheritedWidget发生变化时被调用,可在此更新状态或资源。
    • setState:触发Widget的更新,导致build方法再次调用,以便于更新UI
  3. 移除阶段
    • deactivate:当State对象从渲染树中移除时调用,可在此处理资源释放。
    • dispose:当State对象被永久销毁时调用,可在此清理所有资源,如释放内存或取消订阅等。

结束总结语

合理的利用Widget的生命周期,特别是在StatefulWidget中,可帮助我们优化性能,避免内存泄露,确保应用在不同的运行条件下都能保持良好的响应性和稳定性。

总之,Widget的生命周期是Flutter中的一个关键的概念,理解并正确运用它,对于构建高效且健壮的Flutter应用至关重要。

4、FluttersetState的执行流程?

开场引入

Flutter中,setState是用于更新StatefulWidget状态的关键方法。当状态改变时,通过调用setState来通知Flutter框架,这将触发UI的重新构建,以反映最新的状态变化。

正式回答

setState调用后发生的具体流程

  1. 标记Widget
    • setState被调用时,首先会标记当前的State对象所属的Widget(即状态已改变,需要重新绘制)。
  2. 调度绘制
    • setState调用并不会立即触发UI的重绘,而是将Widget加入到待更新队列中。接下来,Flutter框架会在适当的时机(通常是下一帧)进行重绘。
  3. 执行布局和绘制
    • 当下一帧开始时,Flutter的事件循环会检查是否有待更新的Widget
    • 对于被标记为脏的Widget,框架会调用其build方法,生成一个新的Widget树。
    • 新的Widget树会被布局,然后绘制到屏幕上。
  4. 触发依赖更新
    • 如果StatefulWidget依赖于InheritedWidgetsetState可能会触发InheritedWidget的更新,进而影响到更多的后代Widget
  5. 异步限制
    • setState方法不能在异步操作中直接调用,如Futureasync函数内部。应该在异步操作完成后,在回调函数或then方法中调用。

强调注意事项

  • 避免不必要的重绘:
    • 确保在setState中只更新真正改变了的状态,避免不必要的UI重绘。
  • 异步操作:
    • 不要在setState内部执行耗时操作,如网络请求或数据库读写等,而应在这些操作完成后调用setState
  • 性能优化:
    • 使用shouldRebuild方法或const关键字来优化Widget的构建过程,减少不必要的重绘。

结束总结语

总之,setState的执行流程涉及到标记Widget调度重绘执行布局和重绘等步骤,理解这一流程对于编写高效且响应式的Flutter应用至关重要。

5、FlutterApp的启动流程?

1)、Dart 程序入口( main )

  • Flutter应用的执行始于main.dart文件中的main函数,这是Dart程序的入口点。
  • main函数内部,通常会调用runApp方法,该方法接收一个Widget作为参数,这个Widget将成为应用的根组件。

2)、runApp 方法

  • runApp首先回检查WidgetsFlutterBinding是否已经初始化,如果没有,则调用ensureInitialized方法进行初始化。
  • 初始化过程包括创建DartProjectDartIsolateSpecDartIsolate等,为 Dart VMFlutter引擎的启动做准备。
  • 初始化过程还涉及加载Dart代码,初始化DartIsolate,设置回调函数等。

3)、初始化引擎

  • Flutter引擎是基于C++实现的,它负责图形渲染、文本布局、动画处理等任务。
  • WidgetsFlutterBinding初始化完成,引擎被加载,Dart VM被启动,DartIsolate也已经准备好。
  • Flutter引擎与Dart层通过平台通道(Platform Channels)进行通信,用于数据交换和事件处理。

4)、构建 Widget 树

  • runApp接下来会构建应用程序的Widget树,从根Widget开始,递归地构建子Widgets
  • Widget树的构建涉及到框架层的ElementRenderObject,它们分别负责状态管理和实际的渲染。

5)、调度、渲染及绘制

  • Flutter使用Scheduler来控制屏幕的绘制频率和时机,确保每秒60帧的刷新率。
  • Scheduler负责将Widget树的更新同步到RendererRenderer这计算布局,绘制并提帧到GPU
  • GPU最终将渲染结果输出到屏幕上。

6)、生命周期管理

  • Flutter框架会管理应用的生命周期。
  • 生命周期事件会触发相应的回调。

7)、平台交互

  • Flutter应用在AndroidiOS平台上运行时,还需要与原生交互。
  • 交互通常是通过平台渠道(Platform Channels)实现的,平台渠道允许Dart代码调用原生代码和反向调用。

简洁回答:

  • Flutter应用程序的启动始于main函数中的runApp调用。
  • runApp中,首先确保WidgetsFlutterBinding被初始化,这个过程包括Dart虚拟机Flutter引擎的启动。
  • 接着,构建应用程序的Widget树,从根Widget开始,逐步构建整个UI层次结构。
  • SchedulerRenderer负责管理帧的绘制,确保Widget树的变化能够及时反映在屏幕上。
  • 与此同时,通过平台渠道与原生系统进行交互,处理生命周期事件和其他原生功能。
  • 整个启动流程体现了Flutter在跨平台开发中高效、灵活和高性能的特点。

6、Flutter中的三棵树是什么?

开场引入

Flutter中,UI框架是围绕三棵树的概念构建的,三棵树分别是Widget树、 Element树和RenderObject树。

正式回答

Widget

  • Widget树是应用程序UI的描述性模型。
  • Widgets是不可变的数据结构,它们定义了UI的外观和行为。
  • 每个Widget都是一个具体的UI元素的描述,比如按钮、文本框等。
  • Widgets不直接参与渲染过程,它们的主要职责是提供界面的构建蓝图。

Element

  • Element树是Widget树的具体实例,它包含WidgetUI树中的具体位置信息。
  • ElementWidgetRenderObject之间的桥梁,它负责协调两者之间的通信。
  • Element持有WidgetRenderObject的引用,当Widget树发生变化时, Element树负责更新RenderObject树以反映这些变化。
  • Element还负责脏检查,即确认哪些UI需要重新渲染。

RenderObject

  • RenderObject树是真正负责UI渲染的树,它负责绘制UI到屏幕上。
  • RenderObject是可变的,它们包含布局信息和绘制指令。
  • 这棵树是由Element树中的元素通过调用createRenderObjectupdateRenderOject方法构建的。

三棵树的交互

  • 当应用的状态发生变化时,会重建一个新的Widget树。
  • 新的Widget树与旧的Widget树对比,找出需要更新的部分。
  • 受影响的Element树部分会更新它们的RenderObject,触发布局和绘制过程。
  • 渲染引擎最终根据更新后的RenderObject树绘制出新的UI。

三棵树的之间的关系

  • WidgetElement之间是多对一的关系,因为一个Element可能会对应多个Widget。
  • ElementRenderObject之间是一对一的关系,Element负责创建和更新其关联的RenderObject

7、Flutter的渲染原理?

1)、Flutter渲染原理图image.png 2)、Flutter数据流向图

image.png

开场引入

Flutter的渲染原理涉及到多个层面,从Widget到最终的像素显示,它遵循了一套精心设计的流程,确保了高性能和高效率的用户界面渲染。

正式回答

Flutter渲染流程的六个主要步骤

1)、Widget 树构建

  • Flutter应用的核心是由Widgets构成的树形结构。
  • 每个Widgets代表一个UI元素。
  • 当应用运行时,Flutter引擎会根据当前状态构建一个Widget树。

2)、Element 树创建

  • Widget树的基础上,Flutter会创建一个Element树。
  • 每个Element关联着一个Widget和一个RenderObject
  • Element管理着Widget的生命周期,并且负责将Widget的属性传递给对应的RenderObject

3)、RenderObject 树布局与绘制

  • Element树中的Element会创建或更新RenderOject树。
  • RenderObject树负责实际的布局和绘制工作。
  • RenderObject中的节点会递归地计算它们的尺寸和位置,该过程称为布局。
  • 布局完成后,RenderObject会调用paint方法,将自身和子节点的内容绘制到画布上,该过程称为绘制。

4)、Layer 树合成

  • RenderObject绘制过程中会创建Layer对象,这些Layer对象会形成一个Layer树。
  • Layer树的目的是为了优化渲染过程,通过将UI的不同部分分隔到不同的层中,可以减少重绘的区域,提高渲染效率。
  • Layer层中合成的数据进行栅格化后生成纹理, 然后发送到GPU中处理。

5)、GPU 渲染

  • 使用Skia/Impeller图形库对Layer层发送过来的数据进行渲染。
  • 再使用OpenGLVulkan等图形库进行最终的像素渲染。
  • 最后将渲染好的图像数据发送到显示器,完成一次渲染周期。

6)、屏幕刷新

  • Flutter的帧调度依赖于垂直同步(VSync)信号,这确保了屏幕刷新率与应用帧率同步,通常为60Hz。
  • UI线程在收到VSync信号后开始新的帧的构建过程。

8、Flutter中的Key及其作用?

开场引入

Flutter中,KeyWidget的唯一标识,它在构建Widget树的过程中发挥着关键作用,帮助Flutter框架高效地识别,比较和复用Widget,从而提升应用性能。

正式回答

Key的两种主要类型

1)、GlobalKey

  • GlobalKey在整个生命周期中保持唯一和持久,它可以跨Widget树的不同部分使用,甚至在Widget树重构后依然有效。
  • GlobalKey常用于访问特定Widget的内部状态或方法。

2)、LocalKey

  • LocalKey在构建阶段被创建,用于在当前构建上下文中标识Widget,当Widget树被创建时,LocalKey帮助框架识别哪些Widget可以被重建, 哪些需要重新创建。
  • LocalKey的生命周期与构建过程相关联,一旦构建结束,LocalKey就不在有效。

Key在Flutter中的主要作用

1)、Widget的识别与比较

  • Key帮助框架识别Widget,当Widget树发生变化时,框架会使用Key来比较旧树和新树,确定哪些Widget需要保留,哪些需要创建或销毁。

2)、Widget的复用

  • 通过使用KeyFlutter框架可以识别出在新旧Widget树中相同KeyWidget,从而避免不必要的重建,提高应用性能。

3)、控制Widget的重建

  • 在使用如ListViewGridView等可滚动组件时,为每个item分配一个唯一的Key,可以控制哪些item需要重新构建,从而提高性能。

4)、状态管理与访问

  • GlobalKey可以用于跨Widget访问状态,例如,用于获取表单字段的值或触发复杂Widget的方法调用。

总结

  • KeyFlutter框架中不可或缺的一部分,它不仅帮助提升应用的性能,还提供了控制Widget重建和访问内部状态的能力,是实现高效和响应式用户界面的关键。

9、Flutter中的InheritedWidget及其工作原理?

开场引入

InheritedWidgetFlutter框架中用于在Widget树中传递数据的一种特殊Widget。它被设计用于解决状态共享和数据传递的问题,尤其适用于那些需要被多个Widget访问的数据,如主题、国际化信息、用户偏好设置等。

工作原理

1)、数据的提供

  • InheritedWidget通常被放置在Widget树的较高层级,作为许多需要访问该数据的Widget的共同祖先。
  • 数据通常在InheritedWidget的构造函数中通过一个data参数提供。

2)、数据的传播

  • InheritedWidget被插入到Widget树中时,它会自动向下传播其数据,直到遇到下一个同类型的InheritedWidgetWidget树的底部。
  • Flutter框架会缓存InheritedWidget的数据,以便其后代Widget可以高效地访问。

3)、数据的访问

  • Widget可以通过BuildContext来访问其最近的祖先InheritedWidget的数据。通常使用静态方法of(context)来获取数据。
  • Flutter框架提供了dependOnInheritedWidgetOfExactType方法,使Widget可以依赖于特定类型的InheritedWidget

4)、数据的更新

  • InheritedWidget的数据发生变化时,所有依赖于该数据的后代Widget将会被标记为需要重新构建。
  • 为了确定是否需要重新构建,InheritedWidget有一个updateShouldNotify方法,该方法需要返回一个布尔值,指示数据的变化是否影响了Widget的表现。

使用场景

  • 主题设置
    • InheritedWidget非常适合用于传递主题数据,如颜色、字体大小等,这些数据通常被多个Widget使用。
  • 国际化
    • 用于传递语言和地区设置,使应用能够在不同的语言环境中显示适当的内容。
  • 状态管理
    • 虽然InheritedWidget不是专门的状态管理解决方案,但在某些情况下,它可以用于在小范围内共享状态。

总结

InheritedWidget的优势

  • InheritedWidget提供了一种简洁、高效的方式来在Widget树中共享数据,避免了繁琐的参数传递。
  • 它可以显著减少代码量,使Widget结构更加清晰,易于维护。
  • 通过使用InheritedWidget,可以轻松地在应用的不同部分之间共享数据,而不必担心数据传递的复杂性。

10、Flutter中build方法中的BuildContext具体是什么?

开场引入

BuildContextFlutter中一个非常核心且强大的概念,它代表了在Widget树中的一个位置。它是构建过程中的上下文信息,用于在构建Widget时提供关于当前构建位置的信息。

BuildContext在构建过程中的作用

1)、位置标识

  • BuildContext提供了关于WidgetWidget树中的位置信息。它能够告诉你一个Widget相对于其父Widget或树的其他部分处于何处。

2)、数据访问

  • BuildContext允许Widget访问其祖先Widget的数据和状态,包括InheritedWidget中的数据。这意味着,即使Widget没有直接从其父Widget接收数据,它仍然能够访问更高级别的数据。

3)、资源获取

  • 通过BuildContextWidget可以访问各种资源,如主题数据、屏幕尺寸、方向、媒体查询等。这使得Widget可以根据环境动态调整其行为。

4)、事件处理

  • BuildContext还提供了访问事件处理系统的方法,例如,可以通过BuildContext找到最近的ScaffoldNavigator,从而触发弹出抽屉、导航到新页面等操作。

5)、性能优化

  • BuildContext在性能优化方面也起到关键作用,因为它可以帮助确定哪些Widget需要重新构建。在构建过程中,BuildContext会追踪哪些Widget是“脏”的,即哪些Widget的状态发生了变化,需要重新构建。

总结

  • BuildContextFlutter框架中不可或缺的一部分,它为Widget提供了关于其在树中位置的信息,以及访问资源和数据的能力。
  • 正确理解和使用BuildContext对于构建高效、可扩展的Flutter应用至关重要。

三、状态管理

1、说说Flutter中的状态管理?为什么需要它?

开场引入

  • 状态管理Flutter应用程序开发中的核心概念之一,它涉及到如何在应用的不同部分之间共享、更新和维护数据状态。
  • Flutter中,状态管理主要关注的是如何有效地处理应用的动态数据,确保UI能够实时响应这些数据的变化。

状态管理的本质

  • 状态管理的核心在于提供一套机制,使得数据能够跨组件跨屏幕、甚至跨生命周期地被访问修改
  • 这涉及到数据的一致性更新通知、以及数据的持久化

为什么需要状态管理?

1)、数据共享

  • 在复杂的应用程序中,多个组件可能需要访问同一份数据。
  • 状态管理提供了一种集中化的方式,让这些组件能够访问和更新数据,而无需通过层层传递参数。

2)、响应式UI

  • 状态管理确保UI能够响应数据的变化。
  • 当数据更新时,相关的UI组件可以自动更新,无需手动触发重新构建。

3)、可维护性

  • 集中的状态管理策略使得数据流和状态更新逻辑更易于跟踪和维护。
  • 这有助于大型团队协作,减少代码中的错误和冲突。

4)、性能优化

  • 有效的状态管理可以减少不必要的组件重建,提高应用的性能。

5)、跨屏幕一致性

  • 在多屏幕或多组件的应用中,状态管理确保了数据的一致性,避免了不同部分显示不一致的情况。

状态管理的挑战与解决方案

  • 状态管理的实现方式多种多样,包括内置的StatefulWidgetInheritedWidget,以及第三方库如ProviderRiverpodBloc等。
  • 每种方法都有其适用场景和优缺点,选择合适的状态管理模式取决于应用的规模、数据流的复杂度以及团队的偏好。

总结

  • 状态管理在Flutter中扮演着至关重要的角色,它不仅是实现动态UI的关键,也是构建可维护、可扩展应用程序的基石。
  • 通过合理地设计和实施状态管理策略,开发人员可以构建出既响应迅速又性能优秀的应用,同时简化了代码结构,提高了开发效率。

四、第三方库

五、性能优化

六、跨平台开发