
简介Application Loader 是苹果开发者生态中用于向 App Store 上传 iOS、watchOS 与 tvOS 应用的独立工具适合在 Xcode 上传失败、大体积应用提交或上传前严格验证时作为替代方案使用也可帮助开发者理解 IPA 包与 App Store Connect 的提交流程。资源包共 1280 个文件约 99.26MB包含 strings 本地化文本、nib 界面资源、xml 与 plist 配置、jar 组件、dylib 动态库以及 png、tiff 图标图片等基本覆盖工具运行、界面展示与元数据配置所需结构。已有 867 人学习下载说明其在 iOS 发布环节具有实际参考价值。借助该工具可完成 IPA 选择、代码签名与元数据校验、上传进度查看等环节并围绕证书、Provisioning Profile、Info.plist、屏幕截图、应用大小限制以及网络中断等常见问题积累排查经验资源目录也便于对照了解工具组成与资源组织方式适合需要稳定提交应用、补全发布流程知识的 iOS 开发者参考使用。1. Application Loader 还留在 Xcode 里先搞清楚这条上传通道现在还能走多远手里有一个编译好的 ipa却卡在“怎么把它送进商店后台”这一步——Application Loader 就是很多老教程里点名的 iOS 上传工具选包、填账号、点投递剩下的分片传输、校验、回执交给它。它的定位很实在不想接第三方分发平台、手边只有一台 Mac、又需要偶尔手工发一次包的开发者用它图的就是省事。但这条通道这两年变化不小新版 Xcode 里它已经不在原位替代品换了两代。先摸清楚自己手上这个版本到底能不能用再决定继续点界面还是换成命令行否则最典型的翻车就是卡在“正在验证”的进度条上白等一晚上而日志里其实早就写清楚了原因。2. 拆开一个 ipa 看校验点上传前先自己当一次审核员上传工具干的事本质上是把包体解出来、按商店侧的规则逐条比对比对不过就退回。所以最快的排错方式不是投递之后等报错而是投递之前自己先按同样的顺序查一遍。这一章把 Application Loader 会读的东西拆成四块目录结构、Info.plist 关键键、架构与签名、以及一份可以直接跑的检查脚本。熟手可以只看 2.3 和 2.4新手建议按顺序走一遍整个过程不用联网。2.1 把 ipa 当 zip 拆Payload 里到底有什么ipa 的格式没有任何神秘之处它就是一个改了后缀的 zip里面只有一个Payload目录目录下是唯一的.app。上传工具第一步做的就是这个解包动作解不出Payload/*.app或者出现两个.app后面全都免谈。所以先不解压用unzip -l看一眼条目一分钟就能排除掉手工打包时把文件夹层级搞错这类低级错误。# 不解压先看条目结构重点确认 Payload 下只有一个 .app unzip -l build/MyApp.ipa | head -n 40 # 要逐个文件核对时再解到临时目录 WORK$(mktemp -d) unzip -q build/MyApp.ipa -d $WORK APP$(find $WORK/Payload -maxdepth 1 -name *.app | head -n 1) echo $APP ls -la $APP一个能过校验的包通常能在.app里看到这几样东西主二进制文件名和CFBundleExecutable一致、Info.plist、embedded.mobileprovision、_CodeSignature/CodeResources、Assets.car带扩展的还会有PlugIns/*.appex带动态库的会有Frameworks/*.framework。缺_CodeSignature说明压根没签Frameworks出现在.app之外的位置就是后面 ITMS-90206 那类报错的源头。用老版本 Xcode 出的包还会多一个顶层SwiftSupport/这是给商店侧重新打包用的自己手工拼包时经常会漏。提示ls看到的.app名字必须和CFBundleExecutable对得上。手工改名.app而不改Info.plist是投递阶段最隐蔽的一类失败。2.2 Info.plist 里几个必看的键上传工具读Info.plist读得很细但真正会拦住你的就那几个键。下表是我每次发版前必查的清单左列是键名中间是它在上传链路上的作用右列是踩过的坑。键上传链路里的作用常见错法CFBundleIdentifier决定投递到后台哪条应用记录和后台记录不一致报找不到应用CFBundleShortVersionString对外版本号三段式写成两段或带后缀校验直接拒CFBundleVersion构建号必须严格递增同一个号重复投递触发重复包MinimumOSVersion声明最低系统版本缺失时会按工具默认值处理功能对不上CFBundleIconName/CFBundleIcons图标资源索引图标名改了但资源没打进去UILaunchStoryboardName/UILaunchScreen启动图配置两个都没有启动即黑屏被退回NS*UsageDescription权限用途说明用了相机却没写描述静态扫描必中ITSAppUsesNonExemptEncryption出口合规标记不写就得每次人工回答一遍逐条查最快的方式是PlistBuddy比用 Xcode 点开再翻要快得多也方便塞进脚本里做断言。PLIST$APP/Info.plist /usr/libexec/PlistBuddy -c Print :CFBundleIdentifier $PLIST /usr/libexec/PlistBuddy -c Print :CFBundleShortVersionString $PLIST /usr/libexec/PlistBuddy -c Print :CFBundleVersion $PLIST # 权限描述是数组式的用正则扫一遍更快找出代码里用到但 plist 里没声明的 /usr/libexec/PlistBuddy -c Print $PLIST | grep -E ^ *NS[A-Za-z]UsageDescriptionPlistBuddy -c Print不带键名时会把整个 plist 打成文本配合 grep 就能一次性看清所有权限声明。这一步建议在 CI 里也保留因为权限缺失往往不是打包那一刻出现的而是某个依赖库升级后悄悄引入的——编译期不报错只有上传校验会拦。2.3 架构、签名、描述文件三条硬性检查这三项属于“错了一定过不去”的硬性条件。架构检查看的是包里有没有混进模拟器切片lipo -info输出里出现x86_64或i386那这个包投上去必然被打回真机包应该只有arm64老项目可能还有armv7。签名检查看的是codesign --verify能不能过深度校验要带上--deep否则嵌套的 framework 和扩展是查不出来的。# 1) 主二进制架构 lipo -info $APP/$(/usr/libexec/PlistBuddy -c Print :CFBundleExecutable $PLIST) # 2) 全包签名深度校验这一步会连 Frameworks 和 PlugIns 一起查 codesign --verify --deep --strict --verbose2 $APP # 3) 看签名实体和 Team 标识 codesign -dv --verbose4 $APP 21 | grep -E Authority|TeamIdentifier|Identifier # 4) 读描述文件确认和签名是同一套 security cms -D -i $APP/embedded.mobileprovision \ | plutil -convert xml1 -o - - \ | grep -A1 -E application-identifier|ExpirationDate|get-task-allowget-task-allow这个键值得单独说一句发布包的描述文件里它必须是false是true说明拿的是开发证书上传工具在你点投递的瞬间就会拦下来。ExpirationDate也要看描述文件过期是最容易被忽略的一种失败因为它不影响你本地安装调试只在投递时才炸。codesign --verify报code object is not signed at all八成是某个第三方 framework 在打包脚本里被重新拷贝过、丢了签名删掉Frameworks里的副本让它从Embed Frameworks阶段走一遍即可。2.4 把这套检查写成脚本零散敲命令容易漏项尤其是赶版本的时候。下面这个脚本把上面四块串起来输出[FAIL]就说明别投了先修包。#!/usr/bin/env bash # check_ipa.sh —— 投递前静态自检只读不写可安全重复执行 set -uo pipefail IPA${1:?用法: ./check_ipa.sh MyApp.ipa} WORK$(mktemp -d) trap rm -rf $WORK EXIT unzip -q $IPA -d $WORK || { echo [FAIL] 解包失败ipa 可能损坏; exit 1; } APP$(find $WORK/Payload -maxdepth 1 -name *.app 2/dev/null | head -n 1) [ -n $APP ] || { echo [FAIL] Payload 下没有 .app; exit 1; } PLIST$APP/Info.plist BIN$APP/$(/usr/libexec/PlistBuddy -c Print :CFBundleExecutable $PLIST 2/dev/null) echo 标识 /usr/libexec/PlistBuddy -c Print :CFBundleIdentifier $PLIST || echo [FAIL] 缺 CFBundleIdentifier echo 版本: $(/usr/libexec/PlistBuddy -c Print :CFBundleShortVersionString $PLIST) ($(/usr/libexec/PlistBuddy -c Print :CFBundleVersion $PLIST)) echo 架构 lipo -info $BIN | tee /tmp/_arch.txt grep -qE x86_64|i386 /tmp/_arch.txt echo [FAIL] 含模拟器切片需重新导出 echo 签名 codesign --verify --deep --strict --verbose2 $APP 21 | tail -n 3 \ || echo [FAIL] 签名校验未通过 echo 描述文件 security cms -D -i $APP/embedded.mobileprovision 2/dev/null \ | plutil -convert xml1 -o - - 2/dev/null \ | grep -q keyget-task-allow/key \ security cms -D -i $APP/embedded.mobileprovision 2/dev/null \ | plutil -convert xml1 -o - - 2/dev/null \ | grep -A1 keyget-task-allow/key | grep -q true/ \ echo [FAIL] 这是开发描述文件不能用于发布 echo 完成 脚本里几个参数值得说明。set -uo pipefail只开未定义变量和管道错误不开-e因为自检要跑完全部检查项、把问题一次列全中途退出反而低效。trap ... EXIT保证临时目录一定被清理避免反复运行把磁盘塞满。架构检查那段用grep -qE判断命中即失败比人工读lipo输出可靠。描述文件那段连着 grep 两次看着啰嗦是因为security cms -D的输出要先转成 xml 才能稳定匹配中间的2/dev/null是为了吞掉证书链警告这类噪音——这些噪音不影响判断但会让人误以为出了问题。3. 从点界面到敲命令altool 与 Transporter 的装、配、跑图形界面的好处是所见即所得坏处是没法进流水线、出错只有一行红字。真正要长期发版最后还是得落到命令行。这一章讲清楚三件事现在有哪几条通道可选、凭据怎么配才不折腾、以及一次完整的“先校验再投递”该按什么顺序敲。3.1 三条通道的现状与选型Application Loader 是随 Xcode 一起装的老工具路径基本固定在Xcode.app/Contents/Applications/Application Loader.app独立安装的话在/Applications下。新版 Xcode 已经不再自带它所以“找不到这个 App”本身就是最常见的问题——不是你没装是装的版本里没有了。这种情况下别折腾降级 Xcode直接换下面两条路。通道形态适合场景注意事项Application Loader图形界面偶尔手工发一次、机器上还留着老 Xcode账号密码登录凭据不好自动化altool命令行随 Xcode 命令行工具提供脚本封装、CI 里跑已标记弃用但短期仍可用Transporter独立图形 App 内置命令行大包、需要断点续传、长期方案需单独安装命令行参数与 altool 不同选型逻辑不复杂只要能进脚本就别用界面。altool 用起来最轻一条命令搞定校验和投递Transporter 的命令行更适合包体大、网络不稳的场景因为它对分片和重试的处理更完整。我一般的做法是本地调试用 altool正式流水线挂 Transporter 的命令行两边日志格式不一样但错误码是同一套。确认工具在不在两条命令就够# altool 随命令行工具提供能打印帮助就说明可用 xcrun altool --help 21 | head -n 5 # Transporter 的命令行在 App 包内路径固定 ls /Applications/Transporter.app/Contents/itms/bin/iTMSTransporter3.2 两套凭据专用密码和 API Key命令行上传绕不开认证现在只有两条路。第一条是账号加专用密码在账号安全设置里生成一串十六位的口令专门给第三方工具用泄露了可以单独吊销不影响主密码。它的好处是配置简单坏处是绑定到具体人人一走或者改了密码就全线失效。第二条是 API Key由 Key ID、Issuer ID 和一个.p8私钥文件三部分组成。私钥只能下载一次丢了只能重新生成。这是流水线里应该用的方式因为它不绑定个人账号权限也可以单独收窄。落地时有个约定俗成的目录工具会自动去那儿找私钥省得每次命令行都传路径。mkdir -p ~/.appstoreconnect/private_keys cp AuthKey_XXXXXXXXXX.p8 ~/.appstoreconnect/private_keys/ chmod 600 ~/.appstoreconnect/private_keys/AuthKey_XXXXXXXXXX.p8 # 文件名里的 XXXXXXXXXX 必须和 Key ID 完全一致工具按这个名字索引 export KEY_IDXXXXXXXXXX export ISSUER_IDxxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxxchmod 600不是可选项。私钥文件权限放开工具会拒绝读取报的还是“认证失败”这种看不出根因的错排查起来很浪费时间。另外AuthKey_前缀和.p8后缀都不能改中间那段必须逐字符等于 Key ID差一个字符就是找不到密钥。3.3 先 validate 再 upload这是整个流程里最值得养成的习惯投递前先跑一次校验。校验阶段会把包结构、签名、Info.plist这些问题全过一遍但不占用版本号、不产生构建记录改完可以反复跑。直接上传则不同一旦某个包被接收那个构建号就锁死了想重发只能改号。# 第一步校验。这一步不改任何状态可以放心反复跑 xcrun altool --validate-app \ -f build/MyApp.ipa \ -t ios \ --apiKey $KEY_ID \ --apiIssuer $ISSUER_ID \ --output-format json 21 | tee build/validate.log # 第二步投递。校验干净了再执行 xcrun altool --upload-app \ -f build/MyApp.ipa \ -t ios \ --apiKey $KEY_ID \ --apiIssuer $ISSUER_ID \ --output-format json 21 | tee build/upload.log参数逐个说清楚。-f指向 ipa 的绝对或相对路径路径里有空格要加引号-t ios指定平台类型这个参数漏了工具会猜猜错报的错很奇怪--apiKey和--apiIssuer是 API Key 方式专用的两个参数用专用密码的话换成-u 账号 -p 专用密码--output-format json让结果可被程序解析比默认的富文本友好得多配合tee顺手落盘存档出问题时能回看当次到底返回了什么。Transporter 的命令行参数长得不一样但语义对应/Applications/Transporter.app/Contents/itms/bin/iTMSTransporter \ -m upload \ -assetFile build/MyApp.ipa \ -apiKey $KEY_ID \ -apiIssuer $ISSUER_ID \ -v informational 21 | tee build/itms-upload.log-m upload是模式同系列还有-m verify只做校验-v informational控制日志详细程度默认级别会把每个分片都打出来日志能到几百兆日常用informational就够真要查传输问题再往上调。3.4 投递之后去哪看命令返回成功只代表文件完整送达不代表包能上架。包会先进“处理中”状态工具侧会做一轮更深的静态扫描这一步通常几分钟大包会更久。处理完成后才会出现在后台的构建版本列表里此时才轮到选测试、填审核信息。所以脚本里判断成功不能只看退出码还得在下游确认构建版本真的出现了否则会出现“流水线绿灯、人工发现没包”的尴尬。4. 参数与日志把 ITMS 错误码翻译成可执行动作上传失败的体验很差因为界面只给一行错误码加一句英文。但错误码本身是有规律的ITMS-开头的几乎都是包体静态校验-1003、-1011这类负数基本是认证或网络纯文本的“找不到应用记录”则是对后台配置。把这三类分开排查路径立刻清晰。4.1 常用参数与它们的实际影响参数作用踩坑点-f指定待上传文件相对路径在 CI 里容易踩空建议写绝对路径-t平台类型漏写会走默认推断报错信息有误导性--apiKeyAPI Key 的 Key ID与.p8文件名必须完全一致--apiIssuer发行方标识复制时容易带空格或换行--output-format json结构化输出解析时注意字段是字符串还是数字-v(Transporter)日志级别调太高会撑爆磁盘--validate-app只校验不投递反复跑不消耗构建号应作为默认动作4.2 日志在哪按什么顺序读altool 的日志默认落在~/Library/Logs/altool下按时间戳分文件Transporter 的日志在~/Library/Logs/Transporter或命令行当前目录取决于是否指定了-logFile具体文件名以工具输出为准。读日志的顺序建议固定成三步先搜ITMS-把所有错误码抓出来再看最后一个成功的阶段是解包、签名校验还是传输完成最后才看传输层的重试记录。# 抓本次所有 ITMS 错误码去重后按出现次数排序 grep -oE ITMS-[0-9]{4,5} build/validate.log | sort | uniq -c | sort -rn # 看最后的阶段判断是死在本地校验还是死在传输 tail -n 30 build/upload.log先抓码再读全文是因为一个包经常同时触发好几个错误。工具只把第一条抛给你修完再跑又冒出一条来回折腾三四轮很正常。一次性把所有码都列出来按严重程度排个序一次改完效率差好几倍。4.3 错误码到根因的映射错误码大致含义处置方向ITMS-4238重复的构建该版本号已存在递增构建号后重投ITMS-90168二进制无效查模拟器切片、查签名完整性ITMS-90087不支持的架构剔掉 framework 里的 x86_64/i386 切片ITMS-90206包内含不合规的 Frameworks 目录检查嵌入阶段是否把 framework 放错层ITMS-90535意外出现的 CFBundleExecutable 键三方资源 bundle 的 plist 里删掉该键ITMS-90683缺少权限用途说明按提示补对应的NS*UsageDescriptionITMS-90685标识符冲突多个扩展或 framework 用了同一个 bundle idITMS-90032图标路径无效核对CFBundleIcons与实际资源这张表覆盖了绝大多数高频情况但错误码会随规则调整而变化遇到表里没有的正确做法是拿码去搜官方说明而不是凭感觉改包。历史经验是改包体的问题九成集中在签名和资源两处改配置的问题九成在Info.plist剩下的一成才是网络。4.4 上传卡住不动时的排查顺序传输阶段没有进度输出很常见尤其是大包。别急着 Ctrl-C先按这个顺序看网络出口有没有限速或拦截大文件上传临时目录所在磁盘是不是满了工具会在本地做缓存空间不够会静默卡住凭据是不是过期专用密码改了密码就失效API Key 被吊销同理。这三条排完还在转就换 Transporter 命令行重投它对中断后的续传处理更完整。5. 避坑清单五条被 ITMS 打回来的真实排查记录下面这五条是反复出现、且每次都要重新查一遍的类型。每条按“现象、原因、解决”写遇到同类报错可以直接对号入座。5.1 签名与描述文件现象codesign --verify在本地能过投递后被退回错误信息指向签名不匹配。原因Frameworks目录里某个 framework 是被打包脚本从别处直接cp -R进来的签名是旧的或者根本是另一套证书签的。解决把这些 framework 从Frameworks里剔掉改由构建阶段统一嵌入并重签脚本里加一步codesign --verify --deep --strict卡住这个口子。现象投递瞬间被拒提示不能用于发布。原因embedded.mobileprovision里的get-task-allow是true用的是开发描述文件。解决导出时选发布描述文件别在 Xcode 里手动替换 plist。这类错误本地安装调试完全正常只在投递时现形属于典型的“骗过自己”案例。5.2 包体与资源现象报 ITMS-90535提示某个 bundle 里有不合规的键。原因三方 SDK 附带的资源.bundle里Info.plist带了CFBundleExecutable键但那个 bundle 里并没有同名可执行文件。解决在打包后处理阶段用PlistBuddy -c Delete :CFBundleExecutable删掉这个键或者让依赖方升级版本。现象报 ITMS-90683提示缺少用途说明。原因代码里调了相册、相机、蓝牙之类的能力Info.plist里没写对应的NS*UsageDescription。解决按错误提示的键名补齐描述文字要说明用途不能写空串或“需要此权限”这种废话审核阶段还会再看一遍。现象报 ITMS-90087提示存在不支持的架构。原因某个以二进制形式引入的动态库裁剪不干净带上了模拟器切片或者在真机包上跑过一次模拟器构建产物。解决用lipo -remove剔除多余切片或者在构建脚本里加一步自动瘦身把它变成固定动作而不是临时补救。5.3 认证与传输现象认证失败但账号密码确认无误。原因.p8文件的权限不是600或者文件名里的 Key ID 与--apiKey传的值不一致也可能是--apiIssuer复制时带进了换行。解决chmod 600之后重新核对 Key ID 逐字符一致性环境变量用printf %s打印出来看有没有隐藏字符。现象投递成功但后台一直没有构建版本。原因包还在处理中或者处理阶段发现了更深层的问题而被静默丢弃通知邮件发到了账号对应邮箱。解决先去构建版本列表刷新确认状态超过半小时还没出现翻邮箱找处理失败通知那里面的错误信息比上传日志更具体。6. 塞进流水线一个可复用的上传脚本与验收动作单次手工发版可以把上面几步串着敲长期发版就得固化成脚本。我给几个项目用的骨架长这样把校验、投递、结果判断分三段中间任何一段失败都保留现场。#!/usr/bin/env bash # upload.sh —— 校验 投递 结果判定失败保留日志供复盘 set -euo pipefail : ${KEY_ID:?请设置 KEY_ID} : ${ISSUER_ID:?请设置 ISSUER_ID} IPA${1:?用法: ./upload.sh build/MyApp.ipa} LOG_DIRbuild/upload-logs; mkdir -p $LOG_DIR STAMP$(date %Y%m%d-%H%M%S) VALIDATE_LOG$LOG_DIR/validate-$STAMP.json UPLOAD_LOG$LOG_DIR/upload-$STAMP.json # 段一静态自检逐项打印不影响后续流程但会提示 ./check_ipa.sh $IPA || echo [WARN] 自检有告警继续走校园验 # 段二服务端校验不消耗构建号 xcrun altool --validate-app -f $IPA -t ios \ --apiKey $KEY_ID --apiIssuer $ISSUER_ID \ --output-format json 21 | tee $VALIDATE_LOG grep -qE status[[:space:]]*:[[:space:]]*0 $VALIDATE_LOG \ || { echo [FAIL] 校验未通过见 $VALIDATE_LOG; exit 2; } # 段三投递 xcrun altool --upload-app -f $IPA -t ios \ --apiKey $KEY_ID --apiIssuer $ISSUER_ID \ --output-format json 21 | tee $UPLOAD_LOG echo [OK] 投递完成日志$UPLOAD_LOG三段的分工要说清楚。段一只做本地静态检查发现问题给告警但不拦因为有些告警是已知的、有意为之的硬拦会让流水线三天两头红。段二才是真正的闸门status不为 0 直接退退出码用 2 和段一区分开CI 里一看退出码就知道是哪一类失败。段三不再判断结果把原始返回落盘就结束因为投递的真实结果要等后台处理脚本这一刻判断不了硬判反而会误报。真正需要人工确认的验收动作有三个建议写成发布检查单构建号比上一次大且没有在版本列表里出现过后台的构建版本列表里出现了这个号状态从处理中变成可提交包里带的符号文件与二进制是一一对应的崩溃归因时能还原出符号名。第三个最容易被跳过等到线上出问题才发现符号对不上那时候再去补已经来不及了。还有个我吃过亏的细节grep判断 JSON 结果时不同版本的输出字段类型不一致有时是数字有时是字符串。所以上面用的是宽松正则而不是精确匹配字符串。从那以后我每次改上传脚本都会先拿一个故意做坏的包跑一遍失败路径确认它在该失败的时候真的失败了而不是一路绿灯跑到最后。希望帮到你。本文还有配套的精品资源点击获取