news 2026/9/7 9:50:24

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
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 应用时,如何管理FlutterEngineFlutterViewController的完整生命周期——包括引擎预热(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 宿主应用,包含AppDelegateSceneDelegateMainViewControllerFullScreenViewController等 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 的交互。文档列出了五类场景:

  1. 一个普通 iOS 视图控制器(UIViewController),类似flutter create默认模板(NativeViewController.m);
  2. 一个接管全屏的FlutterViewController子类,分别从冷启动(cold/fresh engine state)和热引擎(warm engine state)两种状态演示(FullScreenViewController.m);
  3. 以子视图方式 push 一个 FlutterViewController 的演示;
  4. 同时展示原生视图和 Flutter 视图,并通过 platform channel 相互交互的演示(HybridViewController.m);
  5. 同时运行两个 FlutterViewController(双引擎)的演示(DualViewController.m)。

对应地,README 指出该工程重点验证五个关键能力(见 IntegrationTests.m):

  1. 能够预热引擎,并让 ViewController 附着/解除(attach/detach)到它;
  2. 能够通过 platform channel 在不同视图间通信;
  3. 能够同时运行两个引擎实例;
  4. FlutterViewController在不再使用时能够被释放(另由 FlutterViewControllerTests.m 验证);
  5. FlutterEngine在不再使用时能够被释放。

需要说明的是,从当前仓库的源码树看,ios_add2app 目录下实际保留的核心文件为AppDelegateSceneDelegateMainViewControllerFullScreenViewControllermain.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附着到该引擎都能以最短延迟出画面。
  • 引擎实例通过AppDelegateengine属性(在 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 列出的多条验证目标串成了一条可自动执行的链路:

  1. 启动 App(原生主界面,对应 README 中的原生基线视图);
  2. 点击原生按钮,push 出全屏 Flutter 页面(验证 attach 到预热引擎的路径);
  3. 监听FlutterSemanticsUpdateNotification——当 Flutter 侧产生语义树更新时,引擎会经NSNotificationCenter发出该通知;测试进程借助 AppDelegate 暴露的notificationCenter桥接捕获它,并用GREY_REMOTE_CLASS_IN_APP(FullScreenViewController)确认通知对象确实是全屏 Flutter 页面。这等于证明了 Flutter 界面已实际渲染并进入语义激活状态(EarlGrey 的交互能力依赖语义层开启);
  4. 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 生命周期的核心:FlutterEngineAppDelegate在启动时创建并runWithEntrypoint:预热;FlutterViewControllerinitWithEngine:绑定引擎,pop 走时setViewController:nil解除绑定而引擎留存,实现「多次进出 Flutter 页面、引擎始终热态」的模式(FullScreenViewController.m);
  • 视图消失时机需要谨慎判断isMovingFromParentViewController只是第一道防线,源码注释进一步提示,若页面涉及 present 型插件,应通过 method channel 让 Flutter 侧确认「可以真正离开」,这是嵌入场景下引擎释放时机的正确姿势;
  • 混合 UI 可测性依赖 EarlGrey 的语义通知桥:App 进程侧的FlutterSemanticsUpdateNotification经由宿主暴露的notificationCenter传给 XCTest 进程,使测试能在「原生按钮点击 → Flutter 语义树变化」这条完整链路上做可自动断言的验证(IntegrationTests.m);
  • 工程集成方式上,宿主工程通过podhelper.rbinstall_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),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/7 9:48:35

2026数模国赛模块求解Skill:问题分析、数据处理与图表绘制全攻略

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/7 9:47:00

Qt飞机大战游戏开发:从项目拆解到打包发布全攻略

简介:这是一份以Qt框架实现“飞机大战”游戏的完整工程,适合具有一定C基础、正在学习Qt图形界面与游戏逻辑的开发者作为练习参考。资源共223个文件,压缩包约6.21MB,其中包含105个h头文件、32个cpp源文件、60个png图片素材&#xf…

作者头像 李华
网站建设 2026/9/7 9:46:59

半球积分 Ω:渲染方程中的核心数值挑战

一、开场:为何 2π sr 是个工程难题 渲染方程最简洁的形式是: L_o(p, ω_o) = L_e(p, ω_o) + ∫_Ω f_r(p, ω_i, ω_o) L_i(p, ω_i) (n ω_i) dω_i那个看似无害的 ∫_Ω 符号,描述的是着色点 p 上方整个半球(立体角 2π sr)上的连续积分。这个积分在数学上无法解…

作者头像 李华
网站建设 2026/9/7 9:45:13

STM32F103C8T6点灯Demo硬核拆解:从GPIO到工程调试全流程

简介:一份基于STM32F103的32x64双色点阵屏静态显示演示工程,面向嵌入式显示驱动开发与STM32入门学习者。工程采用HUB08接口连接双色LED点阵屏,通过连续更新像素状态实现静态图像输出,覆盖系统时钟与GPIO初始化、PWM亮度控制、显示…

作者头像 李华