App Store 上架流程与签名机制

本文把 iOS 上架与签名讲透:从 Apple Developer Program 账号类型、Development 与 Distribution 证书的差异,到代码签名三要素(证书、描述文件、Entitlements)的协作机制;对比自动签名与手动签名的取舍,梳理 App ID 与 Capabilities 的配置、Archive 与导出 IPA 的完整链路、App Store Connect 元数据与隐私清单(Privacy Manifest)、App Review 常见拒审原因与申诉路径;最后覆盖 TestFlight 内测外测、分阶段发布与热更新限制、版本号与构建号管理,并给出上架 checklist 与常见坑清单。

开篇

如果说写代码是「把功能做出来」,那么签名与上架就是「把功能送到用户手里」——而这一步恰恰是 iOS 开发者抱怨最多的地方。证书过期、描述文件不匹配、Entitlements 冲突、Archive 导出失败、审核被拒,每一环都能让人卡上一整天。

这些问题的共同点是:它们都源于一个不透明的信任链。Apple 用「证书 + 描述文件 + Entitlements」三件套来回答一个问题——「这段代码,是否有资格,在这台设备上,使用这些能力运行」。理解了这条链,绝大多数签名报错都能自己推理出来。


一、Apple Developer Program 与账号类型

先分清账号,因为不同类型的账号能做的事情完全不同。

账号类型费用可上架适用场景
个人(Individual)99 美元/年是独立开发者
组织(Organization)99 美元/年是公司,需要 D-U-N-S 编号
企业(Enterprise)299 美元/年否仅内部分发,禁止公开
教育机构免费否教学用途

最常见的坑是把 Enterprise 账号当作分发渠道。Enterprise 证书(In-House)只允许分发给组织内部员工,一旦被检测到对外分发,证书会被 Apple 直接吊销,所有已安装的 App 立刻无法启动。

组织账号申请需要 D-U-N-S 编号,这是邓白氏给企业的唯一标识,申请周期可能长达数周,务必提前准备。此外,组织账号还需要一个与 Apple ID 绑定的法务联系人,用于接收审核与合规通知。


二、证书类型与用途

Apple 的证书按用途分成两大类,理解它们的分工是理解签名的基础。

证书类型用途私钥位置有效期
Apple Development开发调试,装到注册设备开发者机器1 年
Apple Distribution提交 App Store构建机1 年
iOS Distribution (In-House)企业内部分发企业1 年
Apple Push Services推送通知服务端服务端1 年
Pass Type IDWallet 卡券签名服务端1 年

关键认知:证书本身只是一个公钥 + 私钥的组合,公钥由 Apple 签发,私钥在你手里。签名时用私钥对 App 的哈希签名,设备用公钥验证。所以私钥丢失 = 证书作废,只能重新生成。

# 查看本机钥匙串里有哪些签名身份
security find-identity -v -p codesigning

# 输出示例:
#  1) ABC123... "Apple Development: Leeting Yan (TEAMID)"
#  2) DEF456... "Apple Distribution: Example Inc. (TEAMID)"
#     2 valid identities found

# 检查某个 .app 用的签名与 entitlements
codesign -dv --verbose=4 YourApp.app
codesign -d --entitlements - YourApp.app

三、代码签名三要素

签名不是一个孤立动作,而是「证书 + 描述文件 + Entitlements」三者的一致性检查。

1)证书(Certificate):证明「你是谁」,由 Apple 签发。

2)描述文件(Provisioning Profile):证明「你能在哪装、能做什么」。它是一个 .mobileprovision 文件,内部包含:

  • 开发者/团队标识;
  • 绑定的一张证书(可多张);
  • 允许的设备 UDID 列表(仅 Development / Ad Hoc);
  • 允许的 Entitlements 白名单;
  • 关联的 App ID。

3)Entitlements:App 声明要用哪些系统能力(推送、HealthKit、App Groups 等)。它必须同时出现在 App 的签名里和描述文件的白名单里,否则签名校验失败。

<!-- YourApp.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>production</string>
    <key>com.apple.security.application-groups</key>
    <array>
        <string>group.com.example.shop</string>
    </array>
</dict>
</plist>

三者的关系可以记成一句话:描述文件是「授权书」,Entitlements 是「申请单」,证书是「印章」。三者必须对齐,任何一个不匹配,codesign 就会报错。


四、自动签名与手动签名的取舍

Xcode 提供两种签名模式,团队应根据规模选择。

维度自动签名手动签名
配置方式Xcode 自动生成与管理手动指定证书与描述文件
适合场景单人 / 小团队本地开发CI、多环境、企业分发
可复现性差,依赖 Apple ID 登录态强,配置即代码
证书冲突易触发「证书数量超限」可控
上手成本极低需要理解三要素

