Flutter 面试题02

808 阅读5分钟

01. Flutter JIT 和 AOT 之间有什么区别?

JIT: JIT 编译是在应用运行时动态编译代码,将 Dart 代码编译为机器代码,这意味着代码在执行之前是即时编译的

AOT: AOT 编译是在应用发布之前将 Dart 代码编译为机器码,这意味着应用在发布到设备前已经完全编译好

在开发模式下使用 JIT 编译来实现热重载,让开发者快速查看代码更改后的效果。

当应用准备发布时,Flutter 会自动使用 AOT 编译来生成优化后的应用程序,确保发布到生产环境时的高性能和低启动时间。

02. 在 Flutter 中,Keys(键)有哪几种类型, 并解释下其作用?

Widget中有个可选属性key,顾名思义,它是组件的标识符。 Key 是一个非常重要的概念,主要用于区分、保持和管理 Widget 的状态, 尤其在涉及列表、动画和重建(rebuild)等复杂场景时显得尤为重要

2-key的分类

key有两个子类GlobalKeyLocalKey

GlobalKey:GlobalKey全局唯一key,每次build的时候都不会重建,可以长期保持组件的状态,一般用来进行跨组件访问Widget的状态

LocalKey:LocalKey局部key,可以保持当前组件内的子组件状态,用法跟GlobalKey类似,可以访问组件内部的数据。

LocalKey有3个子类ValueKey、ObjectKey、UniqueKey。

  1. 使用特定的值来区分 Widget,在列表项中比较常用。例如用于标识唯一列表项 ValueKey 源码
class ValueKey<T> extends LocalKey {
  /// Creates a key that delegates its [operator==] to the given value.
  const ValueKey(this.value);

  /// The value to which this key delegates its [operator==]
  final T value;

  @override
  bool operator ==(Object other) {
    if (other.runtimeType != runtimeType) {
      return false;
    }
    return other is ValueKey<T>
        && other.value == value;
  }

  @override
  int get hashCode => Object.hash(runtimeType, value);

}
  1. 使用 Dart 对象来标识,适用于对象实例来区分的场景 ObjectKey 源码
class ObjectKey extends LocalKey {
  /// Creates a key that uses [identical] on [value] for its [operator==].
  const ObjectKey(this.value);

  /// The object whose identity is used by this key's [operator==].
  final Object? value;

  @override
  bool operator ==(Object other) {
    if (other.runtimeType != runtimeType) {
      return false;
    }
    return other is ObjectKey
        && identical(other.value, value);
  }

  @override
  int get hashCode => Object.hash(runtimeType, identityHashCode(value));
}
  1. 为每个 Widget 生成唯一的 Key,通常用于强制 Flutter 重建一个 Widget UniqueKey 源码
class UniqueKey extends LocalKey {
  /// Creates a key that is equal only to itself.
  ///
  /// The key cannot be created with a const constructor because that implies
  /// that all instantiated keys would be the same instance and therefore not
  /// be unique.
  // ignore: prefer_const_constructors_in_immutables , never use const for this class
  UniqueKey();
}

为什么要使用 Key?

Key 的主要作用是帮助 Flutter 区分 Widget,以便在 Widget 树发生变化时,可以将特定的状态与对应的 Widget 绑定。以下场景中尤其需要 Key:

  • 列表项重排:当 ListView 中的项发生增删或重新排序时,通过 Key 能让 Flutter 更好地理解哪些项保持不变、哪些需要更新。
  • 保持 Widget 状态:一些 StatefulWidget 需要保持自己的状态不变,比如表单字段的输入、滚动位置等。
  • 动画和过渡效果:在切换页面或动态更新界面时,如果希望某些元素不重新生成,可以用 Key 保证它们保持当前状态。
  1. 使用 GlobalKey 访问状态 GlobalKey 允许在 Widget 树的其他位置访问和操作某个 Widget 的 state。
class LoginPage extends StatefulWidget {
  const LoginPage({super.key});

  @override
  // ignore: library_private_types_in_public_api
  _LoginPageState createState() => _LoginPageState();
}

