Flutter 基础体系 · 第 76/80 篇。示例基于当前稳定 Flutter 与 Dart 3 语言能力;Android、iOS、桌面和 Web 差异会明确说明。
Flutter iOS 发布:证书、Provisioning、Capability、归档和审核
Flutter iOS 发布不是把 flutter build 生成的文件直接上传到 App Store Connect。一个可安装、可运行、可提交审核的 iOS 应用,至少要同时满足以下条件:
- Flutter 工程能够生成 iOS 原生工程和 Release 构建产物。
- Apple Developer 账户中存在与 Bundle ID 对应的 App ID。
- 应用使用有效的签名证书签名。
- 应用使用与签名证书、Bundle ID、Capability 和分发方式匹配的 Provisioning Profile。
- 应用中声明的 entitlements 与 Apple 端允许的能力一致。
- 归档产物通过签名、校验、上传和 App Store Connect 处理。
- 应用的权限说明、隐私信息、元数据和功能行为符合审核要求。
其中,证书解决“谁签署了应用”的问题,Provisioning Profile 解决“这个签名者能以什么身份、在什么环境运行这个应用”的问题,Capability 决定应用被允许使用哪些系统能力,Archive 则是可供上传和导出分发包的构建结果。
一、先区分四个经常混淆的对象
1. Bundle ID:应用的身份
Bundle ID 是 iOS 应用的唯一标识,例如:
com.example.weather
在 Flutter 工程中,它通常由 Xcode 的 Runner target 配置决定,最终写入应用的 CFBundleIdentifier。
Bundle ID 不只是一个字符串,它会关联 Apple Developer 网站中的 App ID。App ID 通常由以下两部分组成:
Team ID + Bundle ID
例如:
Team ID: ABC123XYZ9
Bundle ID: com.example.weather
Apple 用这个组合识别某个开发团队下的某个应用。以下对象必须使用同一个目标 Bundle ID:
- Xcode 中 Runner target 的 Bundle Identifier;
- Apple Developer 中注册的 App ID;
- Provisioning Profile 中包含的 App ID;
- App Store Connect 中应用对应的 Bundle ID;
- 如果有插件扩展,还包括 Notification Service Extension、Share Extension 等各自的 Bundle ID。
如果主应用是:
com.example.weather
通知扩展可能是:
com.example.weather.NotificationService
扩展是独立的可签名 target,通常需要独立的 Bundle ID、证书签名配置和 Provisioning Profile,不能只给主应用配置签名。
2. Certificate:证明签名者身份
证书是 Apple 签发的数字证书,用来证明某个开发者或组织拥有对应的签名身份。
实际签名时使用的不是证书本身,而是:
证书 + 对应的私钥
证书通常包含公钥,私钥保存在本地钥匙串或 CI 的安全存储中。签名流程可以简化为:
- Apple 为开发者签发证书;
- 开发者机器生成或持有对应私钥;
- Xcode 使用私钥对应用代码和资源计算出的摘要进行签名;
- 系统使用证书中的公钥验证签名;
- 系统进一步检查证书是否由 Apple 信任、是否过期或被吊销。
因此,只导出 .cer 或只复制证书文件,不能在另一台机器完成签名。另一台机器还必须拥有对应私钥,通常表现为导入 .p12 文件或通过 CI 的安全签名方案安装证书和私钥。
常见证书类型如下。
Apple Development
用于开发设备安装和调试。它通常配合 Development Provisioning Profile 使用。
Apple Distribution
用于分发构建,例如:
- App Store Connect 上传;
- Ad Hoc 测试;
- 企业分发(仅限符合条件的企业账户和对应流程)。
Apple 对证书名称和分发流程会随 Xcode、Apple Developer 账户体系变化,但核心原则不变:开发签名和分发签名不是同一个运行环境概念,不能因为应用在开发设备上运行,就认为它已经满足 App Store 上传要求。
证书的实际风险
证书本身可以公开验证,但私钥必须保密。如果私钥泄露,持有者可能使用你的团队身份签署应用。生产环境应当:
- 不把
.p12、私钥密码、API Key 提交到 Git; - 限制 CI 中签名文件的访问权限;
- 证书即将过期时提前轮换;
- 发现私钥泄露时立即吊销并重新签发;
- 不把开发证书误用于生产分发。
证书过期、被吊销或缺少私钥,通常会表现为 Xcode 中无法选择签名身份、导出时报 Signing certificate is invalid,或 CI 中出现 No signing certificate ... found。
3. Provisioning Profile:授权应用以某种方式运行
Provisioning Profile 可以理解为 Apple 签发的一份授权文件。它把以下信息绑定起来:
- Team;
- App ID 或 Bundle ID;
- 允许使用的 entitlements;
- 允许使用的证书;
- 允许安装的设备列表(某些类型需要);
- profile 类型和有效期。
它不是证书,也不是简单的配置文件。一个简化的关系可以表示为:
应用 Bundle ID
+
签名证书
+
应用请求的 Entitlements
+
Provisioning Profile
+
当前运行或分发环境
只有这些条件互相匹配,系统才会接受该应用。
Development Profile
用于开发设备安装和调试。通常包含:
- 一个 App ID;
- 一个或多个 Development 证书;
- 已注册的测试设备 UDID;
- 开发环境相关的 entitlements。
Ad Hoc Profile
用于有限范围的设备测试。通常包含:
- App ID;
- Distribution 证书;
- 已注册的设备 UDID;
- Ad Hoc 分发所需授权。
它适合在不经过 App Store 的情况下,把 IPA 安装到预先登记的测试设备上。设备不在 profile 中时,即使 IPA 签名正确,也不能正常安装或启动。
App Store 分发 Profile
用于上传到 App Store Connect。它不依赖某一组测试设备,因为安装权限由 App Store、TestFlight 和系统分发机制管理。
在 Xcode 自动签名模式下,开发者通常不需要手动下载和指定 profile;Xcode 会根据 Team、Bundle ID、Capability 和分发方式请求或选择 profile。但“自动”只意味着 Xcode 管理 profile,不意味着签名规则消失。CI、多个 target、多个环境和特殊 Capability 场景下,手动控制仍然可能更稳定。
Profile 的有效期
Provisioning Profile 有有效期。开发 profile 或 Ad Hoc profile 过期后,已安装应用可能无法继续按原授权运行;App Store 已发布应用的最终运行授权不简单等同于本地 profile 的有效期,因为 App Store 会重新签名或使用自己的分发机制。
4. Capability:应用声明要使用的系统能力
Capability 是 Apple 平台对应用能力的声明机制。它通常同时涉及:
- Developer 账户中对 App ID 的能力开关;
- Xcode target 的
Signing & Capabilities配置; - 应用的 entitlements 文件;
- Provisioning Profile 中允许的 entitlements;
- 相关系统服务的独立配置。
例如,应用需要远程推送时,不能只调用 Flutter 插件 API。完整链路还包括:
Apple Developer 开启 Push Notifications
↓
Xcode target 生成 aps-environment entitlement
↓
Provisioning Profile 允许该 entitlement
↓
应用获得 APNs device token
↓
服务端使用 APNs 凭据发送消息
↓
APNs 将消息路由到设备
常见 Capability 包括:
- Push Notifications;
- Associated Domains;
- App Groups;
- Keychain Sharing;
- Sign in with Apple;
- Background Modes;
- iCloud;
- In-App Purchase;
- Apple Pay;
- HealthKit;
- NFC;
- 并发或后台相关能力。
不同 Capability 的配置复杂度不同。比如 Keychain Sharing 主要影响 keychain-access-groups;Associated Domains 还需要服务器提供正确的 apple-app-site-association 文件;Push Notifications 还需要 APNs 身份配置和服务端发送逻辑。
二、Flutter 工程中的 iOS 原生边界
Flutter 负责 Dart 层和 Flutter Engine,但 iOS 发布仍然由 Xcode 和 Apple 工具链完成。Flutter 工程的典型结构如下:
my_app/
├── lib/
├── pubspec.yaml
├── ios/
│ ├── Runner.xcworkspace
│ ├── Runner.xcodeproj
│ ├── Runner/
│ │ ├── AppDelegate.swift
│ │ ├── Info.plist
│ │ └── Runner.entitlements
│ └── Podfile
└── build/
使用 CocoaPods 的 Flutter 工程应优先打开:
open ios/Runner.xcworkspace
不要在已经集成 Pods 的工程中仅打开 Runner.xcodeproj。.xcworkspace 包含主工程和 Pods 工程;打开 .xcodeproj 可能导致依赖缺失、链接失败或构建配置不完整。
开始发布前可以检查环境:
flutter doctor -v
flutter --version
flutter pub get
预期应能看到:
- Flutter SDK 版本;
- Dart 版本;
- Xcode 版本;
- iOS toolchain 可用;
- CocoaPods 状态正常。
Flutter 和 Dart 版本主要影响 Dart 编译、插件兼容性和 Flutter 构建逻辑;证书、Provisioning Profile、Capability 和最终签名仍由 Apple/Xcode 工具链决定。Android 的 keystore、Web 的静态资源部署、桌面的签名机制,不能直接套用到 iOS。
三、从 Bundle ID 到签名产物的完整关系
可以把 iOS 发布链路表示为:
flowchart TD
A[Flutter Dart 代码] --> B[Flutter Release 编译]
B --> C[iOS Runner.app]
D[Bundle ID] --> E[Apple App ID]
E --> F[Capability 与 Entitlements]
F --> G[Provisioning Profile]
H[Apple Distribution 证书与私钥] --> I[代码签名]
C --> I
G --> I
I --> J[Archive .xcarchive]
J --> K[导出 IPA]
K --> L[App Store Connect 上传]
L --> M[处理与校验]
M --> N[TestFlight 或 App Review]
其中有两个容易被忽略的约束。
约束一:代码签名不是最后才做一次
iOS 应用包含主应用、Flutter.framework、插件生成的原生二进制、资源和可能存在的扩展。最终分发前需要对可执行代码进行签名,并且嵌套代码的签名关系也必须正确。
因此,不能简单地:
- 先生成 IPA;
- 随便替换一个
.framework; - 再认为原签名仍有效。
修改已经签名的内容会使签名摘要失效。任何修改都应在正确的构建和签名流程中完成。
约束二:entitlements 必须形成交集
应用请求的 entitlements 可以记为集合:
E_app
Provisioning Profile 允许的 entitlements 记为:
E_profile
一个必要条件是:
E_app ⊆ E_profile
也就是说,应用不能声明 profile 没有授权的能力。
例如应用签名时声明:
<key>com.apple.security.application-groups</key>
<array>
<string>group.com.example.weather</string>
</array>
但 profile 没有这个 App Group,签名或安装阶段就可能失败。即使本地构建成功,上传或设备安装时也可能出现类似:
Provisioning profile doesn't support the Associated Domains capability
或:
The application was signed with invalid entitlements
这不是 Flutter Dart 代码错误,而是 Apple 端能力授权和本地签名产物不一致。
四、在 Xcode 中配置签名
打开工作区:
open ios/Runner.xcworkspace
在 Xcode 中选择:
Runner project
→ Runner target
→ Signing & Capabilities
至少检查以下项目:
- Team 是否为正确的 Apple Developer 团队;
- Bundle Identifier 是否与预期一致;
Automatically manage signing是否按团队策略启用;- Debug、Profile、Release 配置是否使用了正确的签名方式;
- 所有扩展 target 是否单独配置;
- 添加的 Capability 是否确实被业务使用;
- 应用的 deployment target 是否满足插件和 Apple SDK 要求。
自动签名适合:
- 单个应用;
- 本地开发;
- target 数量较少;
- 使用 Xcode 登录账户构建。
手动签名适合:
- CI 无交互构建;
- 多环境、多 Bundle ID;
- 多个扩展;
- 希望构建结果可重复;
- 需要严格控制 profile 和证书。
自动签名和手动签名不是“正确”和“错误”的区别,而是签名材料由谁解析和选择的区别。自动签名降低初始配置成本,但 CI 如果没有登录账户、网络权限或正确的 Apple 角色,就容易出现本地能构建、流水线不能构建的情况。
五、Capability 的配置示例:以推送通知为例
假设 Flutter 应用使用了推送插件。下面的流程不能省略其中任意一个关键环节。
1. 在 Apple Developer 中开启能力
为主应用的 App ID 开启 Push Notifications。若应用有 Notification Service Extension,扩展也必须根据实际用途配置自己的 App ID 和能力。
2. 在 Xcode target 中添加能力
在 Signing & Capabilities 中添加:
Push Notifications
Xcode 通常会在 entitlements 文件中生成类似内容:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
"http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>aps-environment</key>
<string>development</string>
</dict>
</plist>
这里的 development 和 production 表示 APNs 环境。实际 Release 分发构建通常应使用生产环境。不要直接手工修改而忽略 Xcode 与 profile 的一致性;更安全的方式是通过 target 的 Capability 和签名配置让 Xcode 生成正确值。
3. 获取 APNs 发送凭据
设备端注册 token 只解决“设备如何被识别”。服务端还需要使用 APNs 认证方式发送消息,例如:
- APNs Auth Key;
- APNs Team ID;
- Key ID;
- 目标 Bundle ID。
这些服务端凭据不能放入 Flutter 应用中,因为应用包中的密钥可以被提取。应用端只负责注册和接收,发送凭据应保留在服务端。
4. 处理环境差异
开发构建使用开发 APNs 环境,App Store/TestFlight 构建使用生产环境。常见误判是“设备拿到了 token,因此服务端发送一定成功”。实际上以下因素都可能导致失败:
- token 属于不同 APNs 环境;
- 服务端 topic 与 Bundle ID 不一致;
- 使用了错误的 Team ID 或 Key ID;
- 用户没有授权通知;
- 应用没有正确注册远程通知;
- Notification Service Extension 的签名不完整。
Capability 的配置是系统授权链,不是 Dart 层安装一个插件就自动完成的功能。
六、版本号和构建号
Flutter 的 pubspec.yaml 可以写:
version: 1.2.3+45
在 iOS 中通常对应:
CFBundleShortVersionString = 1.2.3
CFBundleVersion = 45
含义不同:
1.2.3是用户看到的应用版本;45是本次构建版本。
构建命令也可以显式传入:
flutter build ipa \
--release \
--build-name=1.2.3 \
--build-number=45
--build-number 应作为发布流水线中的单调递增标识管理。一个已经上传并被 App Store Connect 接受的构建号通常不能原样再次上传到同一应用版本下。重新构建时,即使 Dart 代码没有改变,也需要提高构建号。
典型发布策略是:
用户版本:1.2.3
构建号:45、46、47
如果审核被拒但需要重新上传相同用户版本,可以保留 1.2.3,只提升构建号。
七、Flutter Release、Archive 和 IPA 的区别
1. Release 构建
Release 表示以发布优化方式编译应用。它与 Debug 的区别包括:
- Dart 代码经过发布编译;
- 调试服务和部分调试能力不再使用;
- 运行时行为更接近生产;
- 需要验证真实签名、权限和原生插件行为。
Release 并不等于已经完成 App Store 上传。未签名的 Release 应用仍然不能作为最终分发包。
2. Archive
Archive 是 Xcode 生成的归档目录,后缀通常为:
.xcarchive
它不是直接给用户安装的 IPA,而是一个包含归档应用、符号文件和构建元数据的交付中间产物。保留 Archive 和 dSYM 对崩溃分析很重要。
Flutter 可以直接生成 Archive 和 IPA:
flutter build ipa \
--release \
--build-name=1.2.3 \
--build-number=45
常见输出位置包括:
build/ios/archive/Runner.xcarchive
build/ios/ipa/*.ipa
具体文件名和输出细节可能随 Flutter 版本变化,应以命令实际输出为准。
3. IPA
IPA 本质上是一个可分发的压缩包。可以查看其结构:
unzip -l build/ios/ipa/*.ipa | head
通常能看到:
Payload/Runner.app/
IPA 是经过导出后的分发包。导出方式决定它是:
- App Store;
- Ad Hoc;
- Development;
- Enterprise(仅符合条件时)。
Archive 可以根据不同导出选项生成不同类型的 IPA;因此 Archive 和 IPA 不是两个独立编译阶段,而是“归档”和“按分发方式导出”的关系。
八、使用 ExportOptions.plist 生成可控的 IPA
对于本地一次性发布,可以让 Flutter/Xcode 自动处理签名:
flutter build ipa \
--release \
--build-name=1.2.3 \
--build-number=45
对于 CI,建议明确导出方式。例如:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
"http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>method</key>
<string>app-store</string>
<key>signingStyle</key>
<string>automatic</string>
<key>teamID</key>
<string>ABC123XYZ9</string>
</dict>
</plist>
构建:
flutter build ipa \
--release \
--build-name=1.2.3 \
--build-number=45 \
--export-options-plist=ios/ExportOptions-AppStore.plist
这里:
method=app-store表示生成上传 App Store Connect 的分发包;signingStyle=automatic表示由 Xcode 自动选择签名材料;teamID指定 Apple Developer 团队;- 前提是构建机器已登录正确账户,或已具备 Xcode 可用的签名授权。
如果 CI 使用手动签名,则需要在导出配置中指定证书和 profile。例如:
<key>method</key>
<string>app-store</string>
<key>signingStyle</key>
<string>manual</string>
<key>signingCertificate</key>
<string>Apple Distribution</string>
<key>provisioningProfiles</key>
<dict>
<key>com.example.weather</key>
<string>Weather AppStore Profile</string>
</dict>
Profile 名称必须与构建机安装的 profile 对应。若存在扩展,还需要为每一个 Bundle ID 配置 profile:
<key>provisioningProfiles</key>
<dict>
<key>com.example.weather</key>
<string>Weather AppStore Profile</string>
<key>com.example.weather.NotificationService</key>
<string>Weather Notification AppStore Profile</string>
</dict>
不要把这个例子理解为“只要改 plist 就能签名”。它只是告诉导出工具如何选择已经安装并且有效的证书和 profile。证书私钥、profile 文件、Team 权限和 Capability 仍必须提前准备。
九、归档前的清理、依赖和配置检查
推荐先完成依赖安装:
flutter pub get
cd ios
pod install
cd ..
如果插件或 Xcode 配置发生重大变化,可以执行:
flutter clean
flutter pub get
cd ios
pod install
cd ..
flutter clean 只能删除构建缓存,不能修复以下问题:
- Bundle ID 配错;
- Team 选错;
- profile 中没有 Capability;
- 证书私钥未导入;
- Apple Developer 账户没有相应权限;
- App Store Connect 元数据缺失;
- 插件要求的
Info.plist权限说明没有填写。
如果使用 flavor,还应明确每个 flavor 的 Bundle ID 和签名配置。例如:
flutter build ipa \
--release \
--flavor production \
-t lib/main_production.dart \
--build-name=1.2.3 \
--build-number=45
production 和 staging 如果使用不同 Bundle ID,就必须在 Apple Developer 中分别注册 App ID,并分别准备 profile。仅在 Dart 层使用不同环境变量,不会自动产生不同的 iOS 应用身份。
十、验证 Archive 和 IPA,而不是只看命令是否成功
构建命令退出码为 0 只能说明当前工具链认为构建成功,还不能证明上传一定成功。可以对导出的 IPA 做基础检查。
1. 解压 IPA
rm -rf /tmp/weather-ipa
mkdir -p /tmp/weather-ipa
unzip -q build/ios/ipa/*.ipa -d /tmp/weather-ipa
检查应用 Bundle ID:
/usr/libexec/PlistBuddy \
-c "Print :CFBundleIdentifier" \
/tmp/weather-ipa/Payload/Runner.app/Info.plist
预期输出:
com.example.weather
检查版本:
/usr/libexec/PlistBuddy \
-c "Print :CFBundleShortVersionString" \
/tmp/weather-ipa/Payload/Runner.app/Info.plist
/usr/libexec/PlistBuddy \
-c "Print :CFBundleVersion" \
/tmp/weather-ipa/Payload/Runner.app/Info.plist
2. 验证代码签名
codesign --verify --deep --strict --verbose=2 \
/tmp/weather-ipa/Payload/Runner.app
预期应看到验证成功;如果失败,常见原因包括:
- 包内容在签名后被修改;
- 嵌套 Framework 签名损坏;
- 扩展签名不匹配;
- 资源或可执行文件被替换;
- 导出过程使用了错误的签名材料。
3. 查看最终 entitlements
codesign -d --entitlements :- \
/tmp/weather-ipa/Payload/Runner.app
该命令查看的是签名产物中实际携带的 entitlements,而不是只看工程源文件。需要重点确认:
application-identifier;com.apple.developer.team-identifier;aps-environment;- App Groups;
- Associated Domains;
- Keychain Groups;
- 其他业务实际使用的能力。
4. 查看嵌入的 Provisioning Profile
应用包中通常会包含:
embedded.mobileprovision
它是 CMS 签名的 plist,可以解码:
security cms -D -i \
/tmp/weather-ipa/Payload/Runner.app/embedded.mobileprovision \
> /tmp/weather-profile.plist
查看 profile 的基本字段:
/usr/libexec/PlistBuddy \
-c "Print :Name" \
/tmp/weather-profile.plist
/usr/libexec/PlistBuddy \
-c "Print :ExpirationDate" \
/tmp/weather-profile.plist
/usr/libexec/PlistBuddy \
-c "Print :Entitlements" \
/tmp/weather-profile.plist
重点比较:
应用签名中的 entitlements
vs
Provisioning Profile 中的 Entitlements
如果应用请求的能力不在 profile 中,就违反了前面所述的集合条件:
E_app ⊆ E_profile
App Store 分发包的 profile 表现和本地开发包不同,不应仅凭某个字段是否存在判断“签名一定错误”;应结合导出方式、Xcode 版本和 App Store Connect 校验结果分析。
十一、典型签名失败及诊断路径
1. No profiles for ... were found
含义通常是 Xcode 找不到与以下条件同时匹配的 profile:
- Bundle ID;
- Team;
- 构建配置;
- 分发方式;
- 证书;
- Capability。
诊断顺序:
- 读取 Xcode 当前显示的 Bundle Identifier;
- 确认 Team 没有选错;
- 检查 Apple Developer 中是否存在该 App ID;
- 检查 profile 是否包含该 Bundle ID;
- 检查 profile 类型是否正确;
- 检查 profile 是否已安装到当前构建机;
- 如果启用自动签名,确认 Xcode 账户权限和网络可用。
2. Provisioning profile doesn't include the ... entitlement
这通常表示工程新增了 Capability,但 profile 尚未更新,或者 profile 对应的 App ID 没有开启该能力。
例如:
- Xcode 添加了 App Groups;
Runner.entitlements出现了 App Group;- Apple Developer 中 App ID 没有开启 App Groups;
- profile 仍是旧版本;
- 构建或上传失败。
恢复方式不是删除 Runner.entitlements 来掩盖错误。如果业务确实使用该能力,应当在 Apple Developer 中开启能力、重新生成或让 Xcode 更新 profile,然后重新归档。
3. 本地能运行,TestFlight 上传失败
开发设备运行通常使用 Development profile,而 TestFlight 上传使用 App Store 分发流程。两者验证的条件不同。
常见差异:
- 开发 profile 允许测试设备,App Store profile 不使用设备列表;
- 开发签名和分发签名不同;
- Release 配置没有包含某些 Debug 下才存在的设置;
- 扩展 target 的分发签名未配置;
- 生产 Capability 没有配置;
- App Store Connect 中缺少隐私或出口合规信息。
因此,“真机可以运行”不能证明“App Store 构建可以上传”。
4. Invalid Bundle
这个错误可能来自多个层面,不能仅凭错误标题定位。应查看上传处理的完整日志,重点检查:
- Bundle ID 与 App Store Connect 应用是否一致;
- 嵌套扩展的 Bundle ID 是否合法;
Info.plist中版本和必需字段;- 架构和最低系统版本;
- 嵌套 Framework 是否正确签名;
- 必需的隐私说明是否缺失;
- 应用包中是否包含不应上传的文件。
5. 上传成功但应用启动即崩溃
这通常发生在签名和上传都成功之后,原因可能转移到运行时:
- Release 配置下原生插件初始化失败;
- 权限请求路径与
Info.plist不一致; - Capability 配置了但服务端或关联域名配置错误;
- Flutter 资源未正确打包;
- 仅在 Debug 中存在的环境变量没有注入;
- 生产环境 API 地址、证书或配置错误。
诊断时应保留对应构建号的 dSYM,并结合 TestFlight 崩溃日志、设备控制台和插件原生日志分析。
十二、从 Archive 到 App Store Connect
上传并不是审核本身。完整流程可以分为几个状态:
stateDiagram-v2
[*] --> 本地构建
本地构建 --> Archive
Archive --> IPA导出
IPA导出 --> 上传
上传 --> AppStore处理
AppStore处理 --> 处理失败
AppStore处理 --> TestFlight可用
TestFlight可用 --> 提交审核
提交审核 --> 审核中
审核中 --> 通过
审核中 --> 被拒
被拒 --> 修复后重新上传
通过 --> 发布
1. 上传
可以使用:
- Xcode Organizer;
- Transporter;
- App Store Connect 支持的自动化上传流程。
在 Xcode 中打开:
Window → Organizer
选择对应 Archive 后,可以执行 Validate App 和 Distribute App。Validate 主要做本地和上传前检查,不能替代 App Review。
2. App Store Connect 处理
上传完成后,Apple 会处理构建,包括:
- 签名和包结构校验;
- 构建信息提取;
- 符号文件处理;
- 合规和平台规则检查;
- 生成 TestFlight 可用构建。
处理失败与审核拒绝不同:
- 处理失败:构建包本身或账户配置有问题;
- 审核拒绝:构建可处理,但应用功能、内容、隐私、元数据或商业行为不符合审核要求。
3. TestFlight
TestFlight 用于分发候选版本并收集测试反馈。内部测试和外部测试的流程、人数和审核要求不同,具体规则可能随 Apple 政策变化。
TestFlight 构建仍然必须是有效的分发构建。它不是“绕过签名”的测试方式,也不是开发 profile 的延伸。
测试时应覆盖:
- 首次安装和升级安装;
- 登录、支付、推送、深链;
- 权限拒绝后的行为;
- 无网络和网络恢复;
- 后台切换;
- 不同屏幕尺寸和系统版本;
- Release 模式下的原生插件行为。
十三、审核关注的不是“能否打包”,而是“是否符合平台契约”
App Review 会检查应用的实际行为和提交信息。以下内容尤其容易造成问题。
1. 权限说明必须解释真实用途
如果应用请求相机、相册、麦克风、定位、蓝牙或通讯录权限,应在 Info.plist 中提供清晰的用途说明。例如相机权限不能只写“需要相机”,而应说明用户将用相机完成什么操作。
Flutter 插件通常会要求这些原生配置。插件文档中的 Info.plist 修改不能省略,否则可能出现:
- 首次调用权限 API 时崩溃;
- 权限弹窗不符合预期;
- 审核人员无法理解用途;
- App Store Connect 或系统检查失败。
2. 隐私披露必须和实际数据流一致
应用应根据实际情况填写 App Store Connect 的隐私信息,考虑:
- 应用是否收集账号、设备标识、位置或诊断数据;
- 数据是否与用户身份关联;
- 数据是否用于追踪;
- 第三方 SDK 是否收集数据;
- 数据是否发送到自有服务或第三方服务。
不能因为业务代码是 Dart,就忽略插件和原生 SDK。分析隐私时应把以下数据流都纳入:
Flutter 代码
→ Flutter 插件
→ iOS 原生 SDK
→ 第三方 SDK
→ 服务端
3. 隐私清单和 SDK 要求
Apple 对部分第三方 SDK 的隐私清单、签名和使用方式有版本相关要求。Flutter 插件可能间接引入这些 SDK。发布前应检查插件版本和其 iOS 集成说明,不能仅因为 pod install 成功,就认为已经满足当前 SDK 政策。
这类要求具有版本敏感性,应以当前 Xcode、iOS SDK 和 Apple Developer/App Store Connect 后台提示为准。
4. 出口合规
如果应用使用加密能力,App Store Connect 可能要求填写出口合规信息。HTTPS、系统安全库和某些第三方加密库的处理方式不同。不能直接用“应用没有加密算法”作为判断依据,应按实际依赖和 Apple 的当前问卷回答。
5. 审核账号和可验证功能
如果应用需要登录,审核人员应能获得可用的测试账号,或者应用提供清晰的演示路径。支付、订阅、账号删除、内容审核和受限功能也必须能被验证。
审核拒绝通常不会因为 Flutter 本身,而是因为:
- 功能无法使用;
- 关键流程需要无法提供的账号;
- 权限用途不清楚;
- 元数据与实际功能不一致;
- 订阅或支付说明不完整;
- 应用包含明显未完成内容;
- 隐私实践与声明不一致。
十四、生产发布的签名材料管理
本地开发和 CI 的关键区别是:本地有交互式钥匙串和登录账户,CI 通常没有。
一个可重复的 CI 发布流程至少要管理:
Flutter SDK 版本
Dart/Flutter 依赖锁定
Xcode 版本
CocoaPods 依赖
Apple Team ID
Bundle ID
Distribution 证书与私钥
Provisioning Profile
App Store Connect 上传凭据
ExportOptions.plist
建议将以下内容作为受保护的 CI Secret 或安全文件:
.p12证书;.p12密码;- Provisioning Profile;
- App Store Connect API Key 及其私钥;
- 导出配置中的团队和 profile 信息。
不要把签名材料放在公开仓库中。构建日志也不应打印私钥密码、API Key 私钥或完整认证令牌。
自动签名依赖 Xcode 访问 Apple 账户或相关服务;手动签名依赖本机已安装的证书、私钥和 profile。两种方式都需要在流水线开始阶段验证材料是否存在,而不是等到导出最后一步才失败。
可以在 CI 中做早期检查:
security find-identity -v -p codesigning
预期应能看到有效的 Apple Distribution 身份。如果没有私钥,常见输出只包含证书或显示 0 valid identities found。
还可以检查 profile 是否安装:
ls -la "$HOME/Library/MobileDevice/Provisioning Profiles"
仅看到文件不能证明文件适用于当前 Bundle ID,应继续使用 security cms -D 解码并检查其 Entitlements、Team 和过期时间。
十五、一个完整的发布示例
假设:
Bundle ID: com.example.weather
用户版本: 1.2.3
构建号: 45
分发方式: App Store Connect
先确认 Flutter 工程:
flutter doctor -v
flutter pub get
open ios/Runner.xcworkspace
在 Xcode 中确认:
- Runner target 的 Bundle ID 是
com.example.weather; - Team 正确;
- Release 使用 Apple Distribution;
- Push Notifications 等所需 Capability 已配置;
Info.plist权限说明完整;- 扩展 target 也完成签名配置。
然后构建:
flutter build ipa \
--release \
--build-name=1.2.3 \
--build-number=45 \
--export-options-plist=ios/ExportOptions-AppStore.plist
检查输出:
find build/ios -maxdepth 3 \
\( -name "*.xcarchive" -o -name "*.ipa" \) \
-print
验证 IPA:
rm -rf /tmp/weather-ipa
mkdir -p /tmp/weather-ipa
unzip -q build/ios/ipa/*.ipa -d /tmp/weather-ipa
codesign --verify --deep --strict --verbose=2 \
/tmp/weather-ipa/Payload/Runner.app
/usr/libexec/PlistBuddy \
-c "Print :CFBundleIdentifier" \
/tmp/weather-ipa/Payload/Runner.app/Info.plist
预期:
com.example.weather
最后通过 Xcode Organizer 或 Transporter 上传,等待 App Store Connect 处理完成,再使用 TestFlight 验证 Release 版本,确认无误后提交审核。
十六、几个重要反例
反例一:只改 Info.plist,不配置 Capability
将以下内容手工写入 Info.plist:
<key>UIBackgroundModes</key>
<array>
<string>remote-notification</string>
</array>
并不等于应用已经获得后台推送能力。Info.plist 描述应用行为,Capability 和 entitlements 描述 Apple 授权。两者职责不同。
反例二:复制别人项目的 Runner.entitlements
不同应用的 Team ID、Bundle ID、App Group、Keychain Group 可能不同。直接复制 entitlements 可能产生:
- profile 不匹配;
- 应用访问错误的共享容器;
- 签名失败;
- 运行时数据隔离错误。
应根据当前 App ID 和实际能力重新配置。
反例三:开发机可以安装,所以可以提交
开发构建可能使用:
Apple Development + Development Profile
而提交构建需要:
Apple Distribution + App Store 分发 Profile
两者的设备授权、entitlements 和分发目标不同。开发安装成功只能证明开发签名链成立。
反例四:修改 IPA 后再上传
例如解压 IPA 后替换图片、Framework 或配置文件,再重新压缩。替换内容会破坏原来的签名;即使文件名和目录结构看起来正确,App Store Connect 仍可能拒绝,设备也可能无法验证。
正确做法是回到 Flutter/Xcode 构建流程中修改源文件,然后重新 Archive、导出和验证。
反例五:把 APNs 私钥放进 Flutter 应用
应用包中的资源和二进制都不能视为秘密。APNs Auth Key 或服务端证书放入应用后,攻击者可能利用它伪造推送请求。发送凭据必须留在服务端或受控的云端推送服务中。
十七、发布前的最小验证顺序
发布前可以按照依赖关系验证,而不是遇到错误后随机删除缓存:
- 身份:Bundle ID、Team、App Store Connect 应用是否一致。
- 能力:业务实际使用的 Capability 是否在 Apple Developer 和 Xcode 中都配置。
- 签名:证书包含私钥且未过期。
- 授权:Provisioning Profile 类型、Bundle ID、entitlements 和证书匹配。
- 构建:Release 模式能生成 Archive。
- 导出:Export method 与目标分发方式一致。
- 产物:IPA 内 Bundle ID、版本、构建号、签名和 entitlements 正确。
- 处理:App Store Connect 处理成功。
- 运行:TestFlight 安装后验证登录、支付、推送、深链、权限和升级。
- 审核:隐私、合规、测试账号和元数据与实际功能一致。
如果某一步失败,应优先检查该步骤所拥有的边界。例如:
flutter build失败,先看 Flutter、Dart、Pods 和 iOS 编译错误;codesign失败,先看证书、私钥和 profile;- App Store 处理失败,先看包结构、签名、Bundle ID、版本和合规;
- 审核被拒,先看功能可用性、权限说明、隐私和元数据。
这样可以避免把 Apple 签名问题误诊为 Flutter 问题,也避免用 flutter clean 处理本质上属于账户权限或分发配置的问题。
系列导航与关联阅读
- 系列入口:Flutter 完整学习路线:从 Dart 与 Widget 到多端架构和应用发布
- 上一篇:Flutter Android 发布:Gradle、签名、Flavor、AAB、权限和混淆
- 下一篇:Flutter 桌面发布:Windows、macOS、Linux 打包、签名和更新
官方资料
本文依据 Flutter 与 Dart 官方文档重新梳理;正文与示例由 WR BLOG 编写。

评论
0 条讨论