iOS Ad Hoc 外网分发实践

最近处理了一个很常见但又容易踩坑的需求:已经打好了 iOS Ad Hoc 包,怎么让测试、客户验收或演示同事通过一个外网链接安装?

先说结论:Ad Hoc 外网分发并不是绕过 Apple 的安装限制,它只是把已经签名好的 IPA 放到一个可访问、可下线、可追踪的下载链路上。设备仍然必须提前登记到 Apple Developer 账号里,IPA 也必须使用包含这些设备的 Ad Hoc provisioning profile 签名。

推荐方案

我更推荐的结构是:

1
静态安装页 + manifest.plist + 外部 IPA 托管

也就是:

1
2
3
4
5
6
7
GitHub Pages / 静态站点
install.html
manifest.plist
icon.png

对象存储 / CDN / GitHub Release
app.ipa

安装页负责展示版本信息和安装按钮,manifest.plist 负责告诉 iOS IPA 在哪里,IPA 本体则放到更适合大文件下载的地方。

如果只是临时验证,并且 IPA 小于 100 MB,也可以短时间把 IPA 放在 GitHub Pages 目录里。但这不适合作为长期方案:包体会让仓库变重,下载量稍大也会碰到带宽和仓库文件大小限制。

安装原理

iOS 网页安装 Ad Hoc 包依赖 itms-services 协议。安装页里一般放一个这样的链接:

1
2
3
<a href="itms-services://?action=download-manifest&url=https%3A%2F%2Fexample.com%2Fios-dist%2Fcompany-app%2Fmanifest.plist">
安装 iOS 测试版
</a>

注意几个细节:

  1. url= 后面的 manifest 地址必须 URL Encode。
  2. manifest 地址必须是 HTTPS。
  3. manifest 里面的 IPA 地址也必须是 HTTPS。
  4. 尽量让用户用 iPhone 或 iPad 的 Safari 打开。
  5. 微信、企业 IM、邮件内置浏览器可能拦截安装动作。

一个最小可用的 manifest.plist 大概是这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
"https://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://download.example.com/company-app/1.2.3/45/app.ipa</string>
</dict>
</array>
<key>metadata</key>
<dict>
<key>bundle-identifier</key>
<string>com.company.app</string>
<key>bundle-version</key>
<string>45</string>
<key>kind</key>
<string>software</string>
<key>title</key>
<string>Company App</string>
</dict>
</dict>
</array>
</dict>
</plist>

目录怎么设计

分发目录最好不要只用 App 名和版本号,因为这种路径太容易猜。

我一般会加一个随机片段:

1
2
3
4
5
6
7
8
ios-dist/
company-app/
1.2.3/
45-a8f3c92d/
install.html
manifest.plist
icon.png
metadata.json

这样安装页地址可以是:

1
https://sweetloser.com/ios-dist/company-app/1.2.3/45-a8f3c92d/install.html

如果希望博客和分发页更干净地隔离,也可以用独立子域名:

1
https://dist.sweetloser.com/company-app/1.2.3/45-a8f3c92d/install.html

独立子域名的好处是后续迁移方便。今天用 GitHub Pages,明天切对象存储、Cloudflare Pages、Vercel 或自建服务,链接结构不会和博客内容绑死。

为什么 IPA 不建议直接放 Pages

GitHub Pages 非常适合放安装页、说明页、图标、manifest,但不适合作为长期 IPA 仓库。

主要原因:

  1. IPA 是大文件,多版本保留后仓库会很快变大。
  2. GitHub 普通仓库会限制大文件,超过 100 MiB 就会很麻烦。
  3. GitHub Pages 更像静态站点托管,不是下载分发服务。
  4. 对象存储和 CDN 在日志、限速、缓存、下线、生命周期清理方面更好用。

所以长期方案里,我会让 Pages 只承担入口层,IPA 放对象存储、CDN、GitHub Release 或其他文件下载服务。

外网链接的安全边界

公开链接模式要接受一个事实:拿到链接的人都能访问。

随机目录、robots.txtnoindex 只能降低被搜索和枚举的概率,它们不是权限控制。

可以做的基础措施有:

  1. 版本目录增加随机片段。
  2. 不提供公开版本列表。
  3. 安装页加 <meta name="robots" content="noindex,nofollow">
  4. robots.txt 屏蔽分发目录。
  5. 保留下载日志,关注异常 IP、异常下载量和异常 User-Agent。
  6. 发现链接扩散后,立刻下线旧目录,重新生成随机目录。

如果包内容敏感,或者分发对象很严格,就不要只依赖随机路径。那种场景应该加鉴权、短期签名 URL,或者使用更正式的内部分发平台。

发布前检查清单

每次发布前,我会按下面这份清单过一遍:

  1. install.html 能通过 HTTPS 访问。
  2. 安装链接里的 manifest URL 已 URL Encode。
  3. manifest.plist 返回 200。
  4. manifest 里的 IPA URL 返回 200。
  5. IPA 使用 Ad Hoc 签名。
  6. 目标设备 UDID 已包含在 provisioning profile 中。
  7. iPhone Safari 能点击安装。
  8. 微信或企业 IM 内置浏览器有 Safari 打开提示。
  9. 页面包含 noindex,nofollow
  10. 分发目录已被 robots.txt 屏蔽。
  11. 旧版本下线后,旧安装页不能继续安装。

常见安装失败原因

点击安装没有反应时,优先排查:

  1. 是否使用 Safari 打开。
  2. manifest 是否 HTTPS。
  3. manifest 是否返回 200。
  4. IPA URL 是否返回 200。
  5. Bundle ID、版本号、title 是否填写正确。
  6. 当前设备是否在 Ad Hoc 设备列表中。
  7. 设备上是否已有同 Bundle ID 但签名不同的 App。

如果页面和 manifest 都没问题,但仍然无法安装,最常见的原因就是设备没有被加入 Ad Hoc profile。外网分发链路只能负责下载,不能改变 iOS 的签名校验。

最后

iOS Ad Hoc 外网分发最重要的不是页面多漂亮,而是链路稳定、路径清晰、能快速下线。

我的推荐可以压缩成一句话:

1
安装页放静态站点,IPA 放对象存储或 CDN,路径加随机片段,发布前真机验证,下线时立刻断入口。

这样做足够轻,也足够稳。对于内部测试、客户验收和小范围演示,比一上来搭完整分发后台更合适。