自动签名的便利建立在「Xcode 用你的 Apple ID 去 Apple 后台自动创建证书和描述文件」之上。它的代价是:证书会被反复创建,免费账号还会受到「最多 3 个 App ID」等限制。

CI 环境必须用手动签名,因为 CI 机器上没有登录态,也无法交互。手动签名的配置项在 Xcode 的 Signing & Capabilities 面板里:

Signing Certificate: Apple Distribution
Provisioning Profile: match AppStore com.example.shop
Team: EXAMPLE1234

取舍:本地开发用自动签名,CI 与发布用手动签名 + fastlane match。这是绝大多数团队的稳态方案。切忌在 CI 上开自动签名,它会在无人值守时静默失败。


五、App ID 与 Capabilities

App ID 是 App 在 Apple 生态里的唯一标识,分两种:

  • 显式 App ID(Explicit):com.example.shop,一个 App 一个,支持所有 Capabilities;
  • 通配符 App ID(Wildcard):com.example.*,支持多个 App,但不支持推送、App Groups、iCloud 等能力。

结论很直接:只要用到任何 Capability,就必须用显式 App ID。

Capabilities 在 Xcode 的 Signing & Capabilities 面板里开启,但它本质上是改 Apple 后台的 App ID 配置。这带来一个经典坑:在 Xcode 里加了 Capability,但描述文件没重新生成,于是签名失败。正确顺序永远是:

  1. 在 Xcode 开启 Capability(同步到 Apple 后台);
  2. 重新生成描述文件(或用 fastlane match --force_for_new_devices);
  3. 在 Xcode 里选择新描述文件;
  4. 清理 DerivedData 后重新构建。

常见 Capabilities 与对应 Entitlement:

CapabilityEntitlement Key备注
Push Notificationsaps-environment需单独申请推送证书/Key
App Groupscom.apple.security.application-groups主 App 与扩展共享数据
Sign in with Applecom.apple.developer.applesignin上架必需(若提供第三方登录)
iCloudcom.apple.developer.icloud-container-identifiers需匹配容器 ID
HealthKitcom.apple.developer.healthkit审核时需说明用途

六、Archive 与导出 IPA

从代码到 IPA 的完整链路如下:

# 1. 归档(生成 .xcarchive)
xcodebuild archive \
  -workspace Shop.xcworkspace \
  -scheme Shop \
  -configuration Release \
  -archivePath build/Shop.xcarchive \
  -destination "generic/platform=iOS"

# 2. 导出 IPA(用 ExportOptions.plist 描述导出方式)
xcodebuild -exportArchive \
  -archivePath build/Shop.xcarchive \
  -exportOptionsPlist ExportOptions.plist \
  -exportPath build/ipa

ExportOptions.plist 决定了导出方式,是最容易配错的地方:

<?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-connect</string>
    <key>teamID</key>
    <string>EXAMPLE1234</string>
    <key>signingStyle</key>
    <string>manual</string>
    <key>uploadSymbols</key>
    <true/>
    <key>stripSwiftSymbols</key>
    <true/>
</dict>
</plist>

method 的取值随 Xcode 版本变化:Xcode 15 之前是 app-store,Xcode 15 起改为 app-store-connect。这是升级 Xcode 后 CI 突然失败的经典原因之一。

上传 IPA 有两种方式:xcrun altool(已废弃)和现代的 xcrun notarytool / xcrun altool --upload-app,目前推荐直接用 xcrun altool 的替代品或 fastlane 的 pilot / deliver,它们对 API Key 支持更好。


七、App Store Connect 元数据

提交审核前,App Store Connect 上的元数据必须完整。清单如下:

项目要求常见驳回原因
应用名称30 字符内,不与已有 App 混淆名称含他人商标
副标题30 字符内堆砌关键词
关键词100 字符内,逗号分隔竞品名称、无关词
截图各尺寸必备,真实反映界面用设计稿而非真机截图
隐私政策 URL必须有,且可访问链接 404
支持 URL必须有同上
年龄分级如实填写与内容不符
隐私清单声明数据收集类型与实际行为不符

隐私清单(Privacy Manifest) 是 iOS 17 之后的强制要求,PrivacyInfo.xcprivacy 必须声明:

  • 收集的数据类型(NSPrivacyCollectedDataTypes);
  • 使用的「必需原因 API」(NSPrivacyAccessedAPITypes),例如 UserDefaults、文件时间戳、磁盘空间等;
  • 追踪域(NSPrivacyTrackingDomains)。