class _LoginPageState extends State<LoginPage> {
  GlobalKey<FormState> formKey = GlobalKey<FormState>();
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("Gobal Keys"),
      ),
      body: Center(
        child: Form(
            key: formKey,
            child: Column(
              children: [
                TextFormField(
                  validator: (value) => value!.isEmpty ? "Enter text" : null,
                ),
                ElevatedButton(
                    onPressed: () {
                      if (formKey.currentState!.validate()) {
                        print("Form is validate");
                      }
                    },
                    child: const Icon(Icons.send))
              ],
            )),
      ),
    );
  }
}
  1. 使用 ValueKey 在列表中保持 Widget 唯一性
class MyListView extends StatelessWidget {
  final List<String> items;

  MyListView(this.items);

  @override
  Widget build(BuildContext context) {
    return ListView(
      children: items.map((item) => ListTile(
        key: ValueKey(item),  // 确保每个 item 的唯一性
        title: Text(item),
      )).toList(),
    );
  }
}

  1. 使用 Key 优化动画效果

当 Widget 在页面中移动时,使用 Key 保持它的状态而不销毁可以提高动画流畅性。例如在 AnimatedList 中保持项的唯一性。

class MyAnimatedList extends StatelessWidget {
  final List<int> items;

  MyAnimatedList(this.items);

  @override
  Widget build(BuildContext context) {
    return AnimatedList(
      initialItemCount: items.length,
      itemBuilder: (context, index, animation) {
        return SizeTransition(
          key: ValueKey(items[index]),  // 为动画列表中的每项设置 Key
          sizeFactor: animation,
          child: ListTile(title: Text('Item ${items[index]}')),
        );
      },
    );
  }
}

03. Flutter从启动到显示

在 Flutter 应用程序启动后,到其内容最终显示在屏幕上,通常涉及以下主要步骤:

  1. Flutter 引擎初始化:

Flutter 引擎初始化是应用程序启动的第一步。在这个阶段,Flutter 引擎会初始化运行时环境,加载 Flutter 框架和原生平台的相关代码。

  1. Dart 代码执行:

一旦引擎初始化完成,Flutter 会加载并执行 Dart 代码。Dart 代码包括应用程序的入口点 main() 函数以及应用程序的其他逻辑。

  1. Widget 树构建:

Flutter 使用一种称为 Widget 的组件模型来构建用户界面。在 Dart 代码执行阶段,Flutter 应用程序会通过调用 build() 方法构建 Widget 树。 在构建 Widget 树期间,Flutter 会创建并配置一系列 Widget,这些 Widget 最终会渲染为用户界面的组件。

  1. 绘制和布局:

一旦 Widget 树构建完成,Flutter 引擎会执行布局(Layout)和绘制(Painting)阶段。在布局阶段,Flutter 计算每个 Widget 的位置和大小。在绘制阶段,Flutter 将每个 Widget 绘制为屏幕上的图像。

  1. 合成:

在绘制阶段完成后,Flutter 引擎会将所有绘制的内容合成为一张图像,并将其显示在屏幕上。 Flutter 使用了一种称为“图层合成”的技术,它能够有效地管理屏幕上的图层,提高绘制效率。

  1. 显示:

最后,一旦合成完成,图像就会被显示在屏幕上,用户就能够看到应用程序的内容了。 总的来说,从 Flutter 应用程序启动到其内容显示在屏幕上,涉及到一系列复杂的过程,包括引擎初始化、Dart 代码执行、Widget 树构建、布局和绘制、合成和显示等阶段。Flutter 通过这些阶段来实现高性能、流畅的用户界面渲染。

04. Flutter三棵树

即Widget树、Element树 和 RenderObject树。

Widget树:控件的配置信息,不涉及渲染,更新代价极低。

RenderObject树:真正的UI渲染树,负责渲染UI,更新代价极大。

Element树:Widget树和RenderObject树之间的粘合剂,负责将Widget树的变更以最低的代价映射到 RenderObject树上

05. 在Flutter项目中,一些常见的棘手问题及其解决方法

  1. 性能问题:卡顿和掉帧:在UI复杂或列表项较多时可能会出现卡顿
  2. 状态管理:状态同步问题:在多个组件之间共享状态时,状态可能不同步
  3. 依赖包冲突:版本冲突:不同依赖包之间可能有版本冲突,导致构建失败
  4. 平台特定问题:iOS和Android差异:有些功能在iOS和Android上的表现不同,或特定平台上的功能无法正常工作
  5. 构建和发布问题:构建失败:在不同环境下(如开发、测试、生产)可能会遇到构建失败的问题。
  6. 网络请求和数据处理:API请求失败:处理网络请求时可能会遇到超时或数据格式错误等问题。

06. flutter中用的哪些组件多一些?

基础组件:

Container:一个多功能容器,支持布局、装饰、定位等属性。
Text:用于显示一段文本。
Image:用于显示图片,可以从网络、文件、内存等加载。
Icon:用于显示图标。
Scaffold:应用程序页面的基础结构,包含AppBar、Drawer、Snackbar等常用组件。
AppBar:顶部应用栏,通常包含标题和操作按钮。

布局组件:

Column:垂直方向布局多个子组件。
Row:水平方向布局多个子组件。
Stack:重叠布局,可以让子组件堆叠显示。
ListView:可滚动列表,用于显示大量子组件。
GridView:网格布局,用于显示两维的子组件列表。
Expanded和Flexible:控制子组件在Flex布局(如RowColumn)中的伸缩行为。

输入组件:

TextField:文本输入框。
Checkbox:复选框。
Radio:单选按钮。
Switch:开关按钮。
Slider:滑块。
DropdownButton:下拉按钮。

按钮组件:

OutlinedButton:带边框按钮。
IconButton:带图标按钮。
FloatingActionButton:悬浮按钮,通常用于突出某个重要操作。

导航和路由:

Navigator:管理应用程序页面的堆栈。
Drawer:侧边栏菜单。
BottomNavigationBar:底部导航栏。
TabBar和TabBarView:标签栏和标签内容视图。

高级组件:

FutureBuilder:基于异步操作的组件,用于处理Future的结果。
StreamBuilder:基于流数据的组件,用于处理Stream的结果。
CustomPaint:自定义绘制组件,允许开发者自己绘制图形。
AnimationController和AnimatedBuilder:动画控制和构建组件。

07. Flutter中的状态管理

  1. 本地状态管理(Local State Management): 本地状态管理是指在小规模应用或组件内部管理状态的简单方法。 通常使用StatefulWidget和setState来更新状态。 该方法适用于较简单的应用或组件,状态的范围有限且不需要在多个组件之间共享

  2. InheritedWidget 通过将状态作为不可变对象传递给子组件来实现状态共享。 当共享的状态发生变化时,它们会自动更新子组件。 这种方法适用于中等规模的应用,可以在组件树中共享状态,但不适用于大型应用或高度复杂的状态管理。

  3. Provider: Provider是Flutter社区中广泛使用的状态管理库,它构建在InheritedWidget之上, 提供了一种简化状态共享的方式。 它使用了依赖注入的概念,可以在组件树的任何位置共享状态,并自动通知相关的子组件进行更新。 Provider支持多种类型的状态管理,包括基于ChangeNotifier、Stream、ValueNotifier等。 它适用于中等到大型规模的应用,具有良好的灵活性和性能。

08. Flutter中的调试技巧有哪些?

  1. 断点
  2. 打印日志
  3. DevTools

09. Flutter中的动画是如何实现的?有哪些常用的动画类?

在Flutter中,动画是通过**Animation****AnimationController**两个类来实现的。
Animation表示动画的当前状态,例如动画的当前值、是否完成、是否反向等。
AnimationController用于控制动画的开始、暂停、恢复、反向等。

Flutter中的动画可以分为两种类型:显式动画和隐式动画。
显式动画是通过AnimationController控制的,例如Tween动画、Curve动画等。
隐式动画则是通过Flutter框架自动执行的,例如AnimatedContainer、AnimatedOpacity等。

常用的动画类包括:

1. Tween:用于在两个值之间进行插值运算,例如在0和1之间插值计算出当前值。
2. Curve:用于定义动画的速度曲线,例如线性曲线、抛物线曲线、弹性曲线等。
3. AnimationController:用于控制动画的开始、暂停、恢复、反向等。
4. AnimatedBuilder:用于在动画变化时自动重建Widget树,可以用于创建复杂的动画效果。
5. AnimatedContainer:用于创建一个可以自动执行动画的Container。
6. AnimatedOpacity:用于创建一个可以自动执行动画的Opacity。

10. PlatformView 以及其原理?

  1. 在引擎的插件注册表中,注册了自定义的 PlatformView

iOS 端:

- (void)viewDidLoad {
    [super viewDidLoad];
    
    _flutterEngine = [[FlutterEngine alloc] initWithName:@"iOSDemo"];
    [_flutterEngine run];
    [GeneratedPluginRegistrant registerWithRegistry:_flutterEngine];
    

    id <FlutterPluginRegistrar> nativeViewRegister = [_flutterEngine registrarForPlugin:@"com.pano.dev.nativeview"];
    [nativeViewRegister registerViewFactory:[DTFlutterViewFactory new] withId:@"DTFlutterView"];
}
  1. 创建 PlatformView 和工厂类

#import "DTFlutterViewFactory.h"

@interface DTFlutterView : UIView <FlutterPlatformView>

@end

@implementation DTFlutterView

- (instancetype)initWithFrame:(CGRect)frame {
    self = [super initWithFrame:frame];
    if (self) {
        self.backgroundColor = [UIColor cyanColor];
    }
    return self;
}

- (nonnull UIView *)view { 
    return self;
}

@end

#import <Foundation/Foundation.h>
#import <Flutter/Flutter.h>

@interface DTFlutterViewFactory : NSObject<FlutterPlatformViewFactory>

@end


@implementation DTFlutterViewFactory

- (NSObject<FlutterPlatformView>*)createWithFrame:(CGRect)frame
                                   viewIdentifier:(int64_t)viewId
                                        arguments:(id _Nullable)args {
    return [[DTFlutterView alloc] initWithFrame:frame];
}
@end

flutter端:

class SampleView extends StatefulWidget {
  const SampleView({super.key});

  @override
  State<StatefulWidget> createState() => _SampleViewState();
}

class _SampleViewState extends State<SampleView> {
  @override
  Widget build(BuildContext context) {
    if (defaultTargetPlatform == TargetPlatform.android) {
      return AndroidView(
        viewType: 'DTFlutterView',
        onPlatformViewCreated: _onPlatformViewCreated,
      );
    } else {
      return UiKitView(
          viewType: 'DTFlutterView',
          onPlatformViewCreated: _onPlatformViewCreated);
    }
  }
  _onPlatformViewCreated(int id) => print("DTFlutterView id: $id");
}

注意 viewType 和 withId 需要保持一致

11. flutter 和 native的通信方式有哪些?

  1. MethodChannel

MethodChannel 是 Flutter 与原生通信的最常用方式,它提供了一种同步调用方法来传递数据。

  • 适用场景:用于 Flutter 调用原生方法,并同步等待返回结果,或原生调用 Flutter 方法。
  • 实现方式:在 Flutter 中通过 MethodChannel 创建通道,在 Android 和 iOS 中实现对应的方法

Flutter 端:

const platform = MethodChannel("com.pano.dev");

Future<String> sendDataToNative() async {
  try {
    final res = await platform.invokeMethod("fetchData", {"key": "value"});
    print(res);
    return res;
  } catch (e) {
    print("Error: $e");
    throw ("fetch data failed");
  }
}

iOS 端:

#import "ViewController.h"
#import <Flutter/Flutter.h>
#import <FlutterPluginRegistrant/GeneratedPluginRegistrant.h>

@interface ViewController ()
@property (nonatomic, strong) FlutterEngine *flutterEngine;

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    _flutterEngine = [[FlutterEngine alloc] initWithName:@"iOSDemo"];
    [_flutterEngine run];
    [GeneratedPluginRegistrant registerWithRegistry:_flutterEngine];
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    // 打开 flutter 页面
    
    self.flutterEngine.viewController = nil;
    FlutterViewController *vc = [[FlutterViewController alloc] initWithEngine:self.flutterEngine nibName:nil bundle:nil];
    vc.modalPresentationStyle = UIModalPresentationFullScreen;
    [self presentViewController:vc animated:YES completion:NULL];
    
    FlutterMethodChannel *channel = [FlutterMethodChannel methodChannelWithName:@"com.pano.dev" binaryMessenger:vc.binaryMessenger];
    [channel setMethodCallHandler:^(FlutterMethodCall * _Nonnull call, FlutterResult  _Nonnull result) {
        if ([call.method isEqualToString:@"fetchData"]) {
            result(@"Received: \(data ?? "")");
        } else {
            result(FlutterMethodNotImplemented);
        }
    }];
}
@end

由于涉及到跨系统数据交互,Flutter 会使用 StandardMessageCodec 对通道中传输的信息进行类似 JSON 的二进制序列化,以标准 化数据传输行为。这样在我们发送或者接收数据时,这些数据就会根据各自系统预定的规则 自动进行序列化和反序列化

方法通道解决了逻辑层的原生能力复用问题,使得 Flutter 能够通过轻量级的异步方法调 用,实现与原生代码的交互。一次典型的调用过程由 Flutter 发起方法调用请求开始,请求 经由唯一标识符指定的方法通道到达原生代码宿主,而原生代码宿主则通过注册对应方法实 现、响应并处理调用请求,最后将执行结果通过消息通道,回传至 Flutter。

需要注意的是,方法通道是非线程安全的。这意味着原生代码与 Flutter 之间所有接口调用 必须发生在主线程。Flutter 是单线程模型,因此自然可以确保方法调用请求是发生在主线 程(Isolate)的;而原生代码在处理方法调用请求时,如果涉及到异步或非主线程切换, 需要确保回调过程是在原生系统的 UI 线程(也就是 Android 和 iOS 的主线程)中执行 的,否则应用可能会出现奇怪的 Bug,甚至是 Crash。

  1. EventChannel

EventChannel 是专门用于 Flutter 和原生之间的持续数据流传输的通信方式。例如从原生到 Flutter 的事件流或数据更新。 • 适用场景:用于持续的数据流传递,例如传感器数据、GPS 定位等。 • 实现方式:在 Flutter 中使用 EventChannel,在原生端通过 EventSink 向 Flutter 发送数据

flutter 端:

const eventChannel = EventChannel('com.pano.dev.my_event_channel');

void _receiveEventStream() {
  eventChannel.receiveBroadcastStream().listen((data) {
    print("Received data: $data");
  });
}

iOS 端:

- (void)viewDidLoad
{
    FlutterEventChannel *eventChannel = [FlutterEventChannel eventChannelWithName:@"com.pano.dev.my_event_channel" 
      binaryMessenger:vc.binaryMessenger];
    [eventChannel setStreamHandler:self];
}


- (FlutterError* _Nullable)onListenWithArguments:(id _Nullable)arguments
                                       eventSink:(FlutterEventSink)events {
    events(@"iOS data");
    return nil;
}
- (FlutterError* _Nullable)onCancelWithArguments:(id _Nullable)arguments {
    return nil;
}
  1. BasicMessageChannel

BasicMessageChannel 适合传输文本和半结构化数据,并支持双向通信。它可以使用 JSON 编码的数据,既适合简单的请求/响应模式,也适合持续的消息传递。

  • 适用场景:适用于需要双向通信、传输 JSON 数据或半结构化数据的场景。
  • 实现方式:在 Flutter 和原生端都使用 BasicMessageChannel,可以实现双向数据传输

flutter 端:

const messageChannel = BasicMessageChannel('com.example/my_message_channel', StandardMessageCodec());

void _sendMessageToNative() {
  messageChannel.send({"key": "Hello from Flutter"}).then((reply) {
    print("Reply from native: $reply");
  });
}

void _receiveMessageFromNative() {
  messageChannel.setMessageHandler((message) async {
    print("Received from native: $message");
    return "Reply from Flutter";
  });
}

iOS 端:

let messageChannel = FlutterBasicMessageChannel(name: "com.example/my_message_channel", binaryMessenger: controller.binaryMessenger, codec: FlutterStandardMessageCodec.sharedInstance())

messageChannel.setMessageHandler { (message, reply) in
  print("Received from Flutter: \(message ?? "")")
  reply("Hello from iOS")
}

4. libffi
待补充