手上拿到一个.ipa文件,双击之后系统弹窗问你要用什么程序打开——这个瞬间大概是很多人第一次独立发布 iOS 应用时最先卡住的地方。构建能跑通、模拟器里点得动、真机装得上,偏偏到了"把它交给 App Store Connect"这一步就原形毕露:Xcode 的 Organizer 转圈转到一半报红字,Transporter 拖进去提示认证失败,命令行敲完一堆命令只留下一句语焉不详的Unexpected status code。
这篇东西就是围绕"IPA 上传到 App Store Connect"这一件事展开的。我不打算把官方文档翻译一遍,而是把几条真实可用的上传路径拆开讲——图形界面的 Archive 与 Organizer、独立工具 Transporter、命令行里的altool与 App Store Connect API、以及进了流水线之后 fastlane 的做法,每一类都交代清楚它适合谁、要求什么前提、容易在哪个环节掉链子。中间会穿插我自己反复踩过的坑,也会给可以直接抄走的命令和配置。不管你是刚接手发布流程的新人,还是想把上传这一步塞进自动化流水线的老手,应该都能从里面挑到一段用得上的东西。
1. 从 .ipa 到 App Store Connect 之间隔着什么
1.1 一个包要过的那几道关
很多人把"上传"理解成一个动作,其实它是四五个环节串起来的链条,任何一环出问题都表现为"上传失败",但根因完全不同。第一关是构建与归档,Xcode 把源码编译成.xcarchive;第二关是签名与导出,用描述文件和证书把归档包成.ipa;第三关才是传输,把二进制推送到 App Store Connect 的服务器;第四关是后台处理,也就是大家常说的 processing,服务端会重新校验签名、扫描 Info.plist、检查图标、分析二进制架构、读取隐私清单,最后才把构建标记成"可供测试"或"可供提交审核"。
之所以要先把这条链路摆出来,是因为后面所有排错都建立在"定位在哪一环"这个判断上。传输阶段的失败通常是网络中断、认证失效、包体过大超时;处理阶段的失败则几乎全是包本身不合规,服务端会给你的开发者邮箱发一封ITMS-xxxxx开头的邮件,里面写清楚了哪一项没过。分不清这两类,排查就会在错误的方向上耗掉一整天。
还有一个容易混淆的点:上传成功不等于包能用。命令行返回 0、Transporter 弹出"交付成功",只代表文件到达了服务器,此时后台状态往往还是"正在处理"。我见过有团队在上传完成的瞬间就去点"提交审核",结果因为构建还在处理中,按钮是灰的,白白慌了半天。
1.2 上传前自己必须核对的三组标识
与其等服务器告诉你哪里不对,不如在点上传之前先做一遍自检。需要对齐的东西其实就三样,但每一样都有细节。
Bundle Identifier,也就是包的唯一身份。它必须和 App Store Connect 后台创建的 App 记录里那个 Bundle ID 完全一致,大小写都算。做过一次改名的项目尤其容易在这里翻车——后台记录还是旧 ID,工程里已经改成新的,上传时报的描述通常是找不到对应的 App 记录。
版本号 CFBundleShortVersionString,面向用户展示的那个,比如2.4.0。这个值只在你准备上架新版本时才需要变,同一个版本号下可以挂多个构建。
构建号 CFBundleVersion,这才是上传时服务端真正卡得最死的一项。同一个版本号下,新上传的构建号必须严格大于此前上传过的任何构建号,否则直接拒绝,报错信息大意是The bundle version must be higher than the previously uploaded version。这里有个坑:构建号不是数字语义上的比较,它按字符串逐段比对,1.10会被判定为小于1.9。如果你用日期加自增数拼接,务必保证位数对齐,比如统一用20240512001这种定长格式。
我的习惯是让构建号由 CI 的流水线序号或者提交时间戳生成,本地调试时随便填,一旦进入发布分支就交给脚本统一注入,人手上限只改版本号。这样能彻底杜绝"两个人各自改构建号结果撞车"的情况。
1.3 不是所有 IPA 都有资格上传
这一条被忽略的频率高得惊人。导出 IPA 的时候有一个分发方式(method)的选择,常见的有 Development、Ad Hoc、Enterprise(企业内部分发)、App Store 四种。只有按 App Store 方式导出的包才能上传到 App Store Connect,其余三种的签名和授权方式完全不同,上传时会被判定为签名无效或者包结构不符。
判断手上这个包是什么血统,不用打开 Xcode,用两条命令就能看清楚:
unzip -p MyApp.ipa "Payload/MyApp.app/embedded.mobileprovision" > /tmp/p.mobileprovision security cms -D -i /tmp/p.mobileprovision > /tmp/p.plist plutil -p /tmp/p.plist | grep -E "get-task-allow|ProvisionsAllDevices|ExpirationDate"如果get-task-allow是true,说明这是开发包,可以挂调试器;如果出现ProvisionsAllDevices为true,那是企业分发包;两者都没有、且描述文件里带着一串设备 UDID,那就是 Ad Hoc。App Store 分发的描述文件既没有调试授权,也不绑定设备。
顺手还可以看看包里的证书类型:
codesign -dvvv Payload/MyApp.app 2>&1 | grep -E "Authority|TeamIdentifier"输出里如果是Apple Distribution或者iPhone Distribution才对路,出现Apple Development就说明导出时选错了方式。这两条命令我几乎每次交接外部团队交付的包时都会跑一遍,比问对方"你是按什么方式打的"可靠得多。
2. Xcode 图形界面这条路,细节比想象中多
2.1 Archive 之前工程里该确认的设置
用 Xcode 走发布是绝大多数人的起点,因为不需要额外装东西,也没有认证配置的负担。但它的顺畅程度高度依赖工程本身是否干净。
第一件事是确认 Scheme 的 Archive 动作使用的是 Release 配置。Product 菜单里选 Archive 时,Xcode 取的是 Scheme 设置中 Archive 对应的 Build Configuration,默认是 Release,但如果有人为了调试方便改过,打出来的包性能表现会完全不对。同时在 Edit Scheme 里看一眼是否勾了Reveal Archive in Organizer,没勾的话归档完成不会自动弹窗。
第二件事是检查SKIP_INSTALL。如果工程里有自建的 framework target,它的SKIP_INSTALL必须是 NO,否则归档时这个 framework 不会被打进 Products 目录,导出的包在启动时会因为 dyld 找不到动态库而闪退。这个坑的隐蔽之处在于:编译全程不报错,上传也成功,只有装到设备上一点开就崩。
第三件事是版本信息的注入方式。如果版本号是从脚本或者环境变量写进 Info.plist 的,要确认 Archive 阶段这个脚本真的执行了。我遇到过一种情况,脚本挂在 Build Phases 的最后,而CFBundleShortVersionString在Process Info.plist File那一步就被读取了,导致写进去的值根本没被采用,最终包里的版本号还是模板里的1.0。
2.2 Organizer 里 Distribute App 的几个分叉
归档结束后 Organizer 会列出所有历史归档,右侧的 Distribute App 按钮展开是一次选择树,选错分支就前功尽弃。
选App Store Connect之后还有下一层:Upload 表示直接传到服务器,Export 表示只导出 IPA 文件到本地。我一般选 Export,理由是本地留一份可回溯的产物,出问题时能拿这个包直接再传一次,不用重新编译。CI 环境里更是必须走 Export,因为 Organizer 本身没法在无人值守的机器上点。
再往下是签名方式的选择:Automatically manage signing 和 Manually manage signing。自动签名省事,Xcode 会去后台拉取或者生成描述文件,前提是本机登录的开发者账号有足够权限;手动签名适合证书统一由团队管理的场景,需要指定证书和描述文件。还有一个Upload your app's symbols的勾选项,用来把 dSYM 一起传上去,方便后台符号化崩溃日志,如果不是出于包体积的极端考虑,建议保持勾选。
最后一屏会列出本次导出的摘要信息,包括签名证书、描述文件、支持的设备架构。这一屏值得花三十秒扫一眼,尤其是描述文件的过期时间——有时候 Xcode 缓存了一个快过期的描述文件,导出的包上传后会在处理阶段被拒。
2.3 卡在"正在验证 App"时的排查顺序
Organizer 的上传过程分两段:先验证(Validate),再传输。卡在验证阶段超过两三分钟,基本可以确定不是网络慢。我的一般排查顺序是这样:
先看包体大小,超过 4GB 的包在部分网络环境下传输会非常吃力,虽然官方的单包上限比这宽松,但实际体验上大包更容易中断。再看本机时间是否准确,系统时间偏差过大时 TLS 握手环节可能出现证书校验异常,这个在虚拟机或者长期没联网的机器上并不罕见。然后检查 Xcode 的登录状态,Account 面板里如果显示会话过期,重新登录一次往往就好了。最后才怀疑网络,换一个出口试试。
传输阶段中断的表现是进度条走到某个百分比停住,然后报The network connection was lost之类。Xcode 的上传支持断点重试,重新点一次 Distribute 会从头开始传,但对同一个构建号重复上传是被允许的——只要内容有变化,构建号不变也能覆盖,这一点和很多人以为的"构建号必须每次递增"不同。真正被卡死的是"构建号小于等于已上传的最大值",等于的时候如果内容不同,是可以覆盖的。
2.4 用命令行复刻 Archive 和导出
图形界面的每一步都能翻译成命令,这在需要重复执行或者写脚本时非常有用。归档:
xcodebuild archive \ -workspace MyApp.xcworkspace \ -scheme MyApp \ -configuration Release \ -destination 'generic/platform=iOS' \ -archivePath ./build/MyApp.xcarchive \ -allowProvisioningUpdates-destination指定generic/platform=iOS而不是具体的模拟器或真机,是因为 Archive 需要的是通用设备构建。-allowProvisioningUpdates允许 Xcode 在命令行环境下自动更新描述文件,不加的话在证书过期时会直接失败。
导出:
xcodebuild -exportArchive \ -archivePath ./build/MyApp.xcarchive \ -exportPath ./build/ipa \ -exportOptionsPlist ./ExportOptions.plist \ -allowProvisioningUpdates配套的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>signingStyle</key> <string>automatic</string> <key>teamID</key> <string>ABCDE12345</string> <key>uploadSymbols</key> <true/> <key>stripSwiftSymbols</key> <true/> <key>destination</key> <string>export</string> </dict> </plist>method这个字段的取值在近几个大版本里改过,老版本写app-store,新版工具链接受app-store-connect,两者在多数版本上还能兼容,但如果导出时报"unknown method",先怀疑这里。destination设为export表示导出到本地目录,设成upload则会直接走上传流程,后者在 CI 里挺方便,代价是看不到中间产物。
3. Transporter:不依赖工程文件的投递箱
3.1 它解决的正是"我手上只有一个包"
Transporter 是一个独立的桌面应用,作用很单纯:把打包好的文件交给 App Store Connect。它最大的价值在于不需要 Xcode 工程。外包团队交付的 IPA、几个月前归档的历史包需要重传、设计同事从别处拿到的测试包,这类场景下打开 Xcode 反而变成了负担——尤其是当你的 Xcode 版本和当初打这个包的版本对不上时,Organizer 可能压根不认这个归档。
它还有一个隐性优势:走的是和 Xcode 完全相同的服务端接口,但因为是个纯上传工具,出现问题时干扰因素少,报错信息往往更直接。我遇到认证类问题时,经常用 Transporter 做交叉验证——如果 Transporter 能传上去而 Organizer 不行,那问题基本锁定在本地 Xcode 环境而不是账号或包本身。
3.2 登录方式为什么必须用 App 专用密码
这一步拦住的人特别多。开启了两步验证或者双重认证的账号,直接在 Transporter 里输账号密码是登不进去的,必须用App 专用密码(App-Specific Password)。
生成路径在 App Store Connect 后台的"用户和访问"里,找到"集成"标签页,选择 App 专用密码,点加号生成一个。生成时会让你填一个标签,随便写个能认出来的名字,比如"发布机专用"。生成结果是一串形如xxxx-xxxx-xxxx-xxxx的字符,只显示一次,关掉窗口就再也看不到了,必须当场存好。
之所以要用专用密码而不是主密码,逻辑很直接:主密码一旦泄露影响的是整个账号,而专用密码可以单独吊销,且权限仅限于工具类操作。团队里多人协作时更应该一人一个专用密码,谁离职就删谁的那一个,不用惊动其他人。这个习惯我从第一次因为共用密码导致全员改密码之后就养成了。
在 Transporter 里填账号和这个专用密码,登录成功后凭证会被记住,后续上传不用重复输入。如果频繁提示认证失败,先去后台确认这个专用密码是否已被吊销——改主密码会导致所有专用密码失效,这个联动关系很多人不知道。
3.3 交付窗口里的验证与上传是两件事
把 IPA 拖进 Transporter 之后,界面会列出这个包的摘要,然后有两个动作:验证(Verify)和上传(Upload)。验证只做检查不上传,速度快,能在真正传输之前把大部分结构性问题拦下来——签名、图标、Info.plist 必填项这些都查。我的习惯是任何一次正式上传之前先跑一遍验证,尤其是接手别人的包时。
上传过程会把包切成若干块并行传输,界面上能看到分块进度。中断后重新拖入同一个文件,Transporter 会从头开始,不像某些工具支持续传,所以网络环境不好的时候尽量不要传大包。
底部的日志面板值得留意,它记录的是原始的服务端响应。上传成功后会出现类似Deliver successfully的字样和本次的传输 ID;失败时这里的信息比弹窗详细得多,能直接看到服务端返回的错误码。很多人只看弹窗就关掉了,其实答案都藏在日志里。
3.4 命令行里调用 iTMSTransporter
Transporter 的底层是一个叫iTMSTransporter的命令行组件,藏在 Xcode 的应用包里,通过xcrun就能调到。这意味着批量上传可以脚本化:
xcrun iTMSTransporter -m upload \ -assetFile ./build/ipa/MyApp.ipa \ -apiKey ABCDE12345 \ -apiIssuer 00000000-0000-0000-0000-000000000000 \ -v informational-m参数除了upload还支持verify,对应图形界面里的验证动作。-v控制日志详细程度,排查问题时调到informational甚至debug,正常跑批的时候用默认值避免日志刷屏。认证方式除了 API 密钥,也支持-u加-p传账号和专用密码,后者在临时脚本里更省事。
这套命令在需要连续上传十几个包(比如多 target 的应用矩阵)时特别有用,写个循环就完事了,比一个个拖窗口快太多。需要注意的是它依赖 Xcode 环境,纯 CI 容器里如果没有装完整 Xcode 是跑不起来的。
4. 命令行与 API 路线:altool 的退场与新接口的登场
4.1 altool 现在处于什么位置
altool曾经是命令行上传的默认选择,参数简单、上手快。随着官方把上传能力逐步收拢到 Transporter 和 App Store Connect API 上,altool的上传功能虽然还能用,但已经处于被淡化处理的状态——新项目不建议再以它为主力,存量脚本倒是没必要急着改。
它仍然值得了解的原因有两个。一是大量历史 CI 配置里还在用它,接手别人的流水线时必须看得懂;二是它的输出格式对脚本很友好,能直接吐 JSON,便于解析成功与否。至于什么时候彻底不能用了,这个时间点只有官方说了算,工程上的合理做法是把上传逻辑封装成一层薄封装,底层从altool换成别的实现时只改一处。
4.2 用 API 密钥走通一次上传
App Store Connect API 密钥是目前最"正规"的认证方式,一次配置,长期可用,不受个人账号密码变更的影响。获取流程在后台的"用户和访问"→"集成"→"App Store Connect API"里,需要账号持有管理员或相应权限。
生成之后会得到三样东西:Issuer ID(一个 UUID 形式的字符串)、Key ID(十位左右的字母数字)、以及一个.p8私钥文件,同样只能下载一次。私钥文件要放在约定位置,命令行工具默认会去这个目录找:
~/.appstoreconnect/private_keys/AuthKey_<KeyID>.p8文件名里的<KeyID>必须和实际的 Key ID 一致,包括大小写。放好之后执行上传:
xcrun altool --upload-app \ -f ./build/ipa/MyApp.ipa \ -t ios \ --apiKey ABCDE12345 \ --apiIssuer 00000000-0000-0000-0000-000000000000-t ios指定平台类型,做 macOS 应用时换成osx。如果不想把私钥放在默认目录,可以用--private-key之类的参数另行指定,具体以当前工具链的--help输出为准,这块参数在版本间有过调整。
密钥的权限范围是可以细分的,后台创建时能选择角色。给 CI 用的密钥建议只授予"开发人员"或者更窄的权限,而不是管理员,这样万一密钥泄露,影响面可控。这一点和前面说的 App 专用密码是同一个思路。
4.3 怎么判断这次上传到底成没成
命令行工具最坑的地方在于:没报错不等于成功。有一次我在一个 shell 脚本里连着跑了三步,前两步失败但被||吞掉了,第三步拿着一堆空变量照样执行,最后的日志看起来一切正常,实际上包根本没传上去。
稳妥的做法是显式检查退出码,并且解析输出内容:
set -o pipefail if xcrun altool --upload-app -f "$IPA_PATH" -t ios \ --apiKey "$API_KEY" --apiIssuer "$API_ISSUER" \ --output-format json > upload.json 2>&1; then echo "上传动作返回成功" else echo "上传失败,退出码 $?" cat upload.json exit 1 fi--output-format json会把结果整理成结构化数据,里面有success-message或者product-errors数组。对于失败的情况,product-errors里每条都带code、message、description,直接拿message去搜比截屏发群里有价值得多。
再强调一次,这里的"成功"仅指文件送达。真正的可用状态要等后台处理完成,脚本里如果需要等待,得轮询构建状态而不是睡眠固定秒数。App Store Connect API 提供了查询构建列表的接口,把上传返回的构建信息拿去做轮询,比盲等靠谱。
4.4 直接调 REST 接口的可行性
如果有更深的定制需求,比如上传完成后自动打标签、自动分配给测试组、自动填写发布说明,那就可以抛开命令行工具,直接用 App Store Connect API 的 REST 接口。上传本身的接口涉及创建上传任务、按服务端返回的分片信息逐块 PUT、最后提交,流程比一条命令复杂不少。
坦白说,除非你在做平台化工具,否则没必要自己实现这套。用现成的封装库或者干脆用 fastlane,收益要明显得多。真要上手,务必先从官方 API 文档把上传流程的时序图看明白,尤其是分片上传的偏移量和校验和这两个字段,格式错了服务端只返回一个笼统的 400,排查起来相当费劲。
5. 塞进流水线:fastlane 把上传变成一行配置
5.1 pilot 和 deliver 不是一回事
fastlane 里跟上上传相关的两个常见工具,新手很容易混。pilot(在 lane 里写作upload_to_testflight)专门负责把构建传到 TestFlight 并分发给测试人员,是发布流程里的高频操作;deliver的主业其实是管理 App Store 的元数据和截图,顺带支持用--ipa参数上传一个已经打好的包。
选择上很简单:日常测试分发用upload_to_testflight,它的参数围绕测试组、外部测试、审核信息展开,正好覆盖 TestFlight 的用法;如果是想把元数据和二进制一起提交,或者只改文案不改包,那用deliver更合适。两个都装在一套 fastlane 里并不冲突。
5.2 一份能直接跑的 Fastfile
下面这份配置是我在多个项目里改改就用的版本,覆盖了从构建到上传的主干:
default_platform(:ios) platform :ios do desc "构建并上传到 TestFlight" lane :beta do app_store_connect_api_key( key_id: ENV["ASC_KEY_ID"], issuer_id: ENV["ASC_ISSUER_ID"], key_content: ENV["ASC_KEY_CONTENT"], in_house: false ) increment_build_number( build_number: ENV["CI_PIPELINE_IID"] || Time.now.strftime("%Y%m%d%H%M") ) build_app( workspace: "MyApp.xcworkspace", scheme: "MyApp", configuration: "Release", export_method: "app-store", output_directory: "./build", clean: true ) upload_to_testflight( skip_waiting_for_build_processing: true, changelog: "本次构建由流水线自动生成", groups: ["内部测试"] ) end end几个参数值得解释。app_store_connect_api_key负责把密钥信息组装成 fastlane 能识别的形式,key_content直接传.p8文件的文本内容,适合放在 CI 的环境变量里,避免把文件写进磁盘。increment_build_number用流水线序号做构建号,天然单调递增,不会撞车;本地没这个变量时退化到时间戳。
skip_waiting_for_build_processing: true是个关键选项。默认情况下 fastlane 会一直等着后台处理完成,大包可能要等十几二十分钟,CI 的 job 时长会被拖得很长甚至超时。设成 true 之后上传完就返回,处理结果通过邮件或者后续的状态查询获得。
groups指定上传后自动分发到哪个测试组,省掉手动去后台勾选的动作。如果测试组配置了外部测试,还会涉及审核流程,这部分就不是纯技术问题了。
5.3 证书和密钥在流水线里怎么安放
这是 CI 落地时最容易出事的环节。几条原则:.p8私钥、描述文件、证书私钥,一律不进代码仓库;用 CI 平台的加密变量或者密钥管理服务注入;不同环境(测试流水线、发布流水线)用不同的密钥,权限各自最小化。
描述文件的管理有两种思路。一种是手动维护,把.mobileprovision和.p12一起作为 base64 塞进环境变量,在流水线里解码后导入钥匙串;另一种是用match这类工具集中管理,证书和描述文件放在一个独立的私有仓库里加密存储,构建时按需拉取。前者适合单应用的小团队,后者在多个应用、多个团队共享证书的场景下优势明显——尤其是新机器加入时,一条命令就能把环境配齐。
钥匙串这块有个细节:CI 机器通常是无头环境,导入.p12之后需要显式设置钥匙串解锁和security set-key-partition-list,否则codesign会静默失败,报错信息还特别含糊。这一步在本地开发机上从来不用管,所以很多人第一次配 CI 时会在这卡很久。
5.4 流水线里最容易断的三个地方
第一个是构建号撞车。并行跑两个 job 时如果都用时间戳生成构建号,秒级并发下可能生成相同的值,第二个上传就被拒。解决办法是用流水线系统提供的全局自增序号,或者在生成时加一个随机后缀,但要注意构建号是字符串比较,加后缀会影响排序,稳妥做法还是用全局递增。
第二个是处理超时。包越大、包含的架构越多,后台处理时间越长。CI 里如果设置了整体的 job 超时,很容易在上传完成但等待处理时被强杀。用skip_waiting_for_build_processing能绕开,代价是失败反馈不及时。
第三个是并发上传冲突。同一个 App 记录下同时上传两个构建,服务端一般能接受,但如果两个包的构建号相同就会有一个失败。另外几个 job 共用一个 API 密钥时,偶尔会遇到限流,表现是返回 429 或者连接被重置,加重试逻辑就能缓解。
6. 上传失败之后怎么顺着线索找根因
6.1 先分清是传不上去还是传上去不合格
前面提过一次,这里展开讲判断方法。看报错出现的时机:如果进度条还没走完就中断,或者命令行在几十秒内就返回失败,属于传输层问题,排查方向是网络、认证、包体大小、本机时间。如果上传动作完整走完、返回成功,但几分钟后邮箱收到一封带ITMS-前缀的邮件,那是校验层问题,排查方向是包内容本身。
这两种情况的处理成本差别巨大。传输层问题往往重试一次或者换个网络就好了;校验层问题必须改包重新构建,一轮下来至少十几分钟。所以我在 CI 里会特意区分这两类失败,传输层失败自动重试三次,校验层失败直接终止并通知负责人,不做无意义的重复。
6.2 ITMS 系列报错的对照与定位
服务端返回的错误码有几百个,常用的那些其实翻来覆去就那几类。下面这张表是我这几年攒下来的高频项,代码对应的具体含义可能随平台版本调整,但成因方向基本不会变:
| 报错关键字 | 常见成因 | 处理方向 |
|---|---|---|
ITMS-90034/Invalid Signature | 签名与描述文件不匹配,或包在导出后被改动过 | 重新导出,确认证书与 Bundle ID 对应 |
ITMS-90076 | 证书过期或本机钥匙串里缺少对应私钥 | 重新下载证书,确认私钥在同一台机器上 |
ITMS-90713 | App 图标缺失或格式不合规,常见于 1024 图标带透明通道 | 检查 Asset Catalog,去掉 alpha 通道 |
ITMS-90535 | 包里某个 bundle 的 Info.plist 含有不该有的可执行文件声明 | 定位到具体 bundle,清理冲突字段 |
ITMS-90683 | Info.plist 缺少权限用途描述字符串 | 补上对应的 Usage Description |
ITMS-90809 | 使用了不再被接受的旧版 WebView 实现 | 升级相关依赖 |
The bundle version must be higher | 构建号未递增或小于历史最大值 | 修改 CFBundleVersion 后重新导出 |
Missing required icon file | 图标尺寸集不全 | 补齐 Asset Catalog 中对应尺寸 |
看这张表的时候要建立一个意识:错误码指向的是现象,不是根因。同一个码可能对应好几种不同的底层问题,尤其是签名类错误,可能是证书问题、可能是描述文件问题、也可能是导出参数问题。所以拿到码之后不要急着照搬解决方案,先想清楚这个码在说哪一层的事。
6.3 一次完整的排查链路复盘
说个印象比较深的案例。某团队做的一个工具类应用,本地 Archive 和导出都正常,Transporter 上传走完流程也返回成功,但每次都在处理阶段被拒,邮件里的错误码指向包内某个 bundle 的 Info.plist 存在问题。
第一步先确认是不是主包的问题。把 IPA 解压,看Payload/MyApp.app/目录结构,主包的 Info.plist 结构正常,没有任何多余字段。这就排除了主 target 配置错误的可能。
第二步往包里翻。Payload/MyApp.app/Frameworks/下有一堆动态库,还有几个.bundle资源包。逐个检查这些.bundle里的 Info.plist,发现其中一个第三方统计组件的资源包里带着CFBundleExecutable字段,值指向一个并不存在的可执行文件。服务端在校验时看到这个声明却找不到对应文件,就判定包结构异常。
第三步是处理。这个.bundle是构建时从静态库复制过来的,直接在 Xcode 里改没用,下一次构建还会被覆盖。正确做法是在 Build Phases 里加一个 Run Script,在复制完成之后删掉那个字段:
PLIST="${BUILT_PRODUCTS_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/SomeSDK.bundle/Info.plist" if [ -f "$PLIST" ]; then /usr/libexec/PlistBuddy -c "Delete :CFBundleExecutable" "$PLIST" 2>/dev/null || true fi脚本里的|| true不能省,因为当字段不存在时 PlistBuddy 会返回非零退出码,加上set -e的构建环境会因此失败。这个细节我踩过一次,花了不少时间才反应过来是脚本自己把构建搞挂了。
第四步是验证。重新归档、导出、上传,处理阶段通过。整个过程从发现到解决花了大半天,但真正定位问题只用了十几分钟,剩下时间都在重新构建和等待处理上。这也说明为什么值得在 CI 里把"包结构自检"做成一个独立步骤,提前跑,别等上传之后才发现。
6.4 上传成功却一直"正在处理"
这是另一种让人抓狂的情况:包传上去了,后台状态一直停在处理中,几个小时不动,也不给任何邮件。可能的原因有几种。
比较常见的是隐私清单缺失或者内容不合规。近几个版本对第三方 SDK 的隐私声明要求越来越细,某个依赖没有提供对应的清单文件,服务端在扫描时可能会卡住或者给出警告。这种情况下邮件里通常会有提示,翻一下垃圾邮件文件夹。
另一种是二进制里包含了不被支持的架构或者体积异常膨胀。比如某些调试符号没被剥掉,包体远大于预期,处理时间会显著拉长。用size或者otool看一眼各个 slice 的大小,心里有个数。
还有一种少见但确实存在的情况:包里带了不必要的能力声明(entitlements),比如申请了推送但实际没用到,服务端在验证配置时对不上。这类问题往往需要重新导出并清理 entitlements 文件才能解决。
遇到处理卡住,我的处理顺序是查邮件、查包体、查 entitlements、最后联系技术支持。前两步能覆盖八成情况,剩下的确实需要官方介入。
7. 几条路线怎么挑,说点取舍上的实话
把前面这些方式横向摆一下,差异其实挺清楚:
| 方式 | 是否需要 Xcode 工程 | 认证方式 | 自动化友好度 | 典型适用场景 |
|---|---|---|---|---|
| Organizer | 需要,且版本要匹配 | 账号登录态 | 低 | 个人开发、首次发布 |
| Transporter 图形界面 | 不需要 | 账号 + App 专用密码 | 低 | 接收外部交付包、补传历史包 |
| iTMSTransporter 命令行 | 需要完整 Xcode 环境 | API 密钥或账号密码 | 中 | 批量上传、脚本化 |
| altool | 需要命令行工具链 | API 密钥或专用密码 | 中高 | 存量 CI 脚本 |
| fastlane + API 密钥 | 需要 | API 密钥 | 高 | 团队协作、持续交付 |
| 直接调 REST 接口 | 不需要 | API 密钥 | 最高 | 平台化工具开发 |
选型上我的实际做法是这样的:个人项目或者小团队初期,直接走 Xcode 的 Organizer,省事,出错也容易看懂,没必要为了自动化而自动化。一旦有了固定的测试分发节奏,就把 fastlane 引进来,重点不在上传本身,而在于它把构建号管理、证书管理、分发组配置这些零碎事情一起解决了。对外交付或者需要接收别人打的包时,Transporter 是必备的兜底工具,装在机器上不占地方,关键时刻能救急。
至于altool和直接调接口,前者看存量代码需不需要维护,后者只有在你要做内部发布平台时才值得投入。我见过有团队为了"技术先进"自己实现了整套上传流程,结果每次平台接口调整都要跟着改,维护成本远超收益。
还有一条经验值得单独说:不要把所有鸡蛋放在一条路上。发布日当天如果 fastlane 卡住,手边得有一个能立刻用的备用方案——最典型的就是打开 Transporter 直接把导出好的 IPA 拖进去。所以无论自动化做得多完善,本地保留一份可手动操作的能力,始终是必要的。
我在实际操作中的体会是,上传这一步本身的技术难度并不高,真正消耗时间的是"出问题之后不知道往哪看"。把链路拆清楚、把每一层的判断标准记住,剩下的事情无非是对号入座。构建号、签名、包结构这三样,八成的问题都出在它们身上,把这三个点的检查固化成构建后的一步自检脚本,能省下的时间比你想象的多得多。