<!-- PrivacyInfo.xcprivacy 片段 -->
<?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>NSPrivacyAccessedAPITypes</key>
    <array>
        <dict>
            <key>NSPrivacyAccessedAPIType</key>
            <string>NSPrivacyAccessedAPICategoryUserDefaults</string>
            <key>NSPrivacyAccessedAPITypeReasons</key>
            <array>
                <string>CA92.1</string>
            </array>
        </dict>
    </array>
    <key>NSPrivacyTracking</key>
    <false/>
</dict>
</plist>

自 iOS 17 起,如果第三方 SDK 自带的隐私清单缺失或声明不全,App 在审核时会被要求补齐,甚至直接拒审。


八、App Review 常见拒审原因

审核被拒是最消耗心力的环节。以下是最常见的原因与应对:

条款典型场景应对
2.1 性能问题崩溃、白屏、按钮无响应提交前用真机跑完整回归
2.3.1 隐藏功能审核时看不到的入口提供演示账号与操作说明
2.5.1 私有 API使用未公开 API静态扫描 PrivateFrameworks
2.5.2 代码下载热更新执行下载的代码移除 JSPatch 类方案
3.1.1 内购虚拟商品未走 IAP数字内容必须用 IAP
3.2.2 欺骗行为元数据与功能不符如实描述
4.2 最低功能只是网页壳增加原生能力
5.1.1 数据收集未获授权就采集弹窗 + 隐私政策
5.1.2 数据使用声明与实际不符更新隐私清单

申诉(Appeal)的正确姿势:不要情绪化回复。审核员看到的是模板化的沟通,所以回复要结构化:

  1. 引用具体的审核条款编号;
  2. 说明你已做的修改或提供澄清证据(截图、录屏、演示账号);
  3. 如有必要,通过 App Store Connect 的「App Review Board」发起正式申诉。

通常一次澄清回复就能解决;如果涉及条款理解分歧,可以请求电话沟通(Resolution Center 支持预约)。


九、TestFlight 与分阶段发布

TestFlight 是上架前最重要的验证环节,分两层:

  • 内部测试:最多 100 名团队成员,构建上传后立即可用,无需审核;
  • 外部测试:最多 10000 人,第一个构建需要经过一次轻量审核(通常 24 小时内)。

外部测试的构建有效期是 90 天,到期后无法再安装。测试者安装需要先装 TestFlight App,这是一个不可忽略的转化漏斗——真实用户测试的参与率往往只有邀请数的一半。

正式发布后,App Store Connect 提供分阶段发布(Phased Release):按 1%、2%、5%、10%、20%、50%、100% 在 7 天内逐步放开。它的价值在于可以暂停:一旦发现崩溃率飙升,立刻暂停发布,把影响面控制在已放开的比例内。

注意:分阶段发布只对自动更新生效,主动到 App Store 页面点击「更新」的用户会直接拿到新版本。所以它降低风险,但不消除风险。


十、热更新限制与合规红线

Apple 对热更新的态度非常明确:禁止下载并执行可改变 App 主要功能的代码。

  • 允许:JavaScriptCore 运行预置脚本、服务端下发的配置、A/B 实验参数、远程开关;
  • 禁止:JSPatch、Rollout 这类动态修复 OC/Swift 方法的方案;用 dlopen 加载外部框架。

这条线的判断标准是「是否改变了 App 的主要功能与预期用途」。配置下发与内容更新是安全的,逻辑热修复是危险的。

替代方案是服务端开关(Remote Config)+ 强制更新:出问题时用开关降级功能,用强制更新弹窗引导用户升级。这是合规且有效的止损手段。


十一、版本号与构建号管理

两个数字容易混淆,必须分清:

字段名称规则示例
CFBundleShortVersionString版本号面向用户,主.次.修订3.2.1
CFBundleVersion构建号面向系统,同一版本内必须递增1024

规则:

  • 同一版本号下的构建号必须唯一且递增,否则上传被拒;
  • 版本号可以跨版本跳跃(如 3.2.1 → 4.0.0),但构建号最好全局单调递增,便于排查;
  • 建议在 CI 里用构建序号自动填充构建号,避免人工出错。
# 在 CI 中用 fastlane 自动递增构建号(推荐用全局递增的 build number)
# Fastfile 片段
lane :bump do
  increment_build_number(
    build_number: ENV["CI_PIPELINE_IID"]   # 或 latest_testflight_build_number + 1
  )
end

运行时要读取这两个值,统一从 Bundle.main 取,避免散落硬编码:

import Foundation
import OSLog

enum AppVersion {
    /// 面向用户的版本号,如 "3.2.1"
    static var short: String {
        Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String ?? "0.0.0"
    }

    /// 面向系统的构建号,如 "1024"
    static var build: String {
        Bundle.main.object(forInfoDictionaryKey: "CFBundleVersion") as? String ?? "0"
    }

    /// 上报崩溃与埋点时带上完整版本,便于按版本切分数据
    static var full: String { "\(short) (\(build))" }
}

// 示例:随崩溃日志一起上报
os_log("app launched version=%{public}@", AppVersion.full)

取舍:构建号用 CI 的流水线序号(全局单调递增)最省心,代价是与版本号不再一一对应;若追求「版本号 + 递增序号」的可读性,则需在 Fastfile 里先查 latest_testflight_build_number 再自增,多一次网络往返。


十二、上架流程 checklist

发布前逐项打勾,能消灭大部分低级失误:

  • 版本号与构建号已更新且唯一;
  • Release 配置下关闭了调试日志、测试后端地址、模拟数据开关;
  • 隐私清单 PrivacyInfo.xcprivacy 已补齐,第三方 SDK 清单齐全;
  • 使用了私有 API 的静态检查(nm / strings 扫一遍);
  • 所有 Capabilities 与描述文件、Entitlements 三者一致;
  • App Store Connect 元数据(截图、关键词、隐私政策、支持 URL)完整;
  • 演示账号已填,且审核期间有效;
  • TestFlight 外部测试跑满 3 天无新增崩溃;
  • 分阶段发布已勾选,崩溃率监控已就绪;
  • dSYM 已归档,崩溃符号化链路可用。

十三、常见签名与上架坑清单

  • 证书数量超限:Apple 限制每类证书的数量,反复用自动签名会耗尽配额,需去后台吊销旧证书。
  • 描述文件不含新设备:Development 描述文件绑定了 UDID 列表,加新设备后必须重新生成。
  • Entitlements 与描述文件不一致:最常见于 App Groups 与推送,务必核对两边的 key 完全一致。
  • 钥匙串里有重复证书:codesign 会选错身份,用 security find-identity 清理。
  • CI 上 -exportArchive 报 no signing certificate:钥匙串未解锁或未设默认。
  • app-store vs app-store-connect:Xcode 15 起 export method 改名,CI 脚本必须同步更新。
  • 上传后长时间 processing:通常是缺少隐私清单或使用了未声明 API,会收到邮件提示。
  • 审核期演示账号失效:审核员登录失败会直接以 2.1 拒审,演示账号有效期要覆盖审核周期。
  • App Groups 在扩展里读不到数据:主 App 与扩展的 App Group ID 拼写必须完全一致,且都需要对应 entitlement。
  • 误用 Enterprise 证书对外分发:证书被吊销后所有 App 立即无法启动,属于不可逆事故。
  • 修改 App 名称后旧版本冲突:新名称若与已下架 App 重名,会被拒;需先在后台确认可用性。
  • 忘记归档 dSYM:上线后崩溃无法符号化,等于放弃了线上排障能力。

取舍:签名配置优先「显式、可复现」而非「省事」。自动签名的省事是以不可复现为代价的,一旦团队超过两人或引入 CI,就必须切换到手动签名 + match 的路线。


相关阅读


小结

本文把 iOS 上架与签名拆成了一条可推理的信任链:

  1. 账号类型决定你能做什么——Enterprise 账号禁止公开分发,组织账号需提前准备 D-U-N-S;
  2. 证书分 Development 与 Distribution 两类,私钥丢失即作废;
  3. 签名三要素是证书、描述文件、Entitlements 的一致性校验,描述文件是授权书,Entitlements 是申请单;
  4. 自动签名适合本地、手动签名适合 CI 与发布,团队一旦引入 CI 就应切到 match;
  5. App ID 与 Capabilities 必须用显式 ID,改 Capability 后必须重新生成描述文件;
  6. Archive 与导出依赖 ExportOptions.plist,注意 Xcode 15 起 app-store-connect 的命名变化;
  7. 隐私清单自 iOS 17 起强制,第三方 SDK 的清单同样要补齐;
  8. App Review 的拒审绝大多数落在性能、隐藏功能、内购与数据收集四类,申诉要结构化;
  9. TestFlight 与分阶段发布是最有效的止损手段,但分阶段只对自动更新生效;
  10. 热更新有明确红线,配置下发安全,逻辑热修复危险,替代方案是远程开关加强制更新。

上架不是开发的终点,而是「与 Apple 的规则长期共存」的起点。把签名配置写成可复现的代码,把隐私与审核要求当成需求的一部分,这条路就会顺畅得多。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「iOS 开发」更多文章

  1. Swift Package Manager 与模块化拆分
  2. Core Animation 与 SwiftUI 动画
  3. iOS 安全:Keychain、生物识别与传输安全