1. 先想清楚:为什么要绕开 App Store 做 iOS 分发
做 uni-app 的朋友大概都遇到过这个场景:项目本身是给内部员工用的巡检工具,或者给一小批种子用户试用的 MVP,功能已经跑通了,用 HBuilderX 打完包拿到了 ipa,结果卡在最后一步——上架审核要时间、要资质、要各种合规材料,而业务方明天就想让用户装到手机上。这时候"不上架 App Store、直接把 ipa 给用户下载"就成了一条现实路径。
但这条路不是把 ipa 丢到网盘里发个链接就完事的。iOS 和 Android 在这一步的差别,相当于"随手把 APK 发给别人安装"和"你要先向苹果证明这台设备有资格装这个包"的差别。iOS 的每一份 ipa 都绑定了签名证书和描述文件,描述文件决定了这个包能装在哪些设备上、能装多久、需不需要用户手动信任。所以真正要解决的问题有三个:用哪种账号和证书签、怎么把包打出来、怎么让用户用一个链接就装上。这三个问题任何一个没搞清楚,用户看到的都是"无法安装此 App"。
这篇记录面向的是已经能用 uni-app 跑通业务、但对 iOS 签名生态不太熟的开发者。不管你是第一次接触 iOS 分发,还是之前用第三方签名平台掉过签想换成自建方案,下面这套流程都可以直接照着走。我会把每一步"为什么这么做"讲透,也会把那些官方文档里不会写、但实际会卡住你半天的地方标出来。
1.1 三条主流分发路径的真实差别
很多人一上来就问"企业证书怎么搞",其实先该问的是"我的用户是谁"。路径选错的代价可能是几千块钱和几周的返工。
Ad Hoc 分发是门槛最低的一种。它用你个人或公司开发者账号(99 美元/年)里的 Ad Hoc 类型描述文件,把指定设备的 UDID 写进描述文件里,只有这些设备能装。优点是不需要苹果审核、当天就能发;缺点是设备数量有硬上限,每个会员年每种设备类型最多 100 台,而且换手机、加新人就要重新生成描述文件、重新打包。适合内部几十人规模的小团队。
企业账号(In-House)分发是大家最想要的方案:299 美元/年,不需要收集 UDID,描述文件里没有设备限制,理论上公司内部任意设备都能装。代价是申请门槛高——需要公司实体、D-U-N-S 编码,苹果会核查你的企业资质和使用场景,而且明确规定只能给公司内部人员使用。一旦被判定为对外公开分发,证书会被直接吊销,所有已安装的设备会立刻打不开应用。这点必须有心理准备,别把企业证书当成万能分发通道。
TestFlight是苹果官方认可的测试分发方式,完全免费,通过 App Store Connect 上传构建版本,外部测试员上限 10000 人,每个构建版本有效期 90 天。它需要过一次 TestFlight 审核(比正式上架宽松很多,通常一两天),而且用户需要在手机上装 TestFlight 这个 App。如果你的场景是"给一批真实用户试用但不想正式上架",TestFlight 其实是合规性最好的选择,只是流程上多了一个外部 App。
1.2 成本和时间账,先算清楚再动手
| 维度 | Ad Hoc | 企业账号 In-House | TestFlight |
|---|---|---|---|
| 年费 | 99 美元 | 299 美元 | 0(需 99 美元开发者账号) |
| 申请难度 | 低,注册即可 | 高,需企业资质审核 | 低 |
| 设备限制 | 每类 100 台,需 UDID | 无 | 10000 名测试员 |
| 有效期 | 描述文件 1 年 | 描述文件 1 年 | 构建 90 天 |
| 苹果审核 | 无 | 无 | 有,但较宽松 |
| 用户操作 | 需信任描述文件 | 需信任企业开发者 | 装 TestFlight 后点安装 |
| 合规风险 | 低 | 中高,严禁外部分发 | 低 |
还有一个经常被问的问题:uni-app 打包 iOS 收费吗。这里要拆成两笔账。第一笔是苹果那边的,开发者账号的年费跑不掉,个人/公司 99 美元,企业 299 美元。第二笔是 HBuilderX 的云打包服务,免费额度有限,超过之后需要购买打包次数;如果你选择本地离线打包(用 Xcode 自己编译),就不产生这笔费用,代价是环境搭建更麻烦。我的建议是:项目初期用云打包快速验证,等发版频率上来了再考虑离线打包。
1.3 我的一般选择顺序
如果是 20 人以内的内部工具,Ad Hoc 就够了,最省心,出问题也好排查。如果是几百人的公司全员使用,且有正规企业资质,走 In-House,但一定要在内部明确"不对外分享安装链接"。如果是要面向真实用户做灰度,别硬上企业证书,老老实实走 TestFlight,能省掉后面一大堆掉签的麻烦。至于第三方签名平台那条路,我个人不推荐——这类平台本质上是拿别人的证书给你的包重签名,证书什么时候被吊销你完全不知道,用户昨天还能用今天打开就闪退,售后成本极高,而且合规上存在明显风险。顺带说一句,像"微信多开自签包"这类应用本身就处在灰色地带,技术上怎么实现是另一回事,但作为开发者要清楚自己在承担什么。
2. 证书与描述文件:打包前最容易翻车的一步
我见过太多人卡在这一步:HBuilderX 里填完 Bundle ID 和证书,一点打包,报"证书与描述文件不匹配",然后开始在网上乱搜。问题的根源通常是没搞清楚 iOS 签名体系里那几个文件分别是什么、谁和谁必须对应。
一句话概括这套体系:证书(Certificate)证明"你是谁",描述文件(Provisioning Profile)规定"你能把 App 装到哪",两者通过 App ID 绑定在一起,任何一环对不上,包就装不进去。
2.1 三种证书分别用来干什么
打开苹果开发者后台的 Certificates 页面,你会看到几种类型,别选错。
Apple Development是开发证书,配合开发描述文件,用来在 Xcode 里真机调试。它的描述文件里必须包含测试设备的 UDID。Apple Distribution是发布证书,用于 App Store 提交和 Ad Hoc 分发,这是你要发 ipa 给别人装的时候用的。Apple Push Services是推送证书,只有你用推送功能时才需要单独生成,注意它和发布证书是两回事,别混。
还有一类是企业账号特有的In-House 描述文件,在 Profiles 页面创建时,Distribution Method 选 "In House",不需要勾选任何设备。
这里有个细节值得单独说:很多人手里的 .p12 是从别人那儿拷来的,或者从某台旧电脑导出的,密码早就忘了。建议每个项目单独生成一套证书,用 1Password 之类的工具把 .p12 和密码一起存起来,导出的 .p12 一定要设密码,因为 HBuilderX 打包时会让你填这个密码。证书丢了不用慌,去后台 Revoke 掉重新生成就行,但要注意 Revoke 会影响所有用这张证书签的 App,如果线上还有在用的包,尽量在低峰期操作。
2.2 在开发者后台把 p12 和 mobileprovision 拿到手
完整流程大概是这样,我第一次做的时候前后花了两个小时,第二次十分钟就搞定了。
第一步,创建 App ID。在 Identifiers 页面新建一个,Bundle ID 建议用反向域名,比如com.yourcompany.inspection。如果项目里用到了推送、iCloud、Associated Domains 这些能力,记得在这里勾上对应的 Capabilities。Bundle ID 一旦确定就别再改,改了等于换了一个新 App,用户装上去会是两个图标。
第二步,生成证书。在 Certificates 页面点加号,选 Apple Distribution,然后按提示在本地生成 CSR 文件(Mac 上用钥匙串访问的"证书助理"就能生成)。上传 CSR 后下载 .cer 文件,双击导入钥匙串,在钥匙串里找到这张证书,右键导出为 .p12,设一个密码。这个 .p12 就是 HBuilderX 要的那个"私钥证书"。
第三步,生成描述文件。在 Profiles 页面新建,选 Ad Hoc 或 In House,绑定刚才的 App ID 和证书,Ad Hoc 还要勾选设备。下载下来的 .mobileprovision 文件就是描述文件。
这里有个非常容易踩的坑:描述文件的命名是 "xxx.mobileprovision",但它在描述文件列表里显示的名字(Name 字段)和文件名不是一回事。HBuilderX 上传时读的是文件本身,所以不要手动改文件后缀,也别用文本编辑器打开另存,那样会把二进制结构破坏掉。
2.3 描述文件里那 100 台 UDID 怎么加
Ad Hoc 最烦的就是这一步。用户需要把设备 UDID 给你,获取方式有好几种:在 Mac 上连数据线用 Finder 看、用 iTunes 看序列号那栏点一下切换成 UDID、或者让用户自己装一个描述文件生成工具。iOS 16 之后苹果还在设置里加了一个开发者模式开关,真机调试的时候要单独打开,这个后面讲排查问题时会再提到。
拿到 UDID 后,在开发者后台的 Devices 页面添加(注意区分 iPhone、iPad 等设备类型,每类独立计数)。加完设备必须回到描述文件里编辑,勾上新增的设备,然后重新下载描述文件,否则新设备还是装不上。这个"改完设备忘更新描述文件"的错误,我至少犯过三次。
还有个隐藏限制:设备从账号里移除之后,同一个 UDID 在本会员年度内不能再次添加。所以清理设备列表的时候要谨慎,别手快把还在用的设备删了。
3. uni-app 工程侧要改的东西
证书准备好了,接下来是工程本身。uni-app 的特点是大部分配置集中在 manifest.json 里,打包工具会把它翻译成 iOS 工程里的 Info.plist 和 entitlements。这个"自动翻译"很方便,但也意味着你填错的地方不会报错,而是会在用户手机上以闪退的形式暴露出来。
3.1 manifest.json 里与 iOS 强相关的几个节点
在 HBuilderX 里打开 manifest.json,切到"App 图标配置"和"App 模块配置"之外,重点是"App 常用其他设置"和源码视图。用源码视图看得更清楚,结构大致是app-plus→distribute→ios。
几个关键字段:
appid:这里的 appid 是 DCloud 的 AppID,和苹果的 Bundle ID 不是一回事,别搞混。- Bundle ID:在打包时单独填写,必须和描述文件里的 App ID 一模一样,大小写敏感。
deviceType:控制支持 iPhone 还是 iPad。如果只支持 iPhone,某些系统检查会认为 iPad 上是以兼容模式运行的,需要注意。privacyDescription:隐私权限描述,下面单独讲。UIStatusBarStyle:状态栏样式,浅色背景配深色文字这类问题。deploymentTarget:最低支持的 iOS 版本,改高一点可以少踩老系统的坑,但用户机型太旧就装不了。
还有个容易被忽略的点:manifest.json 里的版本号(version.name和version.code)就是 ipa 里的 CFBundleShortVersionString 和 CFBundleVersion。version.code必须是递增的整数,如果做 wgt 热更新或者以后转上架,版本号回退会带来一堆麻烦,建议每次发版老老实实加一。
3.2 权限描述写不好,用户打开就闪退
iOS 的隐私机制非常严格:如果代码里调用了相册、相机、蓝牙、定位这类能力,但 Info.plist 里没有对应的用途描述字符串,系统会直接杀掉进程,表现就是用户点开某个页面瞬间闪退,而且不会有任何弹窗提示。这是 uni-app 项目上 iOS 之后最高频的闪退原因之一。
在 HBuilderX 的 App 模块配置里,如果你勾了蓝牙、相册、摄像头等模块,打包界面会自动列出对应的隐私描述输入框。填的时候注意三点:
第一,描述要写清楚具体用途,不能写"为了更好的体验"这种空话。用户看到的是这句话,写得不清楚容易被投诉,审核的时候也会被挑。
第二,蓝牙相关的描述键名要用最新的。老版本用的是NSBluetoothPeripheralUsageDescription,新系统要求用NSBluetoothAlwaysUsageDescription,两个都写上最保险。这跟热词里"uni-app ble ios 可以根据蓝牙 deviceid 建立连接吗"这个问题的排查是同一类——先确认权限描述到位,再谈连接逻辑。
第三,如果用了 IDFA(广告标识符),要额外填NSUserTrackingUsageDescription。纯内部工具别开 IDFA,开了反而会触发 App Tracking Transparency 的弹窗,影响体验。
3.3 图标、启动图、版本号:三个最容易被忽略的坑
图标这块,uni-app 支持一键生成全尺寸图标,但要注意iOS 的图标不能有透明通道。PNG 带 alpha 通道的话,打出来的包在部分系统版本上图标会显示成黑底或者直接白块。上传前用图片工具把透明背景填成实色,能省掉一次返工。
启动图(Launch Screen)在 iOS 上有硬性要求,如果缺失,App 在某些机型上会以非全屏模式运行,看起来像"黑边"。uni-app 的启动图配置里可以选择"自动生成"或"自定义",小项目用自动生成就行,但生成的图会带默认底色,视觉上要能接受。
版本号这件事我在上面提过,这里补充一个实际场景:做内部迭代的时候,很多人懒,每次打包都不改版本号。结果用户装了新版,系统认为是同一个版本,覆盖安装之后新旧代码混在一起,出现一些莫名其妙的 bug。养成习惯:每次打包前改 version.code,哪怕只是内部测试。
4. 打包实操:HBuilderX 云打包与 Xcode 离线打包
配置改完就可以打包了。uni-app 提供两条路:云端打包和本地离线打包。前者快、省事,后者灵活、可控。
4.1 云打包完整流程记录
在 HBuilderX 里点"发行" → "原生 App-云打包",弹窗里选 iOS,然后按顺序填:
- Bundle ID:从开发者后台复制的 App ID,逐字符核对。
- 私钥证书:选之前导出的 .p12 文件。
- 私钥密码:导出 .p12 时设的那个密码,不是 Apple ID 密码。
- 描述文件:选 .mobileprovision 文件。
如果勾选了"支持 iPad",记得确认描述文件里是否包含 iPad 设备(Ad Hoc 场景下)。
点击打包后会进入排队,等待时间取决于当前队列长度。打包成功后 HBuilderX 会提示 ipa 的下载地址,同时邮箱里也会收到一份链接。
几个实测下来的要点:一是打包日志一定要看,很多人只看"打包成功"四个字就关了,其实日志里可能有权限描述缺失、模块冲突的警告。二是如果报"证书和描述文件不匹配",先检查 Bundle ID 是否完全一致,再检查描述文件里的证书是不是你上传的那张。三是一次打包可以生成多个 ipa(不同渠道包),别拿到第一个就以为完事了。
4.2 离线打包这条路,什么时候值得走
本地离线打包需要下载 uni-app 的 iOS 离线 SDK,用 Xcode 打开官方提供的工程模板,把HBuilder-Hello里的 www 目录替换成你的项目编译产物,然后在 Xcode 里配置签名和证书。
这条路的价值在于:你能完全控制 Info.plist、能自己加原生插件、能调试原生层的崩溃、不受云打包次数限制。代价是环境搭建比较折腾——需要 macOS、对应版本的 Xcode、CocoaPods 要能正常拉依赖,首次配置花上半天很正常。
我的经验是,如果项目里有原生插件(比如自定义的蓝牙通信、离线地图),云打包一旦出错你几乎无从下手,这时候就必须上离线打包。反过来,纯 H5 逻辑 + 官方模块的项目,云打包完全够用。
4.3 打包完先验签,别急着发给用户
这一步很多人跳过,结果是把有问题的包发出去,用户反馈"装不上",你再去一步步排查,浪费的是双方时间。养成习惯,ipa 拿到手先在本机验一遍。
# 解压 ipa,看内部结构 unzip -q app.ipa -d check ls check/Payload/ # 查看签名信息:证书主体、Team ID、签名时间 codesign -dv --verbose=4 check/Payload/YourApp.app # 查看内嵌的描述文件内容 security cms -D -i check/Payload/YourApp.app/embedded.mobileprovision第一条命令能看到 Payload 里 App 的实际名称。第二条命令的输出里重点看Authority(签名用的证书名)和TeamIdentifier。第三条命令会输出一大段 plist,重点看三个字段:
ExpirationDate:描述文件过期时间,超过这个日期包就打不开了。ProvisionedDevices:Ad Hoc 场景下必须能找到目标设备的 UDID。Entitlements→application-identifier:格式是TeamID.BundleID,核对一下和你的 Bundle ID 是否一致。
如果这三项都对得上,包基本没问题。这是我最推荐的一个习惯:每次发版前跑一遍这个命令,三十秒的事,能挡掉八成以上的"装不上"工单。
5. 自建 OTA 下载页,让用户点一下就装上
包有了,怎么给用户?Android 那句"下载这个 APK 点安装"在 iOS 上不成立。iOS 装非商店 App 走的是 OTA(Over-The-Air)方式,核心是那个很多人见过但没研究过的itms-services协议。
5.1 itms-services 到底是怎么工作的
一句话说清楚:当你在 Safari 里打开一个itms-services://开头的链接时,系统会去下载链接参数里指定的那个 plist 文件,从 plist 里读出 ipa 的下载地址、App 名称、图标地址,然后弹窗问你要不要安装。你点确认,系统就去下载 ipa 并安装到桌面上。
所以整个分发链条是三个东西:下载页 HTML(用户点的地方)+ manifest.plist(告诉系统去哪下)+ ipa 文件(真正的包)。三者缺一不可,且都要放在 HTTPS 环境下。
有两个硬性约束必须记住。第一,这个链接必须在 Safari 或者系统浏览器里打开,微信、QQ、钉钉的内置浏览器一律不支持,只会报错或者没反应。所以给用户发链接的时候要提醒一句"用 Safari 打开",或者干脆在页面里加个引导。第二,必须是 HTTPS,而且证书得是受信任的 CA 签发的,自签证书 iOS 不认。
5.2 manifest.plist 的完整写法
这个文件是个 XML 格式的 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>items</key> <array> <dict> <key>assets</key> <array> <dict> <key>kind</key> <string>software-package</string> <key>url</key> <string>https://dl.yourdomain.com/app/app-1.0.0.ipa</string> </dict> <dict> <key>kind</key> <string>display-image</string> <key>needs-shine</key> <false/> <key>url</key> <string>https://dl.yourdomain.com/app/icon-57.png</string> </dict> <dict> <key>kind</key> <string>full-size-image</string> <key>needs-shine</key> <false/> <key>url</key> <string>https://dl.yourdomain.com/app/icon-512.png</string> </dict> </array> <key>metadata</key> <dict> <key>bundle-identifier</key> <string>com.yourcompany.inspection</string> <key>bundle-version</key> <string>1.0.0</string> <key>kind</key> <string>software</string> <key>title</key> <string>巡检助手</string> </dict> </dict> </array> </dict> </plist>几个要点。software-package里的 url 就是 ipa 的完整地址。bundle-identifier必须和 ipa 里实际的 Bundle ID 完全一致,不一致的表现是安装时弹窗一闪而过、什么都没发生。bundle-version是显示给用户看的版本号,写错不会导致安装失败,但会显示成旧版本号,容易被误判。两个图标的地址如果 404,App 装完桌面图标会是白块——不影响使用,但很难看,建议还是配上。
5.3 下载页和服务器怎么配
下载页本身很简单:
<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>巡检助手 - 安装</title> </head> <body> <h1>巡检助手 v1.0.0</h1> <p>请在 Safari 浏览器中打开本页面后点击下方按钮安装。</p> <a href="itms-services://?action=download-manifest&url=https%3A%2F%2Fdl.yourdomain.com%2Fapp%2Fmanifest.plist"> 点击安装 </a> </body> </html>注意url=后面的地址做了 URL 编码(冒号和斜杠被转义),这一步不做的话,某些情况下 iOS 解析参数会出问题,尤其是地址里带查询字符串的时候。这个坑我第一次踩的时候排查了整整一个下午。
服务器侧要注意 MIME 类型。.ipa文件的Content-Type建议设成application/octet-stream,.plist设成application/xml或application/octet-stream。有些 CDN 会根据自己的推断返回text/html,导致下载下来变成了网页。用 Nginx 的话大概是这样:
location ~* \.ipa$ { default_type application/octet-stream; add_header Content-Disposition "attachment"; } location ~* \.plist$ { default_type application/xml; }还有个很实际的问题:ipa 文件通常几十兆,放在普通服务器上,多人同时下载容易把带宽打满。有条件的话把 ipa 放到对象存储 + CDN 上,manifest.plist 和下载页放在应用服务器上,这样体验会好很多。
5.4 用户在手机上要做的动作
完整流程是这样的,建议把这段话直接写进给用户的通知里:
用户在 iPhone 上打开 Safari,输入下载页地址(或者从消息里长按复制链接到 Safari)→ 点击"点击安装" → 系统弹出"要安装'巡检助手'吗?" → 点"安装" → 回到桌面,能看到图标变灰、进度圈在转 → 等下载完成 → 第一次点击图标,弹出"未受信任的企业级开发者" → 打开设置 → 通用 → 往下滑找到设备管理相关的入口(不同系统版本这里可能显示为"描述文件"或"描述文件与设备管理")→ 找到对应的开发者名称 → 点进去选"信任" → 再回桌面打开,就能正常用了。
这里有两个高频问题。一是用户在微信里点链接,页面能打开,但点安装按钮毫无反应,因为内置浏览器不支持这个协议,必须引导用户"右上角用 Safari 打开"。二是信任这一步,如果用户在设置里找不到设备管理入口,通常是因为还没有真正点开过 App——必须先在桌面点一次图标、触发那个"未受信任"的提示,设置里的入口才会出现。
6. 加固、重签名与热更新的边界
包发出去了,接下来是维护阶段。这一块遇到的问题往往比首次打包更棘手,因为涉及已经装在用户手机上的包。
6.1 加固之后为什么必须重新签名
很多团队会对 ipa 做加固处理,把二进制搞乱、加壳、防反编译。加固工具改的是 Mach-O 可执行文件本身,而 iOS 的签名是对整个 App Bundle 做的哈希校验——只要二进制变了,原来的签名就失效了。这时候直接安装,系统会报"无法验证其完整性"。
解决方案就是重签名:用你自己的证书和描述文件重新给这个包签一遍。热词里"uni-app 开发的 app 加固后如何重新签名"问的就是这件事。原理不复杂,但顺序做错就会失败。
6.2 codesign 重签名命令拆解
完整流程分六步,我按顺序写。
# 1. 解压原始 ipa unzip -q hardened.ipa -d work cd work # 2. 删掉旧的签名目录 rm -rf Payload/YourApp.app/_CodeSignature # 3. 替换描述文件 cp /path/to/new.mobileprovision Payload/YourApp.app/embedded.mobileprovision # 4. 先签 Frameworks 里的动态库和 Framework(顺序很重要) codesign --force --sign "iPhone Distribution: Your Company Co., Ltd." \ --timestamp=none Payload/YourApp.app/Frameworks/*.framework codesign --force --sign "iPhone Distribution: Your Company Co., Ltd." \ --timestamp=none Payload/YourApp.app/Frameworks/*.dylib # 5. 再签主 App codesign --force --sign "iPhone Distribution: Your Company Co., Ltd." \ --entitlements ent.plist --timestamp=none Payload/YourApp.app # 6. 校验并重新打包 codesign --verify --deep --strict --verbose=2 Payload/YourApp.app zip -qr resigned.ipa Payload几个必须解释的点。顺序绝对不能反:iOS 是嵌套签名,主 App 的签名会覆盖内部所有的代码资源,如果先签主 App 再签 Framework,框架层的签名会被覆盖,安装后打开就闪退。--timestamp=none在 In-House 场景下建议加上,因为时间戳服务器在国内访问经常超时,加上这个参数能跳过联网请求,签名速度快很多。entitlements文件如果不确定,可以从描述文件里提取出来,或者干脆不加这个参数——企业证书场景下大部分 entitlements 都是默认值。
签名时用的证书名称必须和钥匙串里证书的完整名称一字不差。可以用security find-identity -v -p codesigning列出本机所有可用的签名身份,直接从输出里复制。
6.3 wgt 热更新在非商店分发下的边界
uni-app 的 wgt 热更新是个很实用的能力:把前端资源打成一个 wgt 包,App 启动时检查版本、下载、调用plus.runtime.install安装,用户无感完成更新。内部工具用这个做快速迭代非常爽。
但有几个边界要清楚。第一,wgt 只能更新前端资源,也就是 vue 页面、js、css、静态图片,任何涉及原生插件、权限、Info.plist 的改动都不可能通过 wgt 生效,必须重新打包发版。第二,wgt 安装后需要重启应用才生效(虽然plus.runtime.install可以带force参数自动重启,但体验上会闪一下),要做好版本号提示,避免用户以为出 bug 了。第三,企业证书分发的场景下做热更新,一旦你的 wgt 包出问题,用户端会大面积崩溃,所以强烈建议做灰度——先让一小部分内部设备更新,观察一天没问题再全量。第四,也是最容易被忽略的,wgt 包本身要校验完整性,下载一半中断导致的半成品包安装后会白屏,代码里一定要加文件大小和哈希校验。
7. 问题排查速查表与踩坑实录
前面讲的都是顺利路径,但实际项目里出问题的概率不低。这一节把我和身边同行遇到过的典型问题整理出来,按现象分类,方便对号入座。
7.1 安装阶段的报错怎么定位
"无法安装此 App,因为无法验证其完整性"。这个报错九成是签名层面的问题,按这个顺序查:ipa 是否被下载过程中损坏(对比一下文件大小和 MD5)、描述文件里的证书和 ipa 里的签名证书是否是同一张、ipa 是否经过重签名且重签名不完整。
"无法安装,请稍后重试"。这个通常是网络或 plist 层面的问题。先确认 manifest.plist 能不能在浏览器里直接打开,再看里面的bundle-identifier和 ipa 实际的 Bundle ID 是否一致。还有一个隐蔽原因:如果 ipa 放在 CDN 上,某些 CDN 会对超过一定大小的文件做分片或者改写,导致 iOS 下载到的包不完整,这种情况换一个直链试试。
点安装没任何反应。几乎都是因为不在 Safari 里打开的,或者在微信内置浏览器里。别怀疑代码,先换个浏览器。
弹窗一闪而过。一般是 plist 里的 bundle-identifier 写错了,或者 plist 文件的 XML 格式有问题(比如 BOM 头、缩进里有非法字符)。用一个 XML 校验工具过一遍。
7.2 装上了但打开就闪退
表现在打开瞬间消失,没有日志。最常见的原因是描述文件过期或证书被吊销。检查方法是让用户在设置里看那个开发者名称还在不在,或者你自己用security cms -D看 ExpirationDate。企业证书被吊销是突发性的,所有用户同时失效,这也是为什么我一直建议要有 Plan B。
用户装在非 Ad Hoc 描述文件里的设备上。Ad Hoc 场景下,如果目标设备的 UDID 没被写进描述文件,装的时候就会失败;但如果描述文件是旧的(里面没有这台设备),而 ipa 是新的,可能表现为装上后立刻退出。核对ProvisionedDevices是唯一可靠的办法。
隐私权限描述缺失。前面讲过,调用相册、蓝牙、定位但没写描述,系统直接杀进程。排查方法是让用户录屏,看是在哪个页面崩的,然后对照 manifest.json 里的模块勾选,把对应的描述补上。
重签名不完整。Frameworks 目录下有动态库没被签,或者签的顺序错了,表现也是打开闪退。用codesign --verify --deep --strict能查出来。
7.3 网络、权限与蓝牙的疑难杂症
打包后接口请求全部失败。如果你的后端是 HTTP 而不是 HTTPS,iOS 的 ATS(App Transport Security)会直接拦截。解决方式有两种:把后端换成 HTTPS(推荐),或者在 manifest.json 的 iOS 配置里开启"允许 HTTP 明文请求"。后一种在打包界面通常有对应的开关,不同 HBuilderX 版本位置略有差异,找不到就在源码视图里搜ats节点。
关于"uni-app network: unavailable"。这个提示和打包无关,它出现在 HBuilderX 真机运行调试的时候,通常是手机和电脑不在同一个局域网、或者 HBuilderX 的服务端口被防火墙拦了。别把它和打包后的网络问题混为一谈,两者的排查方向完全不同。
蓝牙在 iOS 上用 deviceId 建连的坑。这是一个很值得展开的点。CoreBluetooth 在 iOS 上不会给你设备的真实 MAC 地址,你拿到的deviceId其实是系统生成的一个 UUID。这个 UUID 在同一台手机、同一个 App 内是相对稳定的,但换一个 App 去扫同一台设备,拿到的 UUID 完全不同,所以不能用它做跨应用的设备唯一标识。另外,uni.openBluetoothAdapter之后扫描到的设备列表是动态的,缓存 deviceId 时要注意时效性,设备重启、系统蓝牙开关重启之后,缓存可能失效。稳妥的做法是:连接时用 deviceId 优先,失败后重新扫描并按设备名称或自定义的服务特征值来匹配。
关于"uni-app x 怎么使用 renderjs"。简短回答是:uni-app x 不支持 renderjs,它走的是 UTS 编译到原生的路线,原来的 renderjs 那套 dom 操作逻辑需要改写。如果你的项目重度依赖 renderjs,短期内还是留在 uni-app 上更稳妥。
7.4 常见问题速查表
| 现象 | 最可能的原因 | 快速验证方式 |
|---|---|---|
| 无法验证完整性 | 签名/证书不匹配或包损坏 | codesign -dv看 Authority |
| 安装无反应 | 非 Safari 打开 | 换 Safari 重试 |
| 弹窗一闪而过 | plist 里 bundle-id 错误 | 浏览器直接打开 plist 核对 |
| 桌面图标白块 | display-image 404 | 浏览器访问图标地址 |
| 打开闪退(权限类) | 隐私描述缺失 | 对照模块勾选补描述 |
| 打开闪退(签名类) | 描述文件过期或重签不完整 | security cms -D看 ExpirationDate |
| 接口全失败 | ATS 拦截 HTTP | 抓包看是否有请求发出 |
| 多人同时下载失败 | 服务器带宽或 CDN 缓存 | 换直链下载测试 |
| 版本号显示不对 | plist 里 bundle-version 未更新 | 改 plist 重新发布 |
| 加固后装不上 | 未重签名 | 按 6.2 流程重签 |
最后分享一个我在实际维护中总结出来的做法:给每一个发出去的包做一份"发布记录",内容包括版本号、Bundle ID、签名用的证书名、描述文件的过期时间、ipa 的 MD5、下载页地址。这份表平时用不上,但等到半年后某个用户反馈"装不上了",你打开表一查就知道是不是描述文件到期了,比翻聊天记录快得多。另外,企业证书和 Ad Hoc 描述文件都是一年有效期,建议在日历里提前一个月设个提醒,到期前把新包发出去,别等到用户集体打不开应用的那天再手忙脚乱。