ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

iOS Universal Links 配好了却打开网页?AASA 缓存与 Scene 回调排查指南

iOS Universal Links 配好了却打开网页?AASA 缓存与 Scene 回调排查指南 1. 从一个让人抓狂的现象说起如果你做过 iOS 端和 Web 端的联动大概率踩过这个坑AASA 文件apple-app-site-association已经部署到服务器根目录Xcode 里 Associated Domains 也配好了applinks:yourdomain.com真机装包、卸载重装、重启手机都试过结果在 Safari、备忘录或者微信里点链接它还是老老实实打开网页App 死活不弹。更邪门的是有时候它又能跳。同一台设备同一个链接从不同入口点进去表现完全不一样。你开始怀疑人生是 AASA 没生效是 CDN 缓存是苹果抽风还是我证书有问题我前后在三个项目里被这个问题反复折磨过最长的一次排查了整整两天。后来把苹果官方那份排障文档翻了个底朝天又结合swcutil这个命令行工具反复验证才把整条链路彻底摸清楚。这篇就把我踩过的坑、验证过的方法、以及那些文档里写得含糊但实际很关键的点一次性讲透。这篇文章适合谁看只要你在做 iOS 的 Universal Links 接入不管你是刚配完发现不生效的新手还是配好了偶尔抽风的“老手”都能从里面找到对应的排查路径。核心关键词就几个Universal Links、AASA、Associated Domains、swcutil、Scene。我会围绕这几个点把“为什么配好了还是打开网页”这件事拆到骨头里。先说结论方向绝大多数“配好了不生效”问题不在 AASA 本身而在苹果设备对 AASA 的缓存机制、链接的唤起入口、以及App 生命周期里 Scene 相关的处理这三块。下面一层层剥。2. 先搞懂 Universal Links 到底是怎么被唤起的2.1 从点击到跳转中间发生了什么很多人以为 Universal Links 是“系统识别到链接就打开 App”其实不是。它的完整链路大致是这样用户在某个入口Safari、备忘录、信息、第三方 App 的 WebView 等点击一个https://yourdomain.com/xxx的链接。系统拿到这个域名去查本机是否已经缓存了这个域名的 AASA 记录。如果缓存里存在这个域名并且路径匹配规则paths / components命中了当前链接系统就把这次点击交给对应的 App 处理。App 通过scene(_:continue:)或application(_:continue:restorationHandler:)拿到NSUserActivity从中解析出webpageURL完成跳转。关键点在第 2 步系统查的是本机缓存不是实时去你的服务器拉 AASA。这就是为什么你改了 AASA 之后设备上可能几天都不生效——它压根没重新拉。2.2 AASA 缓存机制为什么你改了服务器却没用苹果设备对 AASA 的缓存策略官方说法是“由系统管理App 无法直接控制”。实际表现是首次安装 App 时系统会尝试拉取一次 AASA。之后会按系统自己的节奏更新可能几小时也可能几天。卸载重装 App 会触发重新拉取但不保证立即。设备重启有时会触发更新但同样不保证。我实测下来最可靠的方式是用swcutil手动触发一次强制更新而不是靠卸载重装碰运气。这个后面会详细讲。注意AASA 必须通过 HTTPS 提供且不能有任何重定向。HTTP 301/302 到另一个地址系统会直接判定失败。CDN 上如果配了强制跳转也会导致拉取失败。2.3 Associated Domains 配置的常见误区Xcode 里 Capabilities 打开 Associated Domains添加applinks:yourdomain.com这一步看起来简单但坑不少不要带路径applinks:yourdomain.com是对的applinks:yourdomain.com/path是错的。不要带协议写applinks:https://yourdomain.com也是错的。子域名要单独加applinks:www.yourdomain.com和applinks:yourdomain.com是两条独立记录只加一个另一个不生效。通配符有限制applinks:*.yourdomain.com这种写法在部分系统版本上行为不一致建议显式列出。还有一个隐蔽的点如果你用的是企业证书或者某些特殊签名方式Associated Domains 的 entitlement 可能没有正确写入。可以用codesign -d --entitlements :- YourApp.app检查一下确认com.apple.developer.associated-domains确实在里面。3. AASA 文件本身的写法与校验3.1 文件位置和 Content-TypeAASA 必须放在域名的根目录路径是https://yourdomain.com/.well-known/apple-app-site-association注意几个硬性要求不能有.json后缀。写成apple-app-site-association.json系统不认。Content-Type 必须是application/json。很多服务器默认给.well-known下的无后缀文件返回application/octet-stream或text/plain这会导致解析失败。不能有重定向。直接返回 200不能 301/302。不能要求鉴权。系统拉取时不会带任何 Cookie 或 Token。我遇到过一次Nginx 配置里对.well-known目录做了统一 rewrite结果 AASA 被 302 到了另一个路径排查了半天才发现。用curl -I看一眼响应头就能确认curl -I https://yourdomain.com/.well-known/apple-app-site-association重点看三行HTTP/1.1 200、Content-Type: application/json、没有Location头。3.2 新旧格式的差异AASA 有两套格式老格式用paths新格式用components。苹果现在推荐用components因为paths的通配符语义比较混乱。老格式示例{ applinks: { apps: [], details: [ { appID: TEAMID.com.yourcompany.yourapp, paths: [/product/*, /share/*] } ] } }新格式示例{ applinks: { details: [ { appIDs: [TEAMID.com.yourcompany.yourapp], components: [ { /: /product/*, comment: 商品详情页 }, { /: /share/*, exclude: true, comment: 分享页不跳 App } ] } ] } }几个容易写错的地方appID是TeamID.BundleID拼起来的中间用点。TeamID 是 10 位字符不是你的账号名。新格式里appIDs是数组老格式里appID是字符串别混。components里的/是路径匹配支持*和?但语义和正则不一样*匹配任意字符包括/?匹配单个字符。exclude: true表示排除优先级高于包含规则。3.3 用苹果官方工具校验苹果提供了一个在线校验入口在开发者后台的 Associated Domains 配置页面里可以输入域名让苹果去拉取并校验。但更方便的是本地用swcutil直接看设备上的缓存状态。另外AASA 的 JSON 语法错误是致命的。一个多余的逗号、一个中文引号都会导致整个文件解析失败。建议用jq先本地校验一遍curl -s https://yourdomain.com/.well-known/apple-app-site-association | jq .如果jq报错说明 JSON 本身有问题先修好再谈其他。4. swcutil排查 Universal Links 的核武器4.1 swcutil 是什么能干什么swcutil是苹果系统内置的一个命令行工具全称大概是 Shared Web Credentials Utility但它管的不只是密码共享还包括 Universal Links 的 AASA 缓存。你可以用它查看当前设备上某个域名的 AASA 缓存状态、手动触发更新、甚至清空缓存。这个工具在 macOS 上可以直接用在 iOS 真机上需要通过devicectl或者 Xcode 的 Devices 窗口连上去操作。我一般在 macOS 上排查因为命令更顺手。4.2 常用命令速查查看某个域名的 AASA 缓存swcutil show -d yourdomain.com输出里会包含applinks相关的记录能看到AppID、Paths、以及LastFetched时间。如果LastFetched是很久以前说明缓存没更新。手动触发更新swcutil update -d yourdomain.com这个命令会强制系统重新去拉一次 AASA。实测下来这是让改动立即生效最靠谱的方式比卸载重装快得多。清空某个域名的缓存swcutil reset -d yourdomain.com如果更新后还是不对可以先 reset 再 update相当于彻底刷新。查看所有已缓存的域名swcutil show不加-d参数会列出所有记录方便你确认域名到底有没有被系统记住。4.3 一个真实的排查案例有次线上反馈部分用户点分享链接不跳 App。我先在出问题的设备上跑swcutil show -d ourdomain.com结果发现LastFetched是三天前而且Paths里还是旧规则。说明设备压根没拉到新 AASA。于是swcutil reset -d ourdomain.com swcutil update -d ourdomain.com再 show 一次LastFetched变成刚刚Paths也更新了。让用户再点链接正常跳转。整个过程不到两分钟。如果靠卸载重装可能得折腾半小时还不一定成功。提示swcutil在 iOS 真机上需要通过 Xcode 的 Devices and Simulators 窗口选中设备后打开终端或者用devicectl转发命令。不同系统版本命令参数可能略有差异建议先swcutil --help看一眼。5. Scene 生命周期跳转拿到了但页面没反应5.1 Scene 引入后 continue 回调的变化iOS 13 引入 Scene 之后Universal Links 的回调路径变了。以前只需要在 AppDelegate 里实现func application(_ application: UIApplication, continue userActivity: NSUserActivity, restorationHandler: escaping ([UIUserActivityRestoring]?) - Void) - Bool { // 处理 }现在如果 App 使用了 Scene这个回调可能不会被调用取而代之的是 SceneDelegate 里的func scene(_ scene: UIScene, continue userActivity: NSUserActivity) { guard userActivity.activityType NSUserActivityTypeBrowsingWeb, let url userActivity.webpageURL else { return } // 处理 url }问题在于如果你的 App 同时实现了 AppDelegate 和 SceneDelegate 的回调系统只会走其中一条。具体走哪条取决于 App 是否声明了 Scene 配置。如果Info.plist里有UIApplicationSceneManifest那就走 SceneDelegate没有的话走 AppDelegate。我见过最坑的情况是项目中途从纯 AppDelegate 迁移到 Scene但旧的回调没删新回调没加结果 Universal Links 完全失效但冷启动又正常。因为冷启动走的是willConnectTo热启动走continue两条路径不一样。5.2 冷启动与热启动的差异Universal Links 的唤起分两种情况冷启动App 没在后台点链接会先启动 App然后通过scene(_:willConnectTo:options:)里的connectionOptions.userActivities拿到 activity。热启动App 在后台点链接会走scene(_:continue:)。很多人的代码只处理了其中一种导致“第一次能跳第二次不行”或者反过来。正确的做法是两个入口都要处理并且抽一个公共方法func handleUniversalLink(_ url: URL) { // 统一的路由逻辑 } // 冷启动 func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { if let activity connectionOptions.userActivities.first(where: { $0.activityType NSUserActivityTypeBrowsingWeb }), let url activity.webpageURL { handleUniversalLink(url) } } // 热启动 func scene(_ scene: UIScene, continue userActivity: NSUserActivity) { guard userActivity.activityType NSUserActivityTypeBrowsingWeb, let url userActivity.webpageURL else { return } handleUniversalLink(url) }5.3 多 Scene 场景下的坑iPadOS 支持多窗口一个 App 可能有多个 Scene 同时存在。这时候点链接系统会把 activity 交给当前活跃的 Scene。如果你的路由逻辑依赖某个全局单例而不同 Scene 的状态不一致就可能出现“跳转了但页面没刷新”的情况。我的做法是路由逻辑尽量无状态或者把状态挂在 Scene 级别而不是 App 级别。如果确实需要全局状态用通知或者共享的 coordinator 来同步。还有一个细节scene(_:continue:)里不要做耗时操作系统对回调有时间限制。如果需要网络请求先返回再异步处理。6. 那些“看起来无关”但实际致命的外部因素6.1 唤起入口的差异同一个链接从不同地方点行为可能完全不同入口是否触发 Universal Links说明Safari 地址栏直接输入否地址栏输入不触发必须点击链接Safari 页面内点击是正常触发备忘录里的链接是正常触发信息 App 里的链接是正常触发微信内置浏览器否微信屏蔽了 Universal Links第三方 App 的 WebView视情况部分 WebView 不触发扫码进入的网页视情况取决于扫码后打开的容器微信是最典型的例子。微信内置浏览器对 Universal Links 做了拦截点链接只会打开网页。这不是你配置的问题是微信的策略。很多团队的做法是在微信里引导用户“用浏览器打开”或者用微信的开放标签能力。6.2 用户手动关闭了跳转iOS 有个机制如果用户在某个链接的顶部横幅里点了“打开网页”而不是“打开 App”系统会记住这个选择之后这个域名的链接都不再跳 App。这个状态存在系统里用户自己可能都不知道。解决办法是让用户长按链接选择“在 App 中打开”或者去设置里重置。但这个操作对普通用户来说太隐蔽了。实际项目中我们会在网页上做一个引导检测到是 iOS 且未跳转时提示用户“长按链接选择在 App 中打开”。6.3 系统版本与设备差异不同 iOS 版本对 AASA 的解析行为有细微差异。比如iOS 13 之前对components格式支持不完整。iOS 14 之后对exclude的处理更严格。某些版本对*通配符的匹配范围有变化。我一般会在 AASA 里同时保留paths和components做兼容。虽然苹果说components优先但老设备上paths还是兜底。另外模拟器上的 Universal Links 行为不可靠一定要用真机测。而且最好准备两台设备一台新系统一台老系统交叉验证。7. 一套可复用的排查流程7.1 从现象到根因的排查顺序遇到“配好了还是打开网页”我一般按这个顺序排查确认 AASA 可访问curl -I看状态码、Content-Type、有无重定向。确认 AASA 内容正确jq校验 JSON检查appID、paths/components。确认设备缓存状态swcutil show -d yourdomain.com看LastFetched和规则。强制更新缓存swcutil resetswcutil update。确认 Associated Domains 配置Xcode Capabilities、entitlement 文件。确认唤起入口换 Safari、备忘录、信息分别测试。确认代码回调冷启动、热启动、Scene 与 AppDelegate 路径。确认用户侧状态是否手动关闭过跳转。这个顺序是从外到内、从易到难大部分问题在前三步就能定位。7.2 常见问题速查表现象可能原因排查方法完全不跳所有入口都不跳AASA 不可访问或格式错误curl jq 校验部分入口跳部分不跳入口限制如微信换 Safari 测试改了 AASA 后不生效设备缓存未更新swcutil update冷启动跳热启动不跳Scene 回调未处理检查 continue 方法热启动跳冷启动不跳willConnectTo 未处理检查 connectionOptions之前能跳突然不跳用户手动关闭了跳转长按链接重新选择新装设备不跳AASA 拉取失败检查 HTTPS 和重定向老设备不跳新设备跳格式兼容问题同时保留 paths 和 components7.3 几个我踩过的坑坑一CDN 缓存了 AASA。有次更新了 AASA服务器上是对的但 CDN 边缘节点还是旧文件。curl加-H Cache-Control: no-cache才能看到新的。解决办法是刷新 CDN 缓存或者给 AASA 设置较短的 TTL。坑二Nginx 对无后缀文件的 MIME 处理。默认配置下.well-known/apple-app-site-association可能被当成application/octet-stream。需要在 Nginx 里显式加location /.well-known/apple-app-site-association { default_type application/json; }坑三TeamID 写错。TeamID 是 10 位容易和账号里的其他 ID 混淆。在开发者后台的 Membership 页面可以查到复制粘贴别手打。坑四测试时用了 TestFlight 版本。TestFlight 版本的 BundleID 可能和正式版不同AASA 里的appID如果只写了正式版TestFlight 就不生效。测试阶段建议把两个都加上。坑五Scene 迁移不彻底。项目从 AppDelegate 迁 Scene 时如果Info.plist里加了UIApplicationSceneManifest但代码没跟上Universal Links 会静默失效。表现是 App 能启动但链接回调不触发。8. 一些进阶的优化思路8.1 AASA 的版本管理AASA 是线上文件但它的内容又和 App 版本强相关。如果新版本 App 支持了新的路径但 AASA 还没更新新路径就不跳。反过来AASA 更新了但用户没升级 App旧版本可能解析不了新规则。我的做法是AASA 里的规则尽量向前兼容新路径用新增而不是修改的方式。同时给 AASA 加一个版本号字段虽然系统不认但方便自己排查配合 CI 在发版时自动部署。8.2 网页侧的兜底引导即使 Universal Links 配得再好也总有跳不过去的情况。网页侧做一个兜底检测到是 iOS 设备且一段时间内没有跳转就显示一个引导层提示用户“点击右上角在 Safari 中打开”或者“长按链接选择在 App 中打开”。这个引导不要一上来就弹会干扰正常用户。可以设一个 1.5 秒的定时器如果页面还在前台说明没跳成功再显示引导。8.3 监控与告警Universal Links 的失败往往是静默的用户不会反馈只会觉得“怎么没跳”。可以在网页侧埋点统计跳转成功率。如果某个版本发布后成功率骤降大概率是 AASA 或配置出了问题。我们内部的做法是网页加载时记录一个时间戳如果 2 秒内页面被切到后台说明跳 App 了就上报成功否则上报失败。按域名和路径维度聚合能快速发现异常。8.4 关于 swcutil 的自动化swcutil目前只能在设备上手动跑没法集成到 CI。但可以在测试阶段写一个脚本通过devicectl连真机批量执行show和update减少人工操作。我们团队内部有一个小工具测试同学点一下就能刷新指定设备的 AASA 缓存省了不少沟通成本。这个工具的核心就是几条命令的封装没什么技术含量但确实好用。如果你团队里也有多台测试机值得花半小时做一个。我个人在实际操作中的体会是Universal Links 这个问题90% 的“配好了不生效”都出在缓存和入口这两块剩下 10% 里又有大半是 Scene 回调没处理对。真正 AASA 写错的反而少因为写错了通常一开始就不生效不会出现“时好时坏”的情况。最后再分享一个小技巧排查时先别急着改代码先在设备上跑一遍swcutil show -d yourdomain.com。如果LastFetched是很久以前或者规则和你服务器上的对不上那问题就在缓存reset加update基本能解决。这一步能帮你省掉大量无谓的代码审查时间。
返回列表