Flutter iOS Add2App 生命周期实践:引擎预热、ViewController 附着/解除与 EarlGrey 自动化验证

📅 发布时间:2026/9/7 9:50:30
Flutter iOS Add2App 生命周期实践:引擎预热、ViewController 附着/解除与 EarlGrey 自动化验证 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/目录下是一个专门用于验证 Add2AppAdd 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 # 应用侧 EarlGreyUI 自动化客户端 end target ios_add2appTests do install_flutter_engine_pod(flutter_application_path) # 测试 target只装引擎 pod pod EarlGreyTest # 测试侧 EarlGreyXCTest 断言库 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.mREADME 中提到的 HybridViewController、DualViewController 等场景文件并未出现在当前目录中可推断当前工程是以「预热引擎 全屏冷启动 语义通知验证」这条主线作为最小可运行、可自动化的核心验证集其余场景为文档记录的完整能力面。下文以源码中可证实的实现为准展开。引擎预热AppDelegate 中创建并运行 FlutterEngineAdd2App 场景与独立应用最大的不同在于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 与 MainViewControllerSceneDelegate.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/detachFullScreenViewController是一个极简的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 自动化方案——原生元素走 XCUITestFlutter 元素通过语义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 OSlatest,nameiPhone 12 test关键参数说明flutter build ios --debug --simulator --no-codesignAdd2App 集成时只需生成 Flutter 产物App.framework 等供 pod 安装引用--no-codesign表示由 Xcode 在测试阶段自行签名--debug模式下引擎支持热重启且语义层默认可用利于调试脚本用../../../../bin/flutter显式指向 Flutter 仓库根目录下的 SDK 二进制保证 CI 环境中使用与本仓库配套的 Flutter 工具链xcodebuild通过-workspace ios_add2app.xcworkspace -scheme ios_add2app组织构建-destination OSlatest,nameiPhone 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构建与测试入口脚本PodfileFlutter pod 集成与 EarlGrey 依赖配置AppDelegate.m引擎预热与 EarlGrey 桥接MainViewController.m原生入口页与按钮导航FullScreenViewController.m全屏接管、attach/detach 与状态栏处理SceneDelegate.mUIScene 生命周期与根导航栈IntegrationTests.mEarlGrey 自动化验证用例flutterapp/pubspec.yamlFlutter 模块配置module 段【免费下载链接】flutterFlutter makes it easy and fast to build beautiful apps for mobile and beyond项目地址: https://gitcode.com/GitHub_Trending/flutter41/flutter创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考