Flutter iOS Add2App 生命周期实践:引擎预热、ViewController 附着/解除与 EarlGrey 自动化验证
【免费下载链接】flutterFlutter makes it easy and fast to build beautiful apps for mobile and beyond项目地址: https://gitcode.com/GitHub_Trending/flutter41/flutter
本文基于 Flutter 官方仓库中 ios_add2app_life_cycle 集成测试应用的文档与源码展开,讲解把 Flutter 以模块(Add to App)形式嵌入原生 iOS 应用时,如何管理FlutterEngine与FlutterViewController的完整生命周期——包括引擎预热(pre-warm)、View Controller 的附着与解除、全屏接管、以及用 EarlGrey 框架对原生与 Flutter 混合界面做 UI 自动化验证,并给出可直接复现的构建与测试命令流程。
测试应用定位与整体结构
ios_add2app_life_cycle位于dev/integration_tests/目录下,是一个专门用于验证 Add2App(Add to App)场景下生命周期行为的集成测试工程。它由两部分组成:
- flutterapp/:一个 Flutter module(模块工程),对应 pubspec 名称为
ios_add2app_life_cycle_flutter,其 pubspec.yaml 中声明了module段:
module: androidPackage: com.example.iosadd2appflutter iosBundleIdentifier: com.example.iosAdd2appFlutter这两个标识符只用于保证 Flutter 工具链在增改 assets 和 plugins 时保持一致性,与宿主原生应用自己的 bundle id 相互独立——宿主应用可以使用完全不同的标识符。pubspec 中同时注明:version字段只影响直接flutter run时的 Runner app,对嵌入的原生宿主应用没有影响。
- ios_add2app/:原生 iOS 宿主应用,包含
AppDelegate、SceneDelegate、MainViewController、FullScreenViewController等 Objective-C 源文件,以及基于 Xcode workspace 的工程文件。
宿主工程通过 Podfile 把两部分串起来,这是 Add2App 集成方式的典型配置:
platform :ios, '15.0' flutter_application_path = 'flutterapp/' # 加载 Flutter 模块生成的 podhelper,负责 Flutter 相关 pod 的安装 load File.join(flutter_application_path, '.ios', 'Flutter', 'podhelper.rb') target 'ios_add2app' do install_all_flutter_pods(flutter_application_path) # 宿主主 target:安装 Flutter 全部 pod pod 'EarlGreyApp' # 应用侧 EarlGrey(UI 自动化客户端) end target 'ios_add2appTests' do install_flutter_engine_pod(flutter_application_path) # 测试 target:只装引擎 pod pod 'EarlGreyTest' # 测试侧 EarlGrey(XCTest 断言库) end post_install do |installer| flutter_post_install(installer) # Flutter 要求的 post_install 处理 end可以看到 Add2App 与独立 Flutter 应用的关键差异:pod 安装逻辑不是模板生成的完整 podspec,而是通过.ios/Flutter/podhelper.rb提供的install_all_flutter_pods/install_flutter_engine_pod辅助函数完成,宿主 target 与测试 target 按需分别安装。
README 描述的五大生命周期场景
该应用的 README 明确了它要演示和验证的 Add2App 基本功能,以原生 iOS ViewController 作为基线并展示与 Flutter 的交互。文档列出了五类场景:
- 一个普通 iOS 视图控制器(
UIViewController),类似flutter create默认模板(NativeViewController.m); - 一个接管全屏的
FlutterViewController子类,分别从冷启动(cold/fresh engine state)和热引擎(warm engine state)两种状态演示(FullScreenViewController.m); - 以子视图方式 push 一个 FlutterViewController 的演示;
- 同时展示原生视图和 Flutter 视图,并通过 platform channel 相互交互的演示(HybridViewController.m);
- 同时运行两个 FlutterViewController(双引擎)的演示(DualViewController.m)。
对应地,README 指出该工程重点验证五个关键能力(见 IntegrationTests.m):
- 能够预热引擎,并让 ViewController 附着/解除(attach/detach)到它;
- 能够通过 platform channel 在不同视图间通信;
- 能够同时运行两个引擎实例;
FlutterViewController在不再使用时能够被释放(另由 FlutterViewControllerTests.m 验证);FlutterEngine在不再使用时能够被释放。
需要说明的是,从当前仓库的源码树看,ios_add2app 目录下实际保留的核心文件为AppDelegate、SceneDelegate、MainViewController、FullScreenViewController与main.m;README 中提到的 HybridViewController、DualViewController 等场景文件并未出现在当前目录中,可推断当前工程是以「预热引擎 + 全屏冷启动 + 语义通知验证」这条主线作为最小可运行、可自动化的核心验证集,其余场景为文档记录的完整能力面。下文以源码中可证实的实现为准展开。
引擎预热:AppDelegate 中创建并运行 FlutterEngine
Add2App 场景与独立应用最大的不同在于:Flutter 界面只是宿主应用中的「一个页面」,因此FlutterEngine的创建时机由开发者控制。本工程选择在应用启动时就创建并运行引擎,这正是 README 第一条要验证的「pre-warm the engine」:
AppDelegate.m
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { // 创建名为 "test" 的引擎实例(不绑定任何 FlutterProject 配置) self.engine = [[FlutterEngine alloc] initWithName:@"test" project:nil]; // 无自定义入口点,立即运行引擎——即完成"预热" [self.engine runWithEntrypoint:nil]; return [super application:application didFinishLaunchingWithOptions:launchOptions]; }要点:
initWithName:project:中project:nil表示使用默认工程配置;引擎名"test"用于区分多个引擎实例(当同时运行多个引擎时,名称必须互不相同)。runWithEntrypoint:nil使用模块默认的main.dart入口,立即执行 Dart 代码、构建第一帧,从而把引擎放到「热」状态。之后任何FlutterViewController附着到该引擎都能以最短延迟出画面。- 引擎实例通过
AppDelegate的engine属性(在 AppDelegate.m 中声明为@property(nonatomic, strong, readwrite) FlutterEngine *engine)暴露给后续页面使用。
此外,AppDelegate 还实现了一个 EarlGrey 专用扩展,用于把 App 内的NSNotificationCenter暴露给测试进程:
@implementation GREYHostApplicationDistantObject (AppDelegate) - (NSNotificationCenter *)notificationCenter { return [NSNotificationCenter defaultCenter]; } @end这是后续「跨进程断言语义事件」能跑起来的前提。
应用骨架:SceneDelegate 与 MainViewController
SceneDelegate.m 遵循 UIScene 生命周期,创建 window 并以MainViewController为根视图构建导航栈:
MainViewController *mainViewController = [[MainViewController alloc] init]; UINavigationController *navigationController = [[UINavigationController alloc] initWithRootViewController:mainViewController]; navigationController.navigationBar.translucent = NO; self.window.rootViewController = navigationController; [self.window makeKeyAndVisible];MainViewController.m 用一个竖向UIStackView动态添加演示按钮。当前实现注册了Full Screen (Cold)按钮,点击后执行:
- (void)showFullScreenCold { // 复用 AppDelegate 中预热的引擎 FlutterEngine *engine = [(AppDelegate *)[[UIApplication sharedApplication] delegate] engine]; FullScreenViewController *flutterViewController = [[FullScreenViewController alloc] initWithEngine:engine nibName:nil bundle:nil]; [self.navigationController pushViewController:flutterViewController animated:NO]; // 冷引擎场景下带动画的过渡会明显卡顿, // 且与原生导航头部的切换叠加后体验更差 }这里体现了两条 Add2App 实践要点:
FlutterViewController通过initWithEngine:nibName:bundle:直接绑定一个已存在的引擎,而不是让工具链新建引擎——引擎的生命周期由宿主掌控;- 源码注释明确说明:冷引擎首帧未就绪时开启 push 动画会产生明显的掉帧(janky transitions),因此此处关闭动画。这是嵌入场景中「引擎状态 × 转场动画」组合的实际取舍。
全屏接管与解除:FullScreenViewController 的 attach/detach
FullScreenViewController是一个极简的FlutterViewController子类(FullScreenViewController.h 仅声明类接口),真正的生命周期逻辑在 FullScreenViewController.m 中,它完整展示了「ViewController 附着/解除到预热引擎」这一核心能力:
-(void)viewWillAppear:(BOOL)animated { [super viewWillAppear:animated]; self.title = @"Full Screen Flutter"; // 全屏接管:隐藏导航栏,并允许下滑隐藏状态栏 self.navigationController.navigationBarHidden = YES; self.navigationController.hidesBarsOnSwipe = YES; } -(void)viewWillDisappear:(BOOL)animated { [super viewWillDisappear:animated]; // 恢复导航栏 self.navigationController.navigationBarHidden = NO; self.navigationController.hidesBarsOnSwipe = NO; if (self.isMovingFromParentViewController) { // 确认自己是"从父控制器中移除"(即返回导航栈), // 才执行解除附着。注释特别提示:若页面在跑 image_picker 等 // 可能触发 presented VC 的插件,不能无脑解除;如需 Flutter 侧 // 告知"何时可以真正离开",应通过 method channel 通信 [self.engine setViewController:nil]; } } -(BOOL)prefersStatusBarHidden { return true; // 全屏 Flutter 页面隐藏状态栏 }几个值得注意的细节:
viewWillDisappear中用isMovingFromParentViewController区分「被 pop 回导航栈」和「其他原因的消失」,只在确认离开时才调用[self.engine setViewController:nil],把引擎与视图控制器解绑。这一步是「引擎预热 + 多次附着/解除」模式的关键:解除后引擎保持热状态,下次再附着可以秒出画面;- 注释中提到的 image_picker 场景是一个真实工程陷阱:某些插件会 present 新视图导致
viewWillDisappear被调用,此时若误判为「页面退场」而解除引擎,会打断 Flutter 侧正在进行的流程; prefersStatusBarHidden返回YES,配合hidesBarsOnSwipe,实现沉浸式全屏。
自动化验证:EarlGrey 如何测「原生 + Flutter」混合界面
测试文件 IntegrationTests.m 使用 Google 的 EarlGrey 框架,它是专为「原生应用中嵌入其他渲染体系(如 Flutter)」这类混合 UI 设计的 UI 自动化方案——原生元素走 XCUITest,Flutter 元素通过语义(semantics)通知跨进程同步,从而让测试进程也能感知 Flutter 界面状态。
@interface FlutterTests : XCTestCase @end @implementation FlutterTests - (void)setUp { self.continueAfterFailure = NO; // 任一步失败立即中止,保证测试有序性 XCUIApplication *app = [[XCUIApplication alloc] init]; [app launch]; } - (void)testFullScreenCanPop { // 期望收到来自 App 进程的语义更新通知 XCTestExpectation *notificationReceived = [self expectationWithDescription:@"Remote semantics notification"]; // 通过 AppDelegate 的 EarlGrey 扩展拿到 App 侧 notificationCenter NSNotificationCenter *notificationCenter = [[GREYHostApplicationDistantObject sharedInstance] notificationCenter]; id observer = [notificationCenter addObserverForName:FlutterSemanticsUpdateNotification object:nil queue:nil usingBlock:^(NSNotification *notification) { // 断言触发通知的正是 FullScreenViewController(跨进程类名匹配) XCTAssertTrue([notification.object isKindOfClass:GREY_REMOTE_CLASS_IN_APP(FullScreenViewController)]); [notificationReceived fulfill]; }]; // 主窗口可见 [[EarlGrey selectElementWithMatcher:grey_keyWindow()] assertWithMatcher:grey_sufficientlyVisible()]; // 点击原生按钮 "Full Screen (Cold)" [[EarlGrey selectElementWithMatcher:grey_buttonTitle(@"Full Screen (Cold)")] performAction:grey_tap()]; [self waitForExpectationsWithTimeout:30.0 handler:nil]; [notificationCenter removeObserver:observer]; } @end这个用例把 README 列出的多条验证目标串成了一条可自动执行的链路:
- 启动 App(原生主界面,对应 README 中的原生基线视图);
- 点击原生按钮,push 出全屏 Flutter 页面(验证 attach 到预热引擎的路径);
- 监听
FlutterSemanticsUpdateNotification——当 Flutter 侧产生语义树更新时,引擎会经NSNotificationCenter发出该通知;测试进程借助 AppDelegate 暴露的notificationCenter桥接捕获它,并用GREY_REMOTE_CLASS_IN_APP(FullScreenViewController)确认通知对象确实是全屏 Flutter 页面。这等于证明了 Flutter 界面已实际渲染并进入语义激活状态(EarlGrey 的交互能力依赖语义层开启); - 30 秒超时内未收到通知则断言失败,失败时
continueAfterFailure = NO会立即终止该测试,便于定位。
构建与运行:build_and_test.sh 全流程
该工程用 build_and_test.sh 一条脚本完成「构建模块 → 安装 pods → 跑 Xcode 测试」,是 CI 中复现本测试的标准入口:
#!/usr/bin/env bash set -e cd "$(dirname "$0")" # 1. 以调试模式构建 Flutter 模块产物(模拟器、不签名) pushd flutterapp ../../../../bin/flutter build ios --debug --simulator --no-codesign popd # 2. 安装 pods(含 Flutter 辅助函数与 EarlGrey) pod install # 3. 用 workspace + scheme 直接执行测试 xcrun xcodebuild \ -workspace ios_add2app.xcworkspace \ -scheme ios_add2app \ -sdk "iphonesimulator" \ -destination "OS=latest,name=iPhone 12" test关键参数说明:
flutter build ios --debug --simulator --no-codesign:Add2App 集成时只需生成 Flutter 产物(App.framework 等)供 pod 安装引用,--no-codesign表示由 Xcode 在测试阶段自行签名;--debug模式下引擎支持热重启且语义层默认可用,利于调试;- 脚本用
../../../../bin/flutter显式指向 Flutter 仓库根目录下的 SDK 二进制,保证 CI 环境中使用与本仓库配套的 Flutter 工具链; xcodebuild通过-workspace ios_add2app.xcworkspace -scheme ios_add2app组织构建,-destination "OS=latest,name=iPhone 12"指定最新系统的 iPhone 12 模拟器——Add2App 测试只能跑在模拟器/真机上(依赖 UIScene 与 UI 交互),这正是它作为 integration test 而非 unit test 的原因。
从源码结构看的核心结论
- 引擎与页面的解耦是 Add2App 生命周期的核心:
FlutterEngine由AppDelegate在启动时创建并runWithEntrypoint:预热;FlutterViewController用initWithEngine:绑定引擎,pop 走时setViewController:nil解除绑定而引擎留存,实现「多次进出 Flutter 页面、引擎始终热态」的模式(FullScreenViewController.m); - 视图消失时机需要谨慎判断:
isMovingFromParentViewController只是第一道防线,源码注释进一步提示,若页面涉及 present 型插件,应通过 method channel 让 Flutter 侧确认「可以真正离开」,这是嵌入场景下引擎释放时机的正确姿势; - 混合 UI 可测性依赖 EarlGrey 的语义通知桥:App 进程侧的
FlutterSemanticsUpdateNotification经由宿主暴露的notificationCenter传给 XCTest 进程,使测试能在「原生按钮点击 → Flutter 语义树变化」这条完整链路上做可自动断言的验证(IntegrationTests.m); - 工程集成方式上,宿主工程通过
podhelper.rb的install_all_flutter_pods/install_flutter_engine_pod接入 Flutter 模块产物(Podfile),模块侧 pubspec 的module段保持工具链一致性标识,二者配合即为可复制的 Add2App 生命周期验证工程。
参考文件
| 文件 | 作用 |
|---|---|
| README.md | 测试目标与五大场景定义 |
| build_and_test.sh | 构建与测试入口脚本 |
| Podfile | Flutter pod 集成与 EarlGrey 依赖配置 |
| AppDelegate.m | 引擎预热与 EarlGrey 桥接 |
| MainViewController.m | 原生入口页与按钮导航 |
| FullScreenViewController.m | 全屏接管、attach/detach 与状态栏处理 |
| SceneDelegate.m | UIScene 生命周期与根导航栈 |
| IntegrationTests.m | EarlGrey 自动化验证用例 |
| flutterapp/pubspec.yaml | Flutter 模块配置(module 段) |
【免费下载链接】flutterFlutter makes it easy and fast to build beautiful apps for mobile and beyond项目地址: https://gitcode.com/GitHub_Trending/flutter41/flutter
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考