现在的混合开发项目中遇到一个棘手的问题,iOS中设置根控制器为flutter界面的tabVC后,点击tabVC中的按钮跳转到新的flutter界面崩溃了。但是如果根控制器是iOS视图的话,打开flutterTabVC后再打开新的flutter界面是正常的。
这个问题只能我处理了,接下来是对flutter混合开发、flutter引擎、混合解决方案的学习和分析。
一、官方提供的混合页面打开方式
在 Flutter 和 iOS 的混合开发环境中,实现从 Flutter 页面打开 iOS 原生页面,以及从 iOS 页面打开 Flutter 页面,主要依赖于 Flutter 提供的平台通道(Platform Channels)来实现通信。
1.从Flutter页面打开iOS页面
思路:通过channel调用iOS打开页面的方法。
设置 Platform Channel 在 Flutter 端创建一个 MethodChannel 来与 iOS 端进行通信:
js
class YourFlutterPage extends StatelessWidget {
static const platform = MethodChannel('your.channel.name');
Future<void> openIosPage() async {
try {
await platform.invokeMethod('openIosPage');
} on PlatformException catch (e) {
print("Failed to open iOS page: '${e.message}'.");
}
}
}
iOS 端处理请求
js
#import "AppDelegate.h"
#import <Flutter/Flutter.h>
@implementation AppDelegate
- (BOOL)application:(UIApplication*)application didFinishLaunchingWithOptions:(NSDictionary*)launchOptions {
FlutterViewController* controller = (FlutterViewController*)self.window.rootViewController;
FlutterMethodChannel* channel = [FlutterMethodChannel methodChannelWithName:@"your.channel.name" binaryMessenger:controller];
[channel setMethodCallHandler:^(FlutterMethodCall* call, FlutterResult result) {
if ([call.method isEqualToString:@"openIosPage"]) {
// 打开iOS原生页面逻辑
UIStoryboard *storyboard = [UIStoryboard storyboardWithName:@"Main" bundle:nil];
UIViewController *viewController = [storyboard instantiateViewControllerWithIdentifier:@"YourIosViewControllerIdentifier"];
[self.window.rootViewController presentViewController:viewController animated:YES completion:nil];
}
}];
//**注册所有 Flutter 插件所需的原生代码(iOS 端)** ,让它们可以在混合工程中正常工作。
[GeneratedPluginRegistrant registerWithRegistry:self];
return [super application:application didFinishLaunchingWithOptions:launchOptions];
}
@end
2.从iOS页面打开Flutter页面
要在 iOS 页面中嵌入或导航到 Flutter 页面,通常有两种方式:全屏替换为 Flutter 页面或者在一个现有的 ViewController 中添加 Flutter 视图。
a.打开一个完整的flutter页面
思路:通过flutter引擎创建flutterViewController,然后当ios视图打开即可。 注意这里的创建启动flutter引擎,后续要专门理解flutter引擎.
js
// Swift 示例
//创建flutter引擎
let flutterEngine = FlutterEngine(name: "my flutter engine")
//启动flutter引擎
flutterEngine.run();
let flutterViewController = FlutterViewController(engine: flutterEngine, nibName: nil, bundle: nil)
self.present(flutterViewController, animated: true, completion: nil)
b.在iOS视图中添加局部的Flutter视图
实现步骤
- 创建并运行一个 Flutter 引擎:Flutter 引擎负责管理 Dart 代码的执行环境。
- 创建一个 Flutter 容器视图(
FlutterViewController
或者FlutterView
) :这个视图将承载 Flutter 界面。 - 将 Flutter 容器视图添加到现有的
UIViewController
中。
js
import UIKit
import Flutter
class MyExistingViewController: UIViewController {
private var flutterEngine: FlutterEngine?
private var flutterViewController: FlutterViewController?
override func viewDidLoad() {
super.viewDidLoad()
// 初始化 Flutter 引擎
self.flutterEngine = FlutterEngine(name: "my flutter engine")
self.flutterEngine?.run();
// 创建 FlutterViewController
self.flutterViewController = FlutterViewController(engine: self.flutterEngine, nibName: nil, bundle: nil)
// 设置 FlutterViewController 的 frame
if let flutterVC = self.flutterViewController {
flutterVC.view.frame = CGRect(x: 0, y: 100, width: self.view.bounds.width, height: 300) // 根据需要调整位置和大小
// 将 Flutter 视图添加到当前 ViewController 中
self.addChild(flutterVC)
self.view.addSubview(flutterVC.view)
flutterVC.didMove(toParent: self)
}
}
}
在 Flutter 端
确保你的 Flutter 工程有一个入口点可以被上述 FlutterViewController
加载。
通常情况下,默认的 lib/main.dart
文件中的 MaterialApp
或者 CupertinoApp
就足够了。如果你希望特定的页面作为 Flutter 视图加载,请确保相应的路由配置正确。
js
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: MyHomePage(),
);
}
}
class MyHomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Embedded Flutter View'),
),
body: Center(
child: Text('Hello from Flutter!'),
),
);
}
}
这样,当你运行 iOS 应用时,指定区域将会显示来自 Flutter 的内容。
- 性能考虑 :频繁地创建和销毁
FlutterEngine
会影响性能。尝试复用同一个FlutterEngine
实例。 - 生命周期管理 :当父
UIViewController
被销毁时,记得清理相关的资源,比如停止FlutterEngine
和移除FlutterViewController
。
二、关于FlutterEngine及其复用
1.什么是FlutterEngine?
FlutterEngine
是 Flutter 框架中的核心组件之一,它负责管理 Dart 代码的执行环境,包括 Dart VM(虚拟机)、Dart 和 Objective-C/Swift 之间的通信桥梁、渲染引擎等。简单来说,FlutterEngine
是运行 Flutter 应用程序的核心引擎。
在混合开发中,当你需要将 Flutter 集成到现有的 iOS 应用时,通常会使用 FlutterEngine
来启动和管理 Flutter 的运行环境。每个 FlutterEngine
实例都是独立的,拥有自己的 Dart VM 和状态,这意味着不同的 FlutterEngine
实例之间不会共享状态。 (这就是为什么要复用FlutterEngine, 不然不同页面的状态都不共享了,而且新建一个FlutterEngine内存大概是2M?)
创建一个新的 FlutterEngine
实例是一个相对耗资源的操作,因为它需要初始化 Dart VM、加载必要的资源等。因此,在混合应用中频繁地创建和销毁 FlutterEngine
实例可能会导致性能问题,如内存占用增加、启动时间延长等。为了避免这些问题,建议复用同一个 FlutterEngine
实例来处理多个 Flutter 页面或视图的显示需求。
2.FlutterEngine复用
以下是如何在 iOS 应用中创建并复用一个 FlutterEngine
实例的示例:
b. 创建并启动 FlutterEngine
首先,你需要在应用启动时创建并启动一个 FlutterEngine
实例。这通常可以在 AppDelegate
中完成:
js
import UIKit
import Flutter
@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
var flutterEngine : FlutterEngine?
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
// 创建并启动 Flutter 引擎
self.flutterEngine = FlutterEngine(name: "my flutter engine")
self.flutterEngine?.run()
return true
}
}
b.使用已有的 FlutterEngine
实例
接下来,在你需要显示 Flutter 页面的地方,可以使用已经创建好的 FlutterEngine
实例来创建 FlutterViewController
或者 FlutterView
。
这样做的好处是,所有的 Flutter 页面或视图都可以共享同一个 FlutterEngine
实例,避免了重复创建和销毁 FlutterEngine
带来的性能开销。
例如,在某个 UIViewController
中嵌入 Flutter 视图:
swift
class MyExistingViewController: UIViewController {
private var flutterEngine: FlutterEngine?
private var flutterViewController: FlutterViewController?
override func viewDidLoad() {
super.viewDidLoad()
// 获取 AppDelegate 中创建的 FlutterEngine 实例
if let appDelegate = UIApplication.shared.delegate as? AppDelegate {
self.flutterEngine = appDelegate.flutterEngine
// 创建 FlutterViewController 使用相同的 FlutterEngine
self.flutterViewController = FlutterViewController(engine: self.flutterEngine, nibName: nil, bundle: nil)
// 设置 FlutterViewController 的 frame
if let flutterVC = self.flutterViewController {
flutterVC.view.frame = CGRect(x: 0, y: 100, width: self.view.bounds.width, height: 300) // 根据需要调整位置和大小
// 将 FlutterViewController 添加为子控制器
self.addChild(flutterVC)
self.view.addSubview(flutterVC.view)
flutterVC.didMove(toParent: self)
}
}
}
}
c.注意:上面打开的flutter页面状态是隔离的
a\b创建的flutter页面并没有指定路由,他么创建的flutterViewController,对应的是flutter的工程入口的界面。
即使复用了FlutterEngine,但是每次创建FlutterVC相当于启动了一个新的Flutter应用实例。不同的Flutter应用实例页面运行在不同的isolate中,状态是隔离的。
如果你只是跳转到另一个 Flutter 路由(route),而不是重新创建新的 FlutterViewController
或重新运行 Dart 的 main()
函数,那么这些页面是运行在同一个 isolate 中,状态是共享的。
d.什么是 "启动一个新的 Flutter 应用实例"?
less
let flutterVC = FlutterViewController(engine: engine, nibName: nil, bundle: nil)
这会触发 Flutter 引擎加载并运行你的 main()
方法,启动一个 Flutter App 实例,相当于启动一个新的"应用上下文"。它会创建一个新的 isolate,因此与之前页面的状态是隔离的。
⚠️ 这就是为什么前面说多个 FlutterViewController
是状态隔离的原因 ------ 它们各自都跑了一个新的 isolate 和新的 Flutter App 实例。
e.如果只是跳转路由呢?
假设你在 Flutter 内部使用了导航器(Navigator)进行跳转:
less
Navigator.push(
context,
MaterialPageRoute(builder: (context) => SecondPage()),
);
或者通过命名路由:
arduino
Navigator.pushNamed(context, '/second');
这种情况下:
- 只是在当前 isolate 内部切换页面;
- 没有重启 isolate,也没有重新执行
main()
; - 所以所有的状态(比如全局变量、InheritedWidget、Provider、Riverpod、Bloc、GetX 等)都是保留的;
✅ 这些页面之间是状态共享的!
✅ 示例场景对比
场景 | 是否新建 isolate | 页面状态是否共享 |
---|---|---|
使用 Navigator.push 跳转新页面 |
❌ 否 | ✅ 是 |
创建新的 FlutterViewController |
✅ 是 | ❌ 否(默认隔离) |
使用 FlutterEngine + 新的 FlutterViewController |
✅ 是 | ❌ 否(每次 new 都是一个新 isolate) |
f.如何做到"只启动一次 Flutter 应用实例",后续只跳转路由?
✅ 方案一:复用同一个 FlutterViewController
- 在首次打开 Flutter 页面时创建一个
FlutterViewController
; - 第二次打开另一个 Flutter 页面时,不再新建,而是通过 Platform Channel 告诉已有的 Flutter 页面去跳转路由;
- 这样所有页面都在同一个 isolate 中,状态不会丢失。
Flutter 端实现路由跳转接口:
js
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
initialRoute: '/',
routes: {
'/': (context) => HomePage(),
'/second': (context) => SecondPage(),
},
);
}
}
// 提供一个方法给原生调用
Future<void> navigateTo(String route) async {
WidgetsBinding.instance.addPostFrameCallback((_) {
Navigator.of(myGlobalKey.currentContext!).pushNamed(route);
});
}
iOS 端调用这个方法:
csharp
[channel invokeMethod:@"navigateTo" arguments:@"/second"];
这样就能做到:
- 只启动一次 Flutter 引擎和 isolate;
- 所有页面都在同一个 Flutter 应用中;
- 状态不丢失,数据可共享;
✅ 总结一句话:
如果你只跳转 Flutter 内部路由,而不是每次都新建 FlutterViewController
,那么这些页面运行在同一个 isolate 中,状态是共享的,不会被隔离。
g.flutter引擎预加载
FlutterEngine
的预加载优化是一个非常关键的性能优化点。它能显著减少首次打开 Flutter 页面时的"白屏时间"和"启动延迟",从而提升用户体验。
FlutterEngine 预加载 是指:在用户真正需要打开 Flutter 页面之前,提前初始化并运行 FlutterEngine,这样当用户真正点击进入 Flutter 页面时,就能立刻展示内容,几乎无等待。
⚡️ 提升用户体验
- 用户点击跳转后,页面响应更快;
- 减少"白屏"或"卡顿"现象;
💡 技术原因
FlutterEngine
初始化(包括 Dart VM 启动)耗时较长(几十到几百毫秒);- 如果等到用户点击才初始化,会明显感受到"卡顿";
- 而如果在 App 启动时就预加载好,后续打开 Flutter 页面就几乎是"秒开"。
App 启动时预加载
swift
import UIKit
import Flutter
@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
var flutterEngine: FlutterEngine?
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
// ✅ 预加载 FlutterEngine
flutterEngine = FlutterEngine(name: "my_flutter_engine")
flutterEngine?.run()
return true
}
}
二、Flutter Engine 的 attach和deattach
在 Flutter 混合开发中,FlutterEngine
与 FlutterViewController
的关系非常紧密。我们通常会复用一个 FlutterEngine
来提升性能,但有时我们也需要在多个 FlutterViewController
之间切换 ------ 这就涉及到 attach 和 detach 操作。
FlutterEngine |
Flutter 的运行时核心,包含 Dart VM、渲染引擎等,负责执行 Flutter App 的逻辑 |
---|---|
FlutterViewController |
UIKit 中的控制器,负责展示 Flutter 渲染的内容(即 FlutterView) |
attachToFlutterEngine(_:) |
将某个 FlutterViewController 绑定到 FlutterEngine 上,使其可以显示和交互 |
detachFromFlutterEngine() |
将当前 ViewController 从 Engine 解绑,停止其对 Flutter 的控制 |
a.FlutterEngine attach
🔗 attachToFlutterEngine(engine: FlutterEngine) 将当前 FlutterViewController
绑定到指定的 FlutterEngine
。
- 把这个 VC 的
FlutterView
注册为 Engine 的主窗口视图; - 开始接收 Engine 的渲染输出;
- 开始处理用户输入事件(如点击、手势);
- 开始执行 Flutter 插件逻辑(比如传感器、相机等);
less
let engine = FlutterEngine(name: "my_engine")
engine.run()
let vc = FlutterViewController(engine: nil, nibName: nil, bundle: nil)
vc.attach(to: engine) // 绑定 Engine 到该 VC
🔓 detachFromFlutterEngine()
将当前 FlutterViewController
从它所绑定的 FlutterEngine
解绑。
- 停止渲染内容;
- 停止响应用户输入;
- 停止 Flutter 插件功能;
- 不会销毁 Engine 或 ViewController 本身;
- 页面状态仍然保留(除非手动 dispose);
scss
vc.detach() // 解除绑定
为什么要有 attach/detach?
场景 1:复用同一个 Engine,切换不同页面 你有多个 Flutter 页面,希望共享同一个 Engine,避免重复初始化:
方式 | 是否复用 Engine | 是否保留状态 |
---|---|---|
创建新 FlutterViewController + 新 Engine | ❌ | ❌ |
创建新 FlutterViewController + 复用 Engine(attach) | ✅ | ✅ |
复用同一个 FlutterViewController | ✅ | ✅(更简单) |
使用 attach/detach 在多个 VC 之间切换,保持 Engine 复用。 |
📌 场景 2:实现"Flutter 页面热切换"或"Tab 切换" 你可以:
- 预加载多个 FlutterViewController;
- 根据 Tab 点击动态 attach 不同的 VC;
- 实现类似原生 TabBar 的体验;
场景 3:节省内存 & 性能优化
- 如果某个 Flutter 页面暂时不用,可以 detach;
- Engine 依然在后台运行,下次 attach 很快恢复;
- 相比完全销毁 VC 和 Engine,性能更高;
注意点 | 说明 |
---|---|
❗不能同时 attach 多个 VC | 同一时刻只能有一个 VC 被 attach 到 Engine |
❗必须先 run Engine 再 attach | Engine 必须已经启动,否则 attach 会失败 |
❗不要频繁 detach/attach | 虽然比重建快,但仍有一定开销 |
❗注意 retain cycle | 确保 VC 和 Engine 的引用关系正确,避免内存泄漏 |
attach 是将 FlutterViewController 与 FlutterEngine 建立连接,使其能够显示和交互;detach 是断开连接,停止渲染和交互。合理使用这两个方法,可以在混合开发中实现 Flutter 页面的高效复用、状态保留和灵活切换。
三、混合开发经典导航困境
简单总结
1.上面提到了iOS打开flutter页面要使用FlutterEngine实例,创建Flutter界面,然后在iOS中打开。
2.但是如果每次都创建FlutterEngine实例,内存暴涨、性能差、flutter页面间的数据不能共享。因为每个FlutterEngine实例相当于一个独立的flutter应用。新建Flutter实例无异于重启新的应用。
3.所以要复用FlutterEngine实例,确保所有flutter都属于同一个FlutterEngine(Flutter应用),保证flutter页面间的数据能共享。
4.如果创建了一个FlutterEngine实例,而打开新的Flutter页面是在这个FlutterEngine里面,那么不会创建新的FlutterEngine实例,数据是共享的。
经典导航困境
🧭 场景描述 你有以下页面:
- iOS 页面:A(原生) → D(原生) - Flutter 页面:B、C 导航路径是:
A(iOS) ➡️ B(Flutter) ➡️ D(iOS) ➡️ C(Flutter)
✅ 总结一句话:
从 Flutter 页面 B 跳转到原生 D 时,B 会被销毁(除非你 retain),但 FlutterEngine 仍存活;D 再跳转到 C 时,如果新建 FlutterViewController,就会创建新 isolate,状态隔离。要保留状态,应复用已有 FlutterViewController 并通过 channel 控制路由跳转。 你想知道:
1. 从 B(Flutter)跳转到 D(iOS)时,B 是否会被销毁?
2. 从 D 跳转回 C(Flutter)时,是否需要重新创建 FlutterEngine?
3. 整个过程中 FlutterEngine 和页面状态如何变化?
✅ 分析开始
🔁 前提假设(便于理解)
- 使用的是一个复用的 FlutterEngine 实例(不是每次都新建)
B
和C
是两个不同的 Flutter 页面(路由不同或入口不同)- 导航方式使用标准的混合开发方式(如通过 UINavigationController push 或 present)
📌 Step 1: A → B(iOS → Flutter)
行为:首次打开 Flutter 页面 B
- 创建了一个
FlutterViewController(B)
,绑定到已有的FlutterEngine
- Flutter 引擎启动一个新的 isolate(默认行为),运行
main()
方法 - 加载并显示页面 B
✅ 此时 FlutterEngine 已创建并运行,isolate 启动,页面 B 状态独立
📌 Step 2: B → D(Flutter → iOS)
行为:关闭 Flutter 页面 B,跳转到原生页面 D
这取决于你是怎么"关闭"B 的: 情况一 :你 pop 或 dismiss 了 FlutterViewController(B)
php
self.navigationController?.popViewController(animated: true)
// 或
self.dismiss(animated: true, completion: nil)
👉 此时:
FlutterViewController(B)
被移除;- 如果你没有对它做任何 retain 操作,它将被释放;
- 但 FlutterEngine 并不会自动销毁!
✅ 结论:页面 B 被销毁(释放),但 FlutterEngine 仍然存活(只要你不手动 stop 它)
📌 Step 3: D → C(iOS → Flutter)
行为:再次打开 Flutter 页面 C
你现在有两种做法: ✅ 方式一:新建一个新的 FlutterViewController(C)
,使用同一个 FlutterEngine
这会触发:
- 新建一个 isolate(因为这是新的 FlutterViewController)
- 再次运行 Dart 的
main()
函数 - 显示 Flutter 页面 C
php
let flutterVC = FlutterViewController(engine: existingFlutterEngine, nibName: nil, bundle: nil)
navigationController?.pushViewController(flutterVC, animated: true)
❌ 页面 C 是全新的 isolate,无法访问之前页面 B 的状态 ✅ FlutterEngine 复用了,所以性能更好,但 isolate 不共享
✅ 方式二:复用之前的 FlutterViewController(B)
,只是跳转路由
如果你希望复用同一个 isolate,保持状态,你应该:
- 不要销毁
FlutterViewController(B)
- 而是通过 Platform Channel 调用 Dart 的方法,让它跳转路由
js
//在 iOS 中:
[channel invokeMethod:@"navigateTo" arguments:@"/c"];
//在 Dart 中:
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
initialRoute: '/',
routes: {
'/': (context) => PageB(),
'/c': (context) => PageC(),
},
);
}
}
这样就可以做到:
✅ 不新建 FlutterViewController,也不重启 isolate
✅ 页面 B → 页面 C,状态保留,数据共享
🧠 总结表格对比
步骤 | 页面 | 行为 | FlutterEngine 是否复用 | Isolate 是否复用 | 页面状态是否保留 |
---|---|---|---|---|---|
A → B | iOS → Flutter | 首次加载 Flutter 页面 | ❌ 第一次创建 | ✅ 新建 | ❌ 初始状态 |
B → D | Flutter → iOS | dismiss/pop FlutterViewController | ✅ 复用 | ❌ 销毁 | ❌ 页面 B 状态丢失 |
D → C | iOS → Flutter | 新建 FlutterViewController | ✅ 复用 | ❌ 新建 | ❌ 页面 C 是全新状态 |
D → C | iOS → Flutter | 通过 channel 跳转路由 | ✅ 复用 | ✅ 复用 | ✅ 页面状态保留 |
✅ 最佳实践建议
如果你想实现: ✅ 页面间状态共享 + 快速切换 Flutter 页面
你应该:
- 只创建一次 FlutterEngine,并复用它
- 不要频繁地创建/销毁 FlutterViewController
- 通过 Platform Channel 控制 Flutter 页面跳转
- 使用全局状态管理工具(如 Riverpod、GetX、Bloc)统一管理状态
四、如何全局只复用一个FlutterViewController
三中提到的:
✅ 方式二:复用之前的 FlutterViewController(B),怎么才能实现复用呢,打开任意flutter界面,只是首次打开flutter界面时创建一个FlutterViewController, 其它情况都复用该FlutterViewController
✅ 问题核心:
如何在 iOS 混合工程中复用同一个
FlutterViewController
,打开任意 Flutter 页面时都只创建一次,后续只是通过跳转路由来切换页面?
🎯 目标
我们希望达到的效果是:
特性 | 状态 |
---|---|
创建 FlutterEngine | 只创建一次 |
创建 FlutterViewController | 只创建一次 |
打开新 Flutter 页面 | 通过路由跳转(不重启 isolate) |
页面状态 | 复用并保持 |
✅ 实现思路总结
1. 全局管理一个单例的 FlutterViewController
2. 首次打开 Flutter 页面时创建它
3. 后续打开 Flutter 页面时不再新建,而是通过 Platform Channel 控制跳转路由
4. Flutter 内部使用 Navigator 跳转页面,保持状态不变
Step 1: 创建一个 FlutterViewController 单例类
你可以创建一个 FlutterManager.swift
来统一管理 Flutter 的生命周期。
js
import UIKit
import Flutter
class FlutterManager: NSObject {
// MARK: - 单例
static let shared = FlutterManager()
private var flutterEngine: FlutterEngine?
private var flutterViewController: FlutterViewController?
private var currentContainerVC: UIViewController?
private override init() {
super.init()
// 初始化 FlutterEngine(只执行一次)
flutterEngine = FlutterEngine(name: "shared_flutter_engine")
flutterEngine?.run()
// 创建 FlutterViewController(只执行一次)
flutterViewController = FlutterViewController(engine: flutterEngine, nibName: nil, bundle: nil)
// 注册导航 Channel
registerNavigationChannel()
}
// MARK: - 打开 Flutter 页面统一入口
func openFlutterPage(from viewController: UIViewController, route: String) {
self.currentContainerVC = viewController
if !isFlutterViewLoaded() {
// 首次加载:push 或 present
viewController.navigationController?.pushViewController(flutterViewController!, animated: true)
} else {
// 已经加载过,通过路由跳转
navigateTo(route: route)
}
}
// MARK: - 是否已经加载
private func isFlutterViewLoaded() -> Bool {
return flutterViewController?.isViewLoaded ?? false
}
// MARK: - 路由跳转
func navigateTo(route: String) {
guard let vc = flutterViewController else { return }
let channel = FlutterMethodChannel(
name: "navigation_channel",
binaryMessenger: vc as! FlutterBinaryMessenger
)
channel.invokeMethod("navigateTo", arguments: route)
}
// MARK: - 获取当前 FlutterViewController(供外部 push/pop 使用)
func getFlutterViewController() -> FlutterViewController? {
return flutterViewController
}
// MARK: - 注册导航 Channel
private func registerNavigationChannel() {
guard let vc = flutterViewController else { return }
let channel = FlutterMethodChannel(
name: "navigation_channel",
binaryMessenger: vc as! FlutterBinaryMessenger
)
channel.setMethodCallHandler { [weak self] call, result in
if call.method == "navigateTo" {
let route = call.arguments as? String ?? "/"
self?.navigateInternally(to: route)
result(nil)
} else {
result(FlutterMethodNotImplemented)
}
}
}
private func navigateInternally(to route: String) {
flutterViewController?.navigator?.pushNamed(route)
}
}
Step 2: 在 Flutter 中定义路由跳转方法
typescript
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
initialRoute: '/',
routes: {
'/': (context) => HomePage(),
'/pageB': (context) => PageB(),
'/pageC': (context) => PageC(),
},
);
}
}
// 提供给原生调用的方法
void setupNavigationChannel() {
const channel = MethodChannel('navigation_channel');
channel.setMethodCallHandler((call) async {
if (call.method == 'navigateTo') {
final String route = call.arguments;
navigatorKey.currentState?.pushNamed(route);
}
});
}
final navigatorKey = GlobalKey<NavigatorState>();
Step2 ✅ Flutter 端配置(main.dart)
typescript
import 'package:flutter/material.dart';
final navigatorKey = GlobalKey<NavigatorState>();
void setupNavigationChannel() {
const channel = MethodChannel('navigation_channel');
channel.setMethodCallHandler((call) async {
if (call.method == 'navigateTo') {
final String route = call.arguments;
navigatorKey.currentState?.pushNamed(route);
}
});
}
void main() {
setupNavigationChannel();
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
navigatorKey: navigatorKey,
initialRoute: '/',
routes: {
'/': (context) => PageB(),
'/pageC': (context) => PageC(),
},
);
}
}
3. ✅ iOS A → Flutter B
php
FlutterManager.shared.openFlutterPage(from: self, route: "/")
4. ✅ Flutter B → iOS D
在 Flutter 页面中提供一个方法调用原生跳转:
csharp
Future<void> goToNativePageD() async {
final channel = MethodChannel('native_navigation');
await channel.invokeMethod('openNativePageD');
}
在原生端监听并跳转:
swift
let nativeChannel = FlutterMethodChannel(name: "native_navigation",
binaryMessenger: flutterViewController as! FlutterBinaryMessenger)
nativeChannel.setMethodCallHandler { [weak self] call, result in
if call.method == "openNativePageD" {
let dVC = NativePageDViewController()
self?.currentContainerVC?.navigationController?.pushViewController(dVC, animated: true)
result(nil)
} else {
result(FlutterMethodNotImplemented)
}
}
5. ✅ iOS D → Flutter C
php
FlutterManager.shared.openFlutterPage(from: self, route: "/pageC")
6. 🧭 整体流程图
scss
iOS A ───openFlutterPage("/")────▶ Flutter B
▲
│ MethodChannel.invoke("openNativePageD")
▼
Native Navigation
▲
│ pushViewController(NativePageD)
▼
iOS D ───openFlutterPage("/pageC")───▶ Flutter C
总结
通过 FlutterManager 全局缓存 FlutterViewController,并配合 MethodChannel 控制路由跳转,可以完美支持 iOS A → Flutter B → iOS D → Flutter C 的完整导航路径,所有 Flutter 页面共享状态,无需重复创建。
五、pop掉Flutter界面会发生什么
🔄 iOS A
➡️ Flutter B
➡️ pop 回到 iOS A
➡️ 再次进入 Flutter B
这其实是一个典型的 Flutter 页面在混合导航中被多次复用的问题。我们要重点讨论:
- FlutterViewController 是否真的保留?
- 再次进入 Flutter B 时是否"重启"了页面?
- 页面状态是否保留?
1.回顾
上面定义的 FlutterManager
是这样工作的:
功能 | 行为 |
---|---|
创建 FlutterEngine | 只创建一次,长期运行 |
创建 FlutterViewController | 只创建一次,全局缓存 |
路由跳转 | 通过 MethodChannel 控制 Navigator.pushNamed |
也就是说:
❗ 即使你 pop 或 dismiss FlutterViewController,它也不会被释放(只要你不主动设置为 nil)
2.✅ 二、完整流程分析
📲 步骤 1:iOS A → Flutter B
php
FlutterManager.shared.openFlutterPage(from: self, route: "/")
- FlutterEngine 已启动
- FlutterViewController 已创建
- Push 到 UINavigationController 中
- Dart 端显示
/
页面(即 Flutter B)
✅ 成功打开 Flutter 页面。
📲 步骤 2:Flutter B ➡️ pop 回到 iOS A
用户点击返回按钮或调用:
php
navigationController?.popViewController(animated: true)
⚠️ 注意:
- 这个操作只是从原生导航栈中移除了
FlutterViewController
- 但 FlutterViewController 实例本身没有被销毁
- 它仍然被
FlutterManager.shared.flutterViewController
强引用着
✅ Flutter 页面没有被销毁,只是隐藏了。
📲 步骤 3:再次从 iOS A 打开 Flutter B
php
FlutterManager.shared.openFlutterPage(from: self, route: "/")
此时:
flutterViewController
已存在- 不会重新创建 FlutterViewController 和 Engine
- 会调用
navigateTo(route: "/")
- Dart 侧执行
Navigator.pushNamed("/")
❗ 但是这里有个关键点要特别注意:
如果当前 Flutter 页面已经是
/
,那么pushNamed("/")
相当于再 push 一次相同的页面,会导致页面堆栈叠加!
✅ 三、此时 Flutter B 发生了什么?
🧠 情况 1:你调用了 navigateTo(route: "/")
Dart 侧执行的是:
arduino
navigatorKey.currentState?.pushNamed("/");
所以:
- 又 push 了一层新的
/
页面 - Flutter 页面堆栈变成:
/
➜/
- 第一次
/
页面的状态还保留 - 新的
/
页面是全新的实例(除非你手动控制)
❌ 页面重复,状态不共享。
六、3种路由混合方案
- 单引擎、单FlutterViewController容器方案。通过设定不同的参数,容器渲染不同的Flutter页面,从而实现多页面支持,同时通过截图实现页面转场;(优点明显,缺点也明显)
- 单引擎、多FlutterViewController容器方案。单个容器渲染单个页面,也即容器和页面一对一的关系,通过切换不同的容器来实现多页面支持;(优点突出)
- 多引擎、多FlutterViewController容器方案。引擎、容器、页面皆为一对一关系,通过切换不同引擎来实现多页面支持。(理论上的,实际上没有人用)
1.单引擎、单FlutterViewController的缺点
a.由于只存在一个容器,在页面之间进行切换时,页面无法『感知』自己的生命周期,而在实际开发中,感知页面生命周期无论是在业务还是技术上都是必要的,比如一些埋点和监控逻辑。
b.其次必须通过截图的方式来保存上个页面的『状态』,而CPU截图保存与加载的耗时操作带来的资源占用,会影响主线程渲染,出现偶尔的白屏和黑屏问题。不仅如此,截图带来的内存占用则在压入过多页面时,增加了OOM的风险。
2. 单引擎、多FlutterViewController的优点
a.通过容器的生命周期赋予页面同等的生命周期感知能力。
b.同时多容器模式也不再需要通过截图来实现老页面还原,避免了因截图引入的黑白屏问题,降低了OOM风险。
3.FlutterBoost(单引擎,多FlutterViewController)
参考: zyqhi.github.io/2020/11/19/...

在最新的FlutterBoost页面管理方案中,我们可以看到,从Native侧的视角来看:
- 页面栈管理全部由UINavigationController管理,FlutterViewController容器和普通的UIViewContorller没有任何区别,可以完全相容,对于UINavigationController而言是完全透明的;
- 页面切换时,通过通知FlutterEngine attach不同的FlutterViewController容器,从而实现多页面支持。
而从Flutter侧的视角来看:
- 只需要接收来自FlutterEngine的通知,在容器切换时,渲染不同的Flutter页面即可,职责单一且简单;
- BoostContainerManager本身是一个StatefulWidget,当Native侧切换到不同的容器时,BootContainerManager改变其子Widget,从而实现与容器的同步。
注:这里Native侧的页面管理以UINavigationController为例,采用其他页面栈管理方案原理是类似的。
总结下来就是:FlutterBoost采用单引擎、多FlutterViewController容器的模式来管理页面。这种方案下,切换不同的页面,就是切换不同的容器,然后通知引擎渲染不同的Flutter Widget。