Flutter 基础体系 · 第 76/80 篇。示例基于当前稳定 Flutter 与 Dart 3 语言能力;Android、iOS、桌面和 Web 差异会明确说明。

Flutter iOS 发布:证书、Provisioning、Capability、归档和审核

Flutter iOS 发布不是把 flutter build 生成的文件直接上传到 App Store Connect。一个可安装、可运行、可提交审核的 iOS 应用,至少要同时满足以下条件:

  1. Flutter 工程能够生成 iOS 原生工程和 Release 构建产物。
  2. Apple Developer 账户中存在与 Bundle ID 对应的 App ID。
  3. 应用使用有效的签名证书签名。
  4. 应用使用与签名证书、Bundle ID、Capability 和分发方式匹配的 Provisioning Profile。
  5. 应用中声明的 entitlements 与 Apple 端允许的能力一致。
  6. 归档产物通过签名、校验、上传和 App Store Connect 处理。
  7. 应用的权限说明、隐私信息、元数据和功能行为符合审核要求。

其中,证书解决“谁签署了应用”的问题,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 的安全存储中。签名流程可以简化为:

  1. Apple 为开发者签发证书;
  2. 开发者机器生成或持有对应私钥;
  3. Xcode 使用私钥对应用代码和资源计算出的摘要进行签名;
  4. 系统使用证书中的公钥验证签名;
  5. 系统进一步检查证书是否由 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 平台对应用能力的声明机制。它通常同时涉及:

  1. Developer 账户中对 App ID 的能力开关;
  2. Xcode target 的 Signing & Capabilities 配置;
  3. 应用的 entitlements 文件;
  4. Provisioning Profile 中允许的 entitlements;
  5. 相关系统服务的独立配置。

例如,应用需要远程推送时,不能只调用 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、插件生成的原生二进制、资源和可能存在的扩展。最终分发前需要对可执行代码进行签名,并且嵌套代码的签名关系也必须正确。

因此,不能简单地:

  1. 先生成 IPA;
  2. 随便替换一个 .framework
  3. 再认为原签名仍有效。

修改已经签名的内容会使签名摘要失效。任何修改都应在正确的构建和签名流程中完成。

约束二: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>

这里的 developmentproduction 表示 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

productionstaging 如果使用不同 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。

诊断顺序:

  1. 读取 Xcode 当前显示的 Bundle Identifier;
  2. 确认 Team 没有选错;
  3. 检查 Apple Developer 中是否存在该 App ID;
  4. 检查 profile 是否包含该 Bundle ID;
  5. 检查 profile 类型是否正确;
  6. 检查 profile 是否已安装到当前构建机;
  7. 如果启用自动签名,确认 Xcode 账户权限和网络可用。

2. Provisioning profile doesn't include the ... entitlement

这通常表示工程新增了 Capability,但 profile 尚未更新,或者 profile 对应的 App ID 没有开启该能力。

例如:

  1. Xcode 添加了 App Groups;
  2. Runner.entitlements 出现了 App Group;
  3. Apple Developer 中 App ID 没有开启 App Groups;
  4. profile 仍是旧版本;
  5. 构建或上传失败。

恢复方式不是删除 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 或服务端证书放入应用后,攻击者可能利用它伪造推送请求。发送凭据必须留在服务端或受控的云端推送服务中。


十七、发布前的最小验证顺序

发布前可以按照依赖关系验证,而不是遇到错误后随机删除缓存:

  1. 身份:Bundle ID、Team、App Store Connect 应用是否一致。
  2. 能力:业务实际使用的 Capability 是否在 Apple Developer 和 Xcode 中都配置。
  3. 签名:证书包含私钥且未过期。
  4. 授权:Provisioning Profile 类型、Bundle ID、entitlements 和证书匹配。
  5. 构建:Release 模式能生成 Archive。
  6. 导出:Export method 与目标分发方式一致。
  7. 产物:IPA 内 Bundle ID、版本、构建号、签名和 entitlements 正确。
  8. 处理:App Store Connect 处理成功。
  9. 运行:TestFlight 安装后验证登录、支付、推送、深链、权限和升级。
  10. 审核:隐私、合规、测试账号和元数据与实际功能一致。

如果某一步失败,应优先检查该步骤所拥有的边界。例如:

  • flutter build 失败,先看 Flutter、Dart、Pods 和 iOS 编译错误;
  • codesign 失败,先看证书、私钥和 profile;
  • App Store 处理失败,先看包结构、签名、Bundle ID、版本和合规;
  • 审核被拒,先看功能可用性、权限说明、隐私和元数据。

这样可以避免把 Apple 签名问题误诊为 Flutter 问题,也避免用 flutter clean 处理本质上属于账户权限或分发配置的问题。


系列导航与关联阅读

官方资料

本文依据 Flutter 与 Dart 官方文档重新梳理;正文与示例由 WR BLOG 编写。