
升级规模:Flutter 3.27.4 → 3.38.7 | 涉及 70+ 依赖包
升级周期:2周
大版本升级是每个移动团队迟早要面对的挑战。受 Apple App Store 2026年4月新政策影响,我们需要将 Flutter 从 3.27.4 升级到 3.38.7,同时适配 Xcode 26。
这次升级过程比预期复杂得多------从本地顺利编译到 CI 打包失败,从 iOS 启动黑屏到 Git 历史异常,我们踩了很多坑。本文将完整记录整个升级过程,重点分享问题排查思路 和最终落地方案,希望能为正在做或计划做类似升级的团队提供参考。
一、升级背景与动因
1.1 政策驱动:Apple App Store 新要求
2026年4月起,Apple App Store 对新提交的 App 和更新强制要求:
- 使用 iOS 26 SDK 构建
- 使用 Xcode 26 或更高版本
我们当前环境:
- Xcode 16.2 → 需升级到 26.2
- Flutter 3.27.4 → 需升级到 3.38.0+
- iOS 最低部署目标:iOS 15.5(可保持不变)
1.2 依赖链分析
markdown
Apple App Store 政策
↓
Xcode 版本升级(16.2 → 26.2)
↓
Flutter SDK 升级(3.27.4 → 3.38.7)
↓
第三方依赖适配
↓
测试与验证
1.3 升级范围
| 组件 | 当前版本 | 目标版本 | 复杂度 |
|---|---|---|---|
| Xcode | 16.2 | 26.2 | 中 |
| Flutter SDK | 3.27.4 | 3.38.7 | 中-高 |
| Dart SDK | 3.6.2 | 3.10.0 | 低 |
| CocoaPods | 1.15.0 | 1.16.2 | 低 |
| 内部 SDK | - | - | 需验证 |
二、升级策略与准备
2.1 版本规划
经过评估,我们确定目标版本:
- Flutter SDK: 3.38.7(3.38 系列最新稳定版)
- Dart SDK: 3.10.0(Flutter 3.38 内置版本)
- FVM: 4.0.5(多版本管理工具)
- Ruby: 4.0.1(通过 Homebrew)
- CocoaPods: 1.16.2
2.2 前期环境准备
bash
# 1. 升级 FVM
brew upgrade fvm
# 2. 安装目标 Flutter 版本
fvm install 3.38.7
fvm use 3.38.7
# 3. 升级 CocoaPods
# 注意:系统同时存在 rbenv 和 Homebrew 安装的版本
gem install cocoapods
pod --version # 验证: 1.16.2
# 4. 升级 Xcode
# 推荐通过 XcodesApp 升级,方便同时管理多个版本
# 不推荐通过 App Store 或开发者网站下载 Xcode 26.2
2.3 Mac App 工具 🛠️
Flutter 升级离不开 Xcode,而 Xcode 的管理和磁盘空间清理是个老大难问题。以下三款工具在本次升级中帮了大忙:
2.3.1 XcodesApp - 多版本 Xcode 管理
项目地址 :github.com/XcodesOrg/X...
核心功能:
- 一键安装/切换多个版本的 Xcode
- 管理 iOS 模拟器及模拟器 Runtime
- 自动下载并安装 Xcode 版本
- 可视化管理已安装的 Xcode
使用场景: 升级期间我们需要同时保留 Xcode 16.2(旧版本兼容性测试)和 Xcode 26.2(新版本开发),XcodesApp 让切换变得非常简单。
markdown
传统方式(痛苦):
1. 去开发者网站下载 Xcode(10GB+)
2. 等待下载完成
3. 手动解压到 Applications
4. 切换 Command Line Tools
5. xcode-select 切换
使用 XcodesApp:
1. 点击安装目标版本
2. 等待自动完成
3. 点击切换
2.3.2 Cleaner for Xcode - Xcode 缓存清理
项目地址 :github.com/waylybaye/X...
App Store 下载 :apps.apple.com/us/app/clea...
核心功能:
- 清理 Xcode 构建产生的各类缓存
- 可视化显示各缓存占用空间
- 支持选择性清理(DerivedData、Archives、iOS Device Support 等)
为什么需要它?
用过 Xcode 的朋友都知道,iOS 构建产生的缓存有多恐怖:
构建几次 → 随便给你整出 50G~100G
典型缓存项:
| 缓存类型 | 位置 | 大小范围 |
|---|---|---|
| Derived Data | ~/Library/Developer/Xcode/DerivedData |
10-50GB |
| Archives | ~/Library/Developer/Xcode/Archives |
5-30GB |
| iOS Device Support | ~/Library/Developer/Xcode/iOS DeviceSupport |
5-20GB |
| Simulator Data | ~/Library/Developer/CoreSimulator/Caches |
5-15GB |
实际收益 :升级期间累计清理了约 80GB 缓存。
2.3.3 Tencent Lemon - 深度清理工具
项目地址 :github.com/Tencent/lem...
官网下载:提供打包好的 .dmg 安装包
核心特点:
- ✅ 无广告
- ✅ 支持深度清理
- ✅ 可视化磁盘占用分析
- ✅ 腾讯开源,可信任
本次升级的意外收获
使用 Lemon 扫描后,发现了一个惊人的问题:
javascript
发现隐藏占用:~/Library/Developer/CoreSimulator/Caches/
Runtime(iOS 模拟器运行时)占用:100GB+
原来之前的 iOS 模拟器 Runtime 没有正确清理,多个旧版本 Runtime 累积占用了超过 100GB 空间。Lemon 帮我们精准定位并清理了这部分空间。
「工具组合使用建议」
| 场景 | 推荐工具 |
|---|---|
| 多版本 Xcode 切换 | XcodesApp |
| 日常构建缓存清理 | Cleaner for Xcode |
| 深度磁盘空间分析 | Tencent Lemon |
| 磁盘空间告急时 | 三者配合使用 |
节省空间总计 :本次升级期间通过三款工具累计释放约 200GB 磁盘空间。
2.4 内部 SDK 兼容性验证
我们的项目依赖多个内部 Git 仓库,包括:
- 基础功能 SDK(Foundation、网络请求、通用组件等)
- 业务模块 SDK(账号管理、数据同步、状态管理等)
- 交易相关 SDK(多渠道交易、订单处理等)
- 其他业务定制模块
验证结果:✅ 全部兼容,无需修改
2.5 分支策略
bash
# 创建升级分支
git checkout -b feat/update-to-3.38
# 准备回滚分支
git branch backup/feat/update-to-3.38-backup-$(date +%Y%m%d) HEAD
三、核心依赖升级
3.1 Flutter SDK 升级
pubspec.yaml 修改:
yaml
# 修改前
environment:
sdk: ">=3.3.3 <4.0.0"
flutter: ">=3.0.0 <4.0.0"
# 修改后
environment:
sdk: "^3.8.0"
flutter: ">=3.38.0 <4.0.0"
3.2 第三方依赖适配
大部分依赖自动兼容,但部分需要手动更新:
| 依赖包 | 原版本 | 新版本 | 原因 |
|---|---|---|---|
| image_picker | ^1.0.0 | ^1.2.1 | Registrar API 弃用 |
| permission_handler | ^11.0.0 | ^12.0.1 | Registrar API 弃用 |
| fluttercontactpicker | * | 删除 | 已废弃 |
| flutter_native_contact_picker | - | ^0.0.11 | 替代方案 |
3.3 开发依赖版本固化
为避免团队协作时版本不一致,固化开发工具版本:
yaml
dev_dependencies:
build_runner: ^2.10.5 # 从无约束 → ^2.10.5
flutter_gen_runner: ^5.12.0
freezed: ^3.2.4
json_serializable: ^6.11.4
四、iOS 平台适配(重头戏)
iOS 是本次升级的重灾区,我们遇到了多个棘手问题。
4.1 UIScene 迁移与启动黑屏问题 ⭐
「问题现象」
升级后,iOS App 启动时出现黑屏/白屏闪现,然后才显示正常页面。
「排查过程」
-
分析启动流程:
iniAppDelegate.didFinishLaunchingWithOptions → 创建 Flutter 引擎 → SceneDelegate.scene(_:willConnectTo:) → 创建 FlutterViewController → window.rootViewController = FlutterViewController ← 黑屏出现时机 -
尝试方案1:固定延时切换 rootViewController
swiftDispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { window.rootViewController = flutterViewController }❌ 失败:延时太短仍黑屏,太长影响用户体验
-
尝试方案2:使用 FlutterNativeSplash.preserve()
dart// main.dart await FlutterNativeSplash.preserve();❌ 失败:配合 root 切换导致时序复杂,出现"空白页卡死"和"启动图闪两次"
「最终落地方案:LaunchScreen overlay」
核心思路:
- root 固定 :
window.rootViewController = FlutterViewController - 启动页作为 overlay :把
LaunchScreen.storyboard的 view 覆盖在 window 上 - 业务就绪后移除:Flutter 通过 MethodChannel 通知原生移除 overlay
iOS 代码实现:
swift
// SceneDelegate.swift
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
guard let windowScene = (scene as? UIWindowScene) else { return }
window = UIWindow(windowScene: windowScene)
let flutterViewController = FlutterViewController(engine: flutterEngine, nibName: nil, bundle: nil)
window.rootViewController = flutterViewController
// 创建 LaunchScreen overlay
let storyboard = UIStoryboard(name: "LaunchScreen", bundle: nil)
let launchViewController = storyboard.instantiateInitialViewController()
launchViewController?.view.frame = window.bounds
window.addSubview(launchViewController!.view)
self.launchOverlayView = launchViewController?.view
setupMethodChannel()
window.makeKeyAndVisible()
// 60秒超时兜底
DispatchQueue.main.asyncAfter(deadline: .now() + 60) {
removeLaunchOverlay(animated: true)
}
}
func removeLaunchOverlay(animated: Bool) {
guard let overlayView = launchOverlayView else { return }
UIView.animate(withDuration: animated ? 0.3 : 0) {
overlayView.alpha = 0
} completion: { _ in
overlayView.removeFromSuperview()
}
launchOverlayView = nil
}
Flutter 代码实现:
dart
// app_init.dart
class AppInit extends ConsumerStatefulWidget {
@override
ConsumerState<AppInit> createState() _AppInitState();
}
class _AppInitState extends ConsumerState<AppInit> {
@override
void initState() {
super.initState();
_initApp();
}
Future<void> _initApp() async {
// ... 各种初始化
// iOS:等路由跳转后,等待2帧再通知原生移除 overlay
if (Platform.isIOS) {
_notifyIOSLaunchOverlayCanDismissAfterTwoFrames();
}
}
void _notifyIOSLaunchOverlayCanDismissAfterTwoFrames() {
// 等待2帧,避免"纯色空白帧/启动图闪两次"
WidgetsBinding.instance.addPostFrameCallback((_) {
WidgetsBinding.instance.addPostFrameCallback((_) {
const channel = MethodChannel('rd/app/launch');
channel.invokeMethod('firstFrameReady');
});
});
}
}
为什么 iOS 不用 FlutterNativeSplash.preserve?
| 方案 | 优点 | 缺点 |
|---|---|---|
| FlutterNativeSplash.preserve | Flutter 侧统一控制 | deferFirstFrame 让时序复杂,首帧/路由/Channel/超时互相影响 |
| 原生 overlay | 时序清晰、稳定可靠 | 需要 iOS/Android 分流处理 |
最终选择:iOS 用原生 overlay,Android 继续用 FlutterNativeSplash(Android 没有黑屏问题)
✅ 问题解决:启动流程稳定,无黑屏、无闪烁
4.2 多语言字体系统重构
「背景」
我们的 App 支持英语、简体中文、繁体中文,需要为不同语言配置不同字体:
- English → Poppins
- 简体中文 → Noto Sans SC
- 繁體中文 → Noto Sans TC
「实现」
重构 AppTextTheme:
dart
// 修改前
class AppTextTheme {
static const TextStyle h1 = TextStyle(fontSize: 32);
}
// 修改后
class AppTextTheme {
final AppLocale locale;
const AppTextTheme(this.locale);
TextStyle get h1 {
return GoogleFonts.getFont(
_getFontFamily(),
fontSize: 32,
fontWeight: FontWeight.w600,
);
}
String _getFontFamily() {
switch (locale.languageCode) {
case 'zh':
return locale.countryCode == 'TW' ? 'NotoSansTC' : 'NotoSansSC';
default:
return 'Poppins';
}
}
}
使用方式:
dart
final theme = ref.watch(appThemeProvider);
Text('Hello', style: theme.textTheme.h1); // 自动根据语言选择字体
字体粗细修复:
dart
// ❌ 错误:copyWith 修改字重无效
GoogleFonts.poppins().copyWith(fontWeight: FontWeight.w600)
// ✅ 正确:直接在构造函数传入
GoogleFonts.poppins(fontWeight: FontWeight.w600)
资源清理:删除旧的 BeVietnamPro 字体文件(18个,约2.4MB)
4.3 下拉刷新手感变化
「问题现象」
Flutter 3.38 升级后,iOS 首页下拉刷新出现:
- 回弹更"软"
- 偶发来回振荡(1-2次)
「原因分析」
Flutter 3.38 修改了 iOS 平台的滚动物理/弹簧模拟,更贴近系统原生手感。我们使用的 pull_to_refresh 包的 SmartRefresher 配合 RefreshStyle.Follow,对 overscroll/spring 依赖强,因此手感差异明显。
「解决方案:iOS-only 弹簧参数调优」
修改位置 :lib/app.dart 的全局 RefreshConfiguration
dart
RefreshConfiguration(
child: MaterialApp(...),
headerBuilder: () => const WaterDropMaterialHeader(),
// iOS-only 参数调优
dragSpeedRatio: Platform.isIOS ? 0.80 : 0.9,
maxOverScrollExtent: Platform.isIOS ? 75 : double.infinity,
springDescription: Platform.isIOS
? const SpringDescription(
mass: 2.2,
stiffness: 220, // 默认 150 → 更硬
damping: 30, // 默认 16 → 更快收敛
)
: null,
)
调参策略:
- 降低拖拽速度比例:0.9 → 0.80,减小进入深过冲区的概率
- 限制最大 overscroll:75 pt,避免拉得过深导致振荡
- 提高弹簧阻尼/刚度:更快收敛,减少来回回弹
进一步调优建议:
markdown
若仍觉得偏软,按优先级逐步调整:
1. maxOverScrollExtent: 75 → 65
2. damping: 30 → 34
3. stiffness: 220 → 250
4.4 CocoaPods 升级与 SDK 头文件问题
CocoaPods 升级
Flutter 3.38 建议使用 CocoaPods ≥ 1.16.2,虽然不强制要求,但为避免潜在的 Pods 处理问题,我们执行:
bash
gem install cocoapods
pod --version # 验证: 1.16.2
神策分析 SDK 头文件问题
升级后出现编译错误:
arduino
'SensorsAnalyticsSDK/SensorsAnalyticsSDK.h' file not found
解决方案 :修改 ios/Podfile,添加头文件搜索路径
ruby
post_install do |installer|
installer.pods_project.targets.each do |target|
if target.name == 'sensors_analytics_flutter_plugin'
config.build_settings['HEADER_SEARCH_PATHS'] ||= ['$(inherited)']
config.build_settings['HEADER_SEARCH_PATHS'] << '$(PODS_ROOT)/SensorsAnalyticsSDK/SensorsAnalyticsSDK/Source/Core/IDFA_Dynamic/SensorsAnalyticsSDK.xcframework/ios-arm64_x86_64-simulator/SensorsAnalyticsSDK.framework/Headers'
config.build_settings['HEADER_SEARCH_PATHS'] << '$(PODS_ROOT)/SensorsAnalyticsSDK/SensorsAnalyticsSDK/Source/Core/IDFA_Dynamic/SensorsAnalyticsSDK.xcframework/ios-arm64/SensorsAnalyticsSDK.framework/Headers'
end
end
end
五、Android 配置调整
5.1 Gradle 与 Kotlin 版本升级
flutter_config 插件修改:
gradle
// 修改前
kotlinVersion = "1.7.20"
agpVersion = "7.3.1"
compileSdkVersion = 29
// 修改后
kotlinVersion = "1.9.24"
agpVersion = "8.1.0"
compileSdkVersion = 34
minSdkVersion = 21
javaVersion = JavaVersion.VERSION_17
5.2 环境变量系统迁移
从 flutter_dotenv 回退到 flutter_config
问题 :flutter_dotenv 缺少 Gradle 集成,无法生成 BuildConfig.SDK_ENV
解决方案 :恢复使用本地修复的 flutter_config
pubspec.yaml:
yaml
dependencies:
flutter_config:
git:
url: git@code.example.com:app/flutter_config.git
ref: "9d2bcd9" # 修复后的版本
环境文件映射配置 (android/app/build.gradle):
gradle
project.ext.envConfigFiles = [
devdebug: ".dev.env",
devrelease: ".dev.env",
prddebug: ".pro.env",
prdrelease: ".pro.env",
// ... 其他 flavor
]
apply from: project(':flutter_config').projectDir.getPath() + "/dotenv.gradle"
Dart 代码修改:
dart
// 修改前
await dotenv.load(fileName: ".env");
final env = dotenv.env['SDK_ENV'] ?? 'dev';
// 修改后
await FlutterConfig.loadEnvVariables();
final env = FlutterConfig.get('SDK_ENV') ?? 'dev';
六、Dart 代码适配
6.1 Freezed v2 → v3 迁移
Freezed 3.0 引入重大变更,更好地支持 Dart 3 原生模式匹配。
主要变更
dart
// 旧模式 (Freezed v2)
@freezed
class ModelState with _$ModelState {
const factory ModelState.loading() = _Loading;
const factory ModelState.loaded(Model data) = _Loaded;
}
// 新模式 (Freezed v3)
@freezed
sealed class ModelState with _$ModelState {
const factory ModelState.loading() = ModelLoadingState;
const factory ModelState.loaded(Model data) = ModelLoadedState;
}
命名冲突问题修复 ⚠️
问题 :同时导入两个 Freezed 文件时,如果都定义了 Loading 类,会导致编译错误。
dart
// ❌ 编译错误
import 'package:my_app/features/auth/models/auth_status.dart';
import 'package:my_app/core/models/result.dart';
if (authStatus is! Loading) { } // 'Loading' is imported from both files
解决方案 :使用 hide 关键字
dart
// ✅ 修复后
import 'package:my_app/features/auth/models/auth_status.dart';
import 'package:my_app/core/models/result.dart' hide Loading;
if (authStatus is! Loading) { } // 现在使用 auth_status 的 Loading
修改文件 :lib/common/screens/app_init.dart:15
6.2 API 弃用处理
Flutter 3.38 弃用旧的插件注册 API (Registrar)。
解决方案:
-
更新插件到支持新 API 的版本
-
修改本地插件
flutter_config:swift// 注释掉旧 API // public static func register(with registrar: FlutterPluginRegistrar) { } // 保留新 API(v2 embedding) public func attach(to engine: FlutterEngine) throws { // ... }
七、深坑解析 ⭐⭐⭐
7.1 本地正常、CI 打包失败
「问题现象」
- ✅ 本地 Mac:
flutter build ipa正常 - ❌ CI 打包机(Jenkins):
gym步骤报错
vbnet
❌ error: Improperly formatted define flag:
Failed to package /Users/jenkins/workspace/project_update-to-3.38_v2
「排查过程」
Step 1:对比环境变量
bash
# 本地环境
env | grep -i flutter
# 输出:正常,有 DART_DEFINES
# CI 日志
# 检查日志 33241-33242 行:
export FLUTTER_BUILD_NAME=2.14.0
export FLUTTER_BUILD_NUMBER=2.14.0
# 全文件搜索 DART_DEFINES:❌ 没有输出!
发现 :CI 构建未通过 Flutter 预构建阶段写入 DART_DEFINES
Step 2:验证 flutter_config 版本
bash
# CI 日志 7707 行
flutter_config-d5646d8818da1a4dd71b8813ca90c9667da2d16e
✅ 确认命中修复后版本(包含 URL 截断修复)
Step 3:检查 URL 注入
bash
# CI 日志 32863, 33495 行:修复后 URL 完整可见
export SENSORS_ANALYTICS_URL=\"https://analytics.example.com/collect?project=default\"
# 对比日志 1293, 3029 行:修复前 URL 被截断(`://` 被误认为注释)
export SENSORS_ANALYTICS_URL="\"https:
根本原因 :CI 打包流程未正确调用 Flutter 构建前置步骤,导致 DART_DEFINES 环境变量未注入。
Flutter 3.38 的 iOS 构建链路中,xcode_backend.dart 会向 flutter assemble 传入 --DartDefines=...。当 DART_DEFINES 在 Xcode 环境中缺失/为空时,assemble 侧会把空值当作一个不合法 define 条目,触发 Improperly formatted define flag: 错误。
「解决方案」
临时方案(已验证可用) :在 ios/fastlane/Fastfile 的 gym 前增加 DART_DEFINES 兜底
ruby
# ios/fastlane/Fastfile
before_all do
# 优先读取 Generated.xcconfig 的 DART_DEFINES
dart_defines = File.read('ios/Flutter/Generated.xcconfig')[/DART_DEFINES=(.+)/, 1]
# 若为空则注入最小合法 fallback
if dart_defines.nil? || dart_defines.empty?
# base64 编码的 "CI_DART_DEFINE_FALLBACK=1"
dart_defines = "Q0lfREFSVF9ERUZJTkVfRkFMTEJBQ0s9MQ=="
end
# 通过 xcargs 显式传给 xcodebuild
ENV['DART_DEFINES'] = dart_defines
end
lane :beta do
gym(
xcargs: "DART_DEFINES=#{ENV['DART_DEFINES']}",
# ... 其他配置
)
end
长期方案(建议):
-
升级 Flutter 版本 (首选):使用包含修复的 Flutter 版本,
xcode_backend在DART_DEFINES为空时不传--DartDefines,或assemble侧容错空条目。 -
在 CI 规范化 Flutter 预构建阶段 :在
gym前增加 Flutter 原生构建初始化步骤(确保Generated.xcconfig和flutter_export_environment.sh完整可用)。
groovy
stage('Build iOS') {
steps {
sh '''
flutter clean
flutter pub get
flutter build ios --no-codesign # 预构建,生成必要配置文件
bundle exec fastlane ios beta
'''
}
}
- 保留"失败前健康检查" :CI 在
gym前增加检查,Generated.xcconfig必须存在且包含DART_DEFINES=...,不满足则立即失败并给出明确错误。
7.2 Git 历史异常:ahead 11003 的假象
「问题现象」
bash
git status -sb # -sb = --short --branch,不是你想的那个 sb 🌝
# feat/update-to-3.38...origin/feat/update-to-3.38 [ahead 11003, behind 16]
git merge-base HEAD origin/feat/update-to-3.38
# 返回空!看起来像无共同祖先
「问题背景」
这个问题是由一次失败的 rebase 操作导致的:
操作过程:
- 对当前分支
feat/update-to-3.38基于准备合并的 PR 分支进行变基操作 - 变基过程中发现大量冲突
- 解决了部分冲突后发现方向不对,中途退出变基(
git rebase --abort) - 重复进行了 2 次"变基 → 冲突 → 退出"的操作流程
- 最终发现:分支的部分 git 历史记录消失了,
git status显示领先远端 11003 个提交
问题本质:rebase 操作会重写提交历史,中途 abort 可能导致本地状态异常,特别是当远端同时也有其他人进行 rebase + force push 时,问题会被放大。
「排查过程」
Step 1:检查是否浅克隆
bash
git rev-parse --is-shallow-repository
# 输出: true
发现是浅克隆,祖先链被截断。
什么是浅克隆(Shallow Clone)?
浅克隆是 Git 的一种优化克隆方式,只下载最近一次提交的历史,不包含完整的祖先链。
bash# 浅克隆(只下载最新提交) git clone --depth 1 <repo-url> # 完整克隆(下载完整历史) git clone <repo-url>浅克隆的特点:
- ✅ 克隆速度快,节省磁盘空间
- ✅ 适合只关注最新代码的场景(如 CI/CD)
- ❌ 无法查看完整历史记录
- ❌
git log只显示部分提交- ❌
git merge-base可能失败或返回错误结果如何转换:
bash# 浅克隆 → 完整克隆 git fetch --unshallow origin
Step 2:检查远端是否被强制更新
bash
git reflog show origin/feat/update-to-3.38
# 2026-02-27 11:45:45 +0800: fetch origin: forced-update
✅ 确认远端分支曾被强制更新(force push / rebase)
Step 3:拉全历史后重新判断
bash
git fetch --unshallow origin
git rev-parse --is-shallow-repository
# 输出: false
git merge-base HEAD origin/feat/update-to-3.38
# 输出: ad9977b81310ea50a500dc59fc9d5874a3b5319d
git status -sb
# feat/update-to-3.38...origin/feat/update-to-3.38 [ahead 14, behind 16]
✅ 恢复正常:并非完全无祖先,而是"浅克隆 + 历史重写"造成了夸张视图。
「根因总结」
- 远端分支被强制更新:提交 hash 被重写
- 本地为浅克隆 :祖先链缺失导致
merge-base失效 - 两者叠加 :分叉数量被放大成
ahead 11003
「团队协作规范改进」
排查三板斧:
bash
# 1. 检查是否浅克隆
git rev-parse --is-shallow-repository
# 2. 检查远端是否被强制更新
git reflog show origin/<branch> -n 20
# 3. 找共同祖先
git merge-base HEAD origin/<branch>
操作规范:
- 重写历史后必须在团队群公告
- 高风险操作前先备份分支
- 对长期协作分支限制 force push
- 提供统一的"历史异常排查脚本"
八、AI 辅助编码实践 ⚡
本次升级是 AI 辅助开发的一次集中实践。我们在不同阶段使用了多种 AI 工具和模型,形成了「常规问题用大众模型,复杂问题用专业模型」的经验法则。
8.1 AI 工具矩阵
| 工具/模型 | 使用阶段 | 主要用途 |
|---|---|---|
| Cursor Composer | 早期预研 | 技术方案预研和规划 |
| Cursor IDE | iOS 迁移 | AppDelegate → UIScene 迁移 |
| Claude Code + GLM 4.7 | 常规迁移 | 依赖升级、配置调整、常规 Bug 修复 |
| Claude Code + MiniMax M2.5 | 常规迁移 | 同上(M2.5 发布后尝试的新模型) |
| Codex CLI/App + GPT-Codex-5.3 | 疑难杂症 | 复杂问题深度排查与修复 |
8.2 分阶段使用策略
阶段一:技术方案预研(Cursor Composer)
升级开始前,使用 Cursor Composer 进行整体技术方案的调研和规划:
markdown
# 输入给 Cursor 的 Prompt 示例
我需要将 Flutter 项目从 3.27 升级到 3.38,主要背景是 Apple 要求
使用 Xcode 26 构建。请帮我:
1. 梳理升级依赖链
2. 识别可能的 Breaking Changes
3. 列出需要适配的第三方插件类型
4. 给出分阶段升级建议
产出:一份包含依赖关系图、风险评估表、分阶段执行计划的技术方案文档。
阶段二:iOS 原生代码迁移(Cursor IDE)
iOS AppDelegate → UIScene 迁移涉及大量原生 Swift 代码修改,使用 Cursor IDE 的 Composer 模式:
swift
// Cursor 生成的迁移代码框架
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
var window: UIWindow?
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
// Cursor 辅助生成:FlutterViewController 创建
// Cursor 辅助生成:MethodChannel 设置
// Cursor 辅助生成:LaunchScreen overlay 逻辑
}
}
效果 :大幅减少了原生代码编写的时间,但需要人工review 每个 TODO 和逻辑细节。
阶段三:常规迁移工作(Claude Code + GLM 4.7 / MiniMax M2.5)
大部分依赖升级、配置调整、简单 Bug 修复由 Claude Code 配合不同模型完成:
GLM 4.7 适合的场景:
- pubspec.yaml 依赖版本更新
- Android Gradle 配置调整
- Dart 代码 API 迁移(如 Freezed v2 → v3)
- 简单编译错误修复
MiniMax M2.5 适合的场景:
- 比 GLM 4.7 在中文语境理解上稍好
- 适合处理中英文混合的技术文档阅读
- 但在本次升级中,效果与 GLM 4.7 差异不大
典型工作流:
bash
# Claude Code + GLM 4.7 处理依赖升级
/cursor
请帮我更新 pubspec.yaml 中的以下依赖:
- image_picker 升级到最新版本(需支持 Flutter 3.38)
- permission_handler 升级到最新版本
- 删除已废弃的 fluttercontactpicker
阶段四:疑难杂症攻坚(Codex + GPT-Codex-5.3)⭐⭐⭐
这是本次升级中最关键的发现。
"本地正常、CI 打包失败"问题实战
问题描述:
- 本地
flutter build ipa正常 - CI(Jenkins)打包时
gym步骤报错:Improperly formatted define flag
问题卡顿时长:3 天
尝试过程:
-
GLM 4.7(失败)
- 尝试次数:5+ 次
- 生成的方案:
- 修改 flutter_config 插件代码(无效)
- 调整 Podfile 配置(无效)
- 修改环境变量导出方式(无效)
- 问题:GLM 4.7 没有抓住"环境变量未注入"这个根本原因,一直在修改插件代码本身
-
MiniMax M2.5(失败)
- 尝试次数:3 次
- 生成的方案与 GLM 4.7 类似,但表达方式略有不同
- 问题:同样没有定位到 CI 流程的缺失环节
-
Codex GPT-Codex-5.3(一击毙命) ✅
bash
# 输入给 Codex 的完整上下文
[错误日志片段]
[本地环境变量输出]
[CI 日志关键片段]
[flutter_config 插件代码]
本地有 DART_DEFINES,CI 没有。为什么?请分析根本原因并给出修复方案。
Codex 的分析:
markdown
关键差异:
1. 本地日志显示 DART_DEFINES 存在
2. CI 日志中 FLUTTER_BUILD_NAME/NUMBER 存在,但 DART_DEFINES 缺失
3. 这说明 CI 没有执行 Flutter 预构建步骤
根本原因:
CI 直接调用 gym,跳过了 flutter build ipa 的前置流程。
flutter build ipa 会:
1. 执行 flutter 预构建
2. 写入 DART_DEFINES 环境变量
3. 调用 xcodebuild/gym
临时解决方案:
在 Fastfile 的 gym 前增加 DART_DEFINES 兜底逻辑,优先读取 Generated.xcconfig,
若为空则注入最小合法 fallback,通过 xcargs 显式传给 xcodebuild。
长期解决方案:
1. 升级 Flutter 版本获取修复
2. 在 CI 规范化 Flutter 预构建阶段
3. 保留"失败前健康检查"
关键洞察 :Codex 能够从日志差异 推断出流程缺失,这是一个需要"跳出代码看流程"的能力。
8.3 模型能力对比总结
| 问题类型 | GLM 4.7 | MiniMax M2.5 | Codex 5.3 | Claude Code 4.6 |
|---|---|---|---|---|
| 依赖升级 | ✅ 优秀 | ✅ 优秀 | ✅ 优秀 | ✅ 优秀 |
| API 迁移 | ✅ 良好 | ✅ 良好 | ✅ 优秀 | ✅ 优秀 |
| 简单 Bug | ✅ 良好 | ✅ 良好 | ✅ 优秀 | ✅ 优秀 |
| 日志分析 | ⚠️ 一般 | ⚠️ 一般 | ✅ 优秀 | ✅ 优秀 |
| 流程问题 | ❌ 较弱 | ❌ 较弱 | ✅ 优秀 | ✅ 优秀 |
| 中文场景 | ✅ 优秀 | ✅ 优秀 | ✅ 良好 | ✅ 优秀 |
8.4 经验法则:常规 vs 复杂
基于本次实践,我们总结出以下选择策略:
常规问题(大众模型即可)
特征:
- 问题边界清晰
- 有明确 API 文档或迁移指南
- 日志信息完整直接
- 不涉及跨系统流程
推荐工具:
- Claude Code + GLM 4.7 / MiniMax M2.5
- Cursor Composer / Cursor IDE
典型场景:
diff
- 依赖版本升级
- API 迁移(如 Freezed v2 → v3)
- 配置文件调整
- 简单编译错误修复
复杂问题(专业模型)
特征:
- 问题边界模糊
- 需要关联多个系统/组件
- 日志信息需要深度推理
- 涉及流程或架构问题
推荐工具:
- Codex CLI/App + GPT-Codex-5.3 high
- Claude Code + Claude 4.6 (Opus)
典型场景:
diff
- 本地正常但 CI 失败(环境差异)
- 偶发性崩溃(需要深度分析)
- 性能问题(需要架构优化)
- 跨平台不一致问题
8.5 AI 辅助编码的心得
-
不要期望一次性解决
- 大模型生成代码后,必须人工 review
- 复杂问题需要多次迭代,每次提供更精准的上下文
-
上下文质量决定输出质量
- GLM 4.7 和 MiniMax M2.5 在本次 CI 问题中失败,部分原因是上下文给得不够完整
- Codex 成功是因为我们整理了完整的日志对比和插件代码
-
多模型协作的价值
- 常规工作用大众模型更快、更经济
- 遇到瓶颈时切换到专业模型,避免陷入局部最优
-
AI 不能替代思考
- 所有 AI 生成的方案都需要开发者理解背后的原理
- 本次 CI 问题的根本原因,最终是由人基于 Codex 的分析验证后确认的
8.6 时间投入对比
| 问题 | 人工排查 | GLM 4.7 | MiniMax M2.5 | Codex 5.3 |
|---|---|---|---|---|
| iOS 启动黑屏 | 2-3 天 | 辅助但需人工 | 辅助但需人工 | 辅助但需人工 |
| CI 打包失败 | 3 天+ | ❌ 失败 | ❌ 失败 | ✅ 1 小时 |
| Freezed 迁移 | 1-2 天 | ✅ 0.5 天 | ✅ 0.5 天 | ✅ 0.3 天 |
| 依赖升级 | 1 天 | ✅ 0.2 天 | ✅ 0.2 天 | ✅ 0.2 天 |
九、验证清单与上线策略
9.1 功能验证清单
| 模块 | 验证项 | 状态 |
|---|---|---|
| 环境变量 | 各 flavor 正确加载环境文件 | ✅ |
| 多语言 | 英文/简中/繁中字体正确切换 | ✅ |
| 权限 | 相机/相册/定位权限请求 | ✅ |
| 启动 | iOS 无黑屏,Android 无白屏 | ✅ |
| 下拉刷新 | 手感正常,无振荡 | ✅ |
| 账号 | 用户登录认证功能正常 | ✅ |
| 资产 | 资产管理模块功能正常 | ✅ |
| 交易 | 交易处理模块功能正常 | ✅ |
9.2 构建验证
bash
# 已验证的构建命令
flutter build apk --debug --flavor dev ✅
flutter build apk --release --flavor prd ✅
flutter build apk --release --flavor uat ✅
flutter build ipa --release --flavor prd ✅
十、经验总结与最佳实践
10.1 升级过程关键经验
-
前期准备很重要
- 梳理完整的依赖链
- 验证内部 SDK 兼容性
- 准备回滚方案
-
iOS 是重灾区
- UIScene 迁移要谨慎
- 启动流程问题优先考虑原生 overlay 方案
- 不要过度依赖 Flutter 插件的跨平台抽象
-
本地 ≠ CI
- 环境变量问题最容易被忽略
- CI 流程必须完整执行 Flutter 构建步骤
- 定期在 CI 环境进行完整构建验证
-
Git 历史管理
- 浅克隆会导致奇怪的
merge-base行为 - 重写历史前必须通知团队
- 高风险操作前先备份
- 浅克隆会导致奇怪的
10.2 给后续升级项目的建议
| 阶段 | 建议 |
|---|---|
| 准备期 | 至少提前 2 个月开始,留出充足测试时间 |
| 执行期 | 按 平台逐个击破,每个平台独立验证 |
| 验证期 | 本地 + CI 都要验证,不能只看本地 |
| 上线期 | 灰度发布,监控错误率和性能指标 |
10.3 时间投入复盘
| 活动 | 时间 |
|---|---|
| 环境准备 | 1 天 |
| 依赖升级 | 2 天 |
| iOS 启动黑屏排查 | 3 天 |
| CI 打包问题排查 | 2 天 |
| Git 历史问题 | 0.5 天 |
| 测试验证 | 2 天 |
| 其他问题修复 | 2.5 天 |
| 总计 | 约 15 天 |
十一、总结
升级成果
历时 2 周,Flutter 3.38 升级项目成功完成。本次升级涵盖了 SDK 升级、70+ 依赖包适配、iOS 原生代码迁移、CI/CD 流程优化等多个方面,最终实现生产环境稳定上线。
技术成就:
- ✅ Flutter SDK 3.27.4 → 3.38.7 升级完成
- ✅ Xcode 16.2 → 26.2 适配完成
- ✅ iOS 启动黑屏问题通过原生 overlay 方案彻底解决
- ✅ 多语言字体系统重构,支持按语言动态加载
- ✅ CI 打包环境差异问题修复,构建流程稳定
- ✅ 生产环境上线后无重大问题
时间投入:总计约 15 天
核心收获
技术深度:
-
Flutter 启动流程:深入理解了 iOS UIScene 架构下的 Flutter 引擎初始化流程,以及 LaunchScreen overlay 与首帧渲染的时序关系。
-
跨平台差异处理:学会了如何在保持代码复用的前提下,针对 iOS/Android 平台差异进行精细化处理(如 iOS 用原生 overlay,Android 用 FlutterNativeSplash)。
-
系统化排查方法:面对"本地正常、CI 失败"这类环境差异问题时,建立了"对比环境变量 → 追踪构建链路 → 定位缺失环节"的排查思路。
AI 辅助开发实践:
本次升级也是 AI 辅助开发的一次深度实践。通过合理使用 Cursor、Claude Code、Codex 等工具,配合 GLM 4.7、MiniMax M2.5、GPT-Codex-5.3 等不同模型,我们形成了「常规问题用大众模型,复杂问题用专业模型」的策略。
效率提升:
| 任务 | 预估人工时间 | AI 辅助时间 | 提升幅度 |
|---|---|---|---|
| Freezed v2→v3 | 1-2 天 | 0.5 天 | 60-75% |
| 依赖包升级 | 1 天 | 0.2 天 | 80% |
| CI 问题排查 | 3 天+ | 1 小时 | 95%+ |
| iOS 原生代码迁移 | 2-3 天 | 辅助但需人工 | 30-50% |
实践心得:
-
AI 是加速器,不是自动驾驶:AI 能快速完成依赖升级、配置调整、API 迁移等重复性工作,但在涉及架构决策、复杂问题诊断时,仍需要开发者理解原理并做出判断。
-
上下文质量决定输出质量:本次 CI 问题中,GLM 4.7 和 MiniMax M2.5 最初失败,模型本身能力有一部分原因,另外一部分原因是开始提供的上下文提供不完整。当我们整理出完整的日志对比和插件代码后,Codex 一次性定位根本原因。
-
多模型协作的价值:常规工作用经济的大众模型(GLM 4.7、MiniMax M2.5)更快、更划算;遇到瓶颈时切换到专业模型(Codex 5.3、Claude Opus),避免陷入局部最优。
给后续升级项目的建议
| 维度 | 建议 |
|---|---|
| 准备期 | 至少提前 2 个月开始,留出充足测试时间;验证内部 SDK 兼容性 |
| 执行期 | 按平台逐个击破,每个平台独立验证;高风险操作前先备份分支 |
| 验证期 | 本地 + CI 都要验证,不能只看本地;建立"失败前健康检查"机制 |
| 协作期 | 重写历史前必须通知团队;对长期协作分支谨慎使用 force push |
| 工具期 | 善用 AI 工具提升效率,但保持对原理的理解;复杂问题优先尝试专业模型 |