1. 整体链路拆解:Deep Link 唤醒到底在解决什么问题
手游做用户运营、分享裂变、广告投放的时候,几乎绕不开一个场景:用户在外部环境(短信、邮件、浏览器、社交平台)点了一个链接,希望直接拉起游戏 App,并且能精准落到游戏内的某个页面——比如活动页、直播间、好友邀请关系绑定页。这就是 Deep Link 在移动端的典型应用。iOS 平台上最常用的两条链路,一条是 URL Scheme,一条是 Universal Links,围绕这两条链路,Unity 开发的 C# 层如何拿到参数并完成页面跳转,就是这个项目标题里说的“全流程”。
先说结论:这两条链路不是替代关系,而是互补关系。URL Scheme 是 iOS 从 2013 年就开始支持的老机制,只要是自家 App 提前注册好的协议,比如mygame://,就能被系统识别并唤起;Universal Links 是苹果在 iOS 9 引入的关联域名技术,使用标准的 HTTPS 链接,更“像”一个普通网页链接。两者最大的区别在于:URL Scheme 在唤醒前无法判断 App 是否已安装,而 Universal Links 可以。这个区别直接决定了拉新场景下能否优雅地做“未安装则跳转 App Store”的功能。
Unity 在这条链路里扮演的角色并不复杂,引擎本身已经封装了 iOS 原生的回调,关键是你要知道它在什么时机、用什么 API 把参数传递到 C# 层,以及冷启动和热启动两种唤醒场景下参数到达的时序完全不同。这一步处理不好,很容易出现“点了链接、游戏起来了、但参数没收到”的丢参数现象。
这篇文章适合谁看?正在用 Unity 做 iOS 手游、需要在运营活动里接分享回流或广告归因链接的开发者;也适合 Unity 客户端想补全外部唤醒能力、但对苹果侧配置不熟的团队。我会从 iOS 工程侧配置、服务器侧校验、C# 层事件监听到联调排查,把整条链路完整过一遍,并且最后分享几个我在生产环境实际踩过的坑。
2. 双链路并存:为什么只用 URL Scheme 是不够的
2.1 URL Scheme 的定位与天生短板
URL Scheme 的原理说穿了很简单:App 在 Info.plist 里声明一个自定义协议名,iOS 系统在用户点击链接时,检查这个协议是否被某个已安装的 App 注册过,注册过就直接唤起。它的优点是兼容性极好,iOS 全版本支持,配置也简单,不需要服务器配合。缺点是系统给出的是一个“二选一”的提示框:打开 App 或取消,无法得知用户当前是不是已经装了 App;如果没装,点击链接只会跳到系统错误页,完全没有补救机会。
在实际业务里,这个短板会被放大。比如某手游要做“老玩家召回”,给流失用户发短信,链接是mygame://open?uid=10086&scene=hall,如果用户已经卸载游戏,那这条短信基本等于无效触达。而现在的广告投放平台(如 Facebook、Google Ads)做 iOS 投放时,普遍要求接入 Universal Links 或使用 SKAdNetwork 归因,单纯 URL Scheme 根本无法满足“未安装时跳 App Store 下载”的诉求。所以大多数成熟项目的做法是双链路并存:Universal Links 作为主链路承载所有外部流量,URL Scheme 作为兜底机制覆盖那些 Universal Links 不稳定或失效的场景。
2.2 Universal Links 解决了什么,又带来什么新问题
Universal Links 的核心机制是“域名关联验证”:苹果要求你的 App 在后台开启 Associated Domains 能力,同时你的官网服务器上必须放一个名为apple-app-site-association的 JSON 文件,这个文件声明了哪些路径允许被 App 接管。当用户点击一个 HTTPS 链接时,iOS 系统会去请求这个文件做匹配,匹配成功并且用户已安装 App,系统会直接静默唤醒 App,不弹窗,体验比 URL Scheme 顺滑得多。
但注意,它带来了三个新问题,每一个都够配置的人喝一壶。第一,必须拥有 HTTPS 域名且能控制服务器的文件下发,这比改 Info.plist 麻烦得多;第二,苹果对apple-app-site-association文件有明确的网络和质量校验要求,文件格式错误、域名证书异常、验证入口访问不了都会失败;第三,也是最重要的——冷启动(进程被杀后通过链接唤醒)时,Universal Links 的首次触发可能走不到 Unity 的 C# 回调,因为 iOS 系统在 App 尚未初始化完毕前就把链接传给了 App,而 Unity 引擎层的回调监听注册还没完成。这个问题在后面的实操部分我会重点展开。
2.3 参数格式约定:两个链路共用一套设计
双链路并存最容易踩的坑是两个链路的参数格式不统一,导致 C# 层解析逻辑要写两套。我的建议是:URL Scheme 和 Universal Links 最终都收敛到同一种“意图表述”上,即把场景和关键参数全部放在 query string 里。比如:
- URL Scheme:
mygame://open?scene=hall&roomid=9527&uid=10086 - Universal Links:
https://m.example.com/play?scene=hall&roomid=9527&uid=10086
从 C# 层的视角,看到的都是“一个带 query 的链接”,解析时可以共用同一个函数。场景字段建议用英文小写枚举值(hall、activity、invite),参数名保持稳定,这样前后端和运营都好管理。另外我强烈建议参数值统一做 URL Encode,防止中文、emoji 或特殊字符在链路中变形——这个坑我在第 6 节会具体给案例分析。
3. 落地配置:iOS 工程侧与服务器侧全套步骤
3.1 第一步:配置 URL Scheme
URL Scheme 的配置在 Xcode 工程里操作,Unity 项目导出 Xcode 工程后,可以直接改 Unity 生成的 Info.plist,也可以使用 Unity 的Player Settings -> iOS -> Other Settings里的URL Scheme输入框(具体位置随 Unity 版本略有不同,较新版本在Player Settings -> iOS -> Configuration)。我更推荐直接在Player Settings配置,这样每次导出 Xcode 工程不会丢失配置。
配置内容是填写一个自定义协议名,比如mygame。也可以填多个,用分号分隔。需要提醒的是:协议名必须全局唯一,所以你最好用游戏产品名的英文缩写或者专门的域名反向写法,比如com.mygame.ios,而不是简单的game——因为game这种短协议名很可能已经被别的 App 注册了,系统无法判断该唤起哪一个,最终结果就是唤起失败。
配置完 URL Scheme 后,验证是否成功可以用最简单的办法:在 iOS 系统自带的 Safari 地址栏直接输入mygame://open?scene=hall,回车后如果弹出“是否在‘我的游戏’中打开?”的确认框,说明注册成功。注意,iOS 10.1 之后系统在唤起前必然弹一次确认框,这是系统策略,无法去掉。
3.2 第二步:Universal Links 的服务器文件下发
Universal Links 的配置分三块,任何一块出错都可能导致关联失败。第一块是服务器,你需要在域名根目录(也可以是子目录,但推荐/.well-known/apple-app-site-association)放一个 JSON 文件,没有.json后缀。文件内容格式如下:
{ "applinks": { "apps": [], "details": [ { "appID": "TEAMID.com.mygame.ios", "paths": ["/play/*", "/dl/*"] } ] }, "webcredentials": { "apps": ["TEAMID.com.mygame.ios"] } }这个文件里的appID是你的 Apple Developer Team ID 加 Bundle Identifier 的组合,paths声明哪些 URL 路径允许被 App 接管。要注意的是:苹果在 iOS 13 之后的系统里,要求该文件必须可以通过 HTTPS 直接访问,不能 404,不能跳转,不能有自签名证书问题,且响应头不能是text/html,而应该是application/json之类的类型(其实不严格检查 Content-Type,但保证纯 JSON 内容最安全)。
验证文件是否配置正确,可以在 Safari 里直接访问https://m.example.com/apple-app-site-association,看到 JSON 内容就是通的。还有一个更便利的方法:Shortcuts 应用里的“开深链接”快捷指令可以做文件校验,但实测下来大多数开发者用浏览器直接访问就已经够了。
3.3 第三步:Xcode 里开启 Associated Domains
服务器文件放好之后,下一步是到 Xcode 工程打开 Associated Domains 能力。需要留意的是,这个操作必须在 Apple Developer Portal 里为 App ID 开启 Associated Domains 的 App Service,然后用新的 Provisioning Profile 重签,否则 Xcode 里即使勾了也会在签名校验时报错。
在Signing & Capabilities -> All里点加号,选 Associated Domains,然后在 Domains 列表里填写applinks:m.example.com。applinks:是固定的前缀,表示这是 Universal Links 关联。一个 App 可以配置多个域名。填完之后构建安装到真机,Universal Links 的关联关系就建立了。
完整流程里有一个容易忽略的点:虽然你在 Xcode 里配置了 Associated Domains,但苹果验证关联是否生效往往有 CDN 缓存延迟,刚配置完 10 分钟内在真机测试可能会反复失败,这属于正常现象。通常等 20 分钟到 1 小时后再验证,成功率会明显提高。
3.4 第四步:iOS 工程里是否需要自行处理回调?
很多 Unity 开发者会疑惑:我已经在 iOS 原生层实现了application:openURL:options:代理方法,为什么收不到回调?这是因为 Unity 引擎的 iOS 运行时已经在UnityAppController里实现了这些代理方法,并且统一将转发给了 Unity 引擎自带的消息机制,最终会在 C# 层触发Application.deepLinkActivated事件。如果你在原生层手动再实现一套,很可能会覆盖 Unity 的处理,导致 C# 侧收不到参数。
正确的做法是:不要在 iOS 原生代码里手动拦截,直接使用 Unity 引擎提供的 C# API。假设你的游戏需要同时兼容旧版本(Unity 2018.3 之前),才需要考虑自己写原生的转发代码,但在 2018.3 之后引擎已经内置支持,不建议再造轮子。只有一种情况会考虑原生介入:需要读取 Universal Links 参数但 Unity 引擎的绝对启动参数(Application.absoluteURL)为空且deepLinkActivated没有触发时——这种情况通常是链接触发了后台恢复,苹果在旧版本 iOS(低于 13)上确实存在参数丢失的问题,我后面会在排查小结里给方案。
4. C# 层参数投递:时序、排队与延迟初始化
4.1 Unity 官方 API 的两条数据通道
Unity 自 2018.3 起提供了两个关键 API:Application.deepLinkActivated事件,以及Application.absoluteURL属性(旧版本叫Application.absoluteURL,部分老版本叫Application.url)。理解这两者的区别是整个参数投递的核心。
absoluteURL是 App 启动时引擎从系统侧拿到的“初始链接”,它只在冷启动场景下有意义。假设用户点击一个营销链接,此时游戏进程已被杀死,系统会将这个链接作为启动参数传给 App,Unity 在启动流程中会从 iOS 原生层获取它并缓存到absoluteURL里,供 C# 层随时读取。而deepLinkActivated是一个事件,它在以下两种场景都会被触发:冷启动时如果absoluteURL有值,事件也会被触发;热启动(App 已在后台运行,通过链接再唤起到前台)时,系统会向运行中的进程发送一个新的 URL,Unity 会即时派发这个事件。所以事件是唯一能覆盖“冷启动 + 热启动”的通道,绝对值得依赖。
需要注意的是一些特殊场景,比如 App 在前台时用户从外部点击链接,Unity 只触发了deepLinkActivated事件而absoluteURL并没有变化;又比如 App 从后台恢复且用户是通过 App 图标点入的,此时absoluteURL可能还停留在上一次启动时的链接上,需要额外判断避免重复跳转。
4.2 冷启动与热启动的时序差异
冷启动时序是新手最容易踩坑的地方。假设你在Awake或Start里注册了Application.deepLinkActivated += handler,理论上事件触发时会调用你的 handler。但问题在于:Unity 引擎的 C# 侧回调触发时机不一定晚于你的脚本注册时机。在实际项目中,我见过不少次deepLinkActivated在Awake里还没注册上就被触发的情况。触发早于注册的直接结果就是:丢参数。
从引擎源码和 iOS 原生运行的实现来看,Unity 在didFinishLaunching阶段就拿到了 Universal Link 参数,然后可能在UnityReady之前就向 C# 派发了事件。所以最稳妥的做法是:注册监听放在某个引导场景的最早期脚本里,然后用一个静态字段把事件里收到的原始 URL 缓存下来,再配合延迟执行的逻辑等待核心管理器初始化完毕后再消费这个缓存。
我的实现方案是在一个专门的DeepLinkManager.cs中做如下处理:
public class DeepLinkManager : MonoBehaviour { private static string _pendingLink; void Awake() { Application.deepLinkActivated += OnDeepLinkActivated; // 处理冷启动缓存 if (!string.IsNullOrEmpty(Application.absoluteURL)) { OnDeepLinkActivated(Application.absoluteURL); } } private void OnDeepLinkActivated(string url) { _pendingLink = url; // 延迟到主逻辑就绪 StartCoroutine(DispatchWhenReady()); } private IEnumerator DispatchWhenReady() { // TODO: 等待游戏初始化完成 while (!GameInitializer.IsReady) { yield return null; } HandleDeepLink(_pendingLink); } }这套方案能同时覆盖冷启动和热启动,核心思路是“先缓存,后分发”。在实际项目中,我通常还会加一个IsReady的全局初始化状态,由启动流程最后置位,而不是简单地yield return null一帧。
4.3 参数解析:从原始 URL 到游戏内指令
拿到原始 URL 后,需要把参数解析成游戏内可执行的指令。建议使用System.Uri类来解析,而不是手动字符串切割。示例代码如下:
private void HandleDeepLink(string url) { if (string.IsNullOrEmpty(url)) return; var uri = new System.Uri(url); var scene = GetQueryParam(uri, "scene"); var roomId = GetQueryParam(uri, "roomid"); switch (scene) { case "hall": UIManager.OpenHall(); break; case "room": RoomService.EnterRoom(roomId); break; case "invite": SocialService.BindInviter(GetQueryParam(uri, "uid")); break; } } private static string GetQueryParam(System.Uri uri, string key) { var query = uri.Query.TrimStart('?'); if (string.IsNullOrEmpty(query)) return string.Empty; foreach (var pair in query.Split('&')) { var idx = pair.IndexOf('='); if (idx <= 0) continue; var k = System.Uri.UnescapeDataString(pair.Substring(0, idx)); var v = System.Uri.UnescapeDataString(pair.Substring(idx + 1)); if (k == key) return v; } return string.Empty; }这里的解析逻辑核心是Uri.Query已经帮你去掉了协议名和路径,只保留从?开始的部分。如果你的链接参数里还包含路径参数(比如https://m.example.com/play/9527),则可以使用uri.Segments来取路径。注意UnescapeDataString会把%20还原成空格、%E4%B8%AD还原成中文,防止中文参数乱码。
4.4 一个容易忽视的坑:参数校验与重复消费
接 Deep Link 时还有个隐蔽问题值得展开:参数被消费后没有标记,用户在多任务切换、App 反复被唤醒的场景下,deepLinkActivated可能被重复派发同一 content。我的处理方式是在HandleDeepLink里维护一个最新一次消费的 URL 哈希值,如果当前 URL 和上次消费过的 URL 相同,则直接忽略。这个逻辑虽然简单,但能在用户重复点同一链接时避免页面被反复重置,体验会稳很多。
另外,Deep Link 参数本质上是外部输入,不能盲目信任。场景名、房间号、用户 ID 都可能被恶意构造,建议在解析后做合法性校验——比如场景是否为已知枚举值、房间号是否在合理区间、uid 是否符合规则。否则一旦被攻击者构造了特殊参数,可能导致游戏内页面报错甚至崩溃。这是我经历过线上事故后的教训,宁可多几行防御代码也不要裸奔。
5. 联调实测:从 Safari、短信、微信到推送的全场景验证
5.1 测试用例怎么设计
Deep Link 的联调不能只在电脑上点链接验通,真实用户环境远比这复杂。我建议至少覆盖以下五类场景,每一项都记录结果:
| 测试场景 | 触发方式 | 预期结果 |
|---|---|---|
| 冷启动 URL Scheme | 进程杀掉,Safari 输入mygame://open?... | 弹确认框,点开后进游戏,参数正常 |
| 冷启动 Universal Link | 进程杀掉,Safari 打开https://m.example.com/play?... | 不弹确认框,直接进游戏,参数正常 |
| 热启动 URL Scheme | App 在后台,Safari 触发 scheme | App 回到前台,事件触发,参数正常 |
| 热启动 Universal Link | App 在后台,点击链接 | App 回到前台,事件触发,参数正常 |
| 未安装状态 | 卸载 App 后点击 Universal Link | 跳转 App Store(需 fallback 逻辑) |
其中“未安装状态”的测试最能检验 Universal Links 的配置是否成功。如果配置正确,点击链接后 iOS 会打开网页,你可以通过apple-app-site-association文件里的paths设计来决定是显示 404 还是跳到官网或 App Store。这个 fallback 通常由网页本身的 JS 判断完成,比如检测到 Universal Link 未能触发 App 唤醒,就自动跳转到 App Store 对应游戏的页面。
5.2 特定来源的专项验证:微信、短信、二维码
微信内的链接跳转是一个很特殊的场景。微信有自己的内置浏览器(WKWebView),并且微信对 Universal Links 做了拦截,部分版本下点击链接并不会直接唤起 App,而是停留在页面内。常见做法是在 H5 页面上放一个“在浏览器中打开”的引导,或者用微信开放平台的openSDK进行应用内唤起授权(需要接入微信 SDK 和申请对应能力)。如果只是简单地做 Deep Link 投放,建议在测试时就明确微信内的行为是否符合预期,不要贸然上线后再来排查。
短信来源相对友好。iOS 短信里的链接点击后会直接用系统 Safari 处理,Universal Links 能平稳触发。需要注意的一点是:iOS 的链接预览(Link Preview)会自动请求你的链接页面,如果服务端对这个自动化请求处理不当,可能会触发一些埋点误报或链接热度虚高等问题。为规避这个,可以在页面响应时区分User-Agent,识别到系统预览请求时就返回轻量页面。
二维码场景本质上是 Safari 扫一扫然后再跳转,链路较长。测试时建议用系统相机扫描而不是直接用微信扫,因为微信扫码后同样会进入内置浏览器,感受会和短信链路不同。线上用户习惯往往无法强制,所以更建议在 H5 中间页里做统一处理:页面加载时立刻尝试触发 App 唤醒,并同时监听失活事件做 fallback。
5.3 一个常用而高效的自测技巧:Console 日志打通
联调时最痛苦的是无法直观看到参数是否到达 C# 层。推荐一个低成本方案:在DeepLinkManager里把收到的 URL 完整打印到日志,同时用 UNITY 的调试工具观察。如果是真机测试,可以用 Xcode 的 Console 查看系统层日志,确认 iOS 是否找到了 App 关联。如果 Xcode 日志里压根没有尝试 App 唤醒的迹象,问题基本出在服务器apple-app-site-association文件或 Associated Domains 配置上;反之,如果系统日志显示已经尝试唤起但 C# 层没有日志,问题大概率出在 Unity 引擎的事件时序上。
有一次我被一个 Universal Link 配置问题困扰了整整一个下午,最后发现是服务器返回的文件里paths写成了数组格式的嵌套,导致 JSON 解析成功但苹果的关联校验一直失败。后来习惯性地,我每次配置完都会用 curl 查看实际返回内容,避免服务器压缩或转发导致内容变形。
6. 常见问题与排查记录:上线前后最容易翻车的地方
6.1 冷启动丢参数:为什么你监听了却拿不到
这是反馈最多的问题。现象是:App 进程被杀,点击 Universal Link 唤起游戏,游戏正常启动,但deepLinkActivated事件没有触发或者触发了但参数是空的。产生这个现象的原因通常有两类。
第一类是注册时机问题。上文已经提过,Unity 引擎派发事件可能早于你的监听注册。解决办法是用Application.absoluteURL做兜底,在Awake里主动读取初始链接,并在事件回调中做去重。第二类是 iOS 系统在冷启动时传递 Universal Link 给 App 的时机和 Unity 引擎获取参数的时机之间存在竞态,部分 iOS 版本上 Unity 启动流程到 C# 层时参数已被丢弃。这个场景最有效的处理路径是:在 iOS 原生层把application:continueUserActivity:restorationHandler:拿到的参数手动缓存到一个本地文件或NSUserDefaults,待 C# 层启动后通过外部通信接口再读取。虽然绕了一圈,但能确保参数不丢。
6.2 URL Scheme 完全没反应:先分清是系统层还是引擎层
如果点击 URL Scheme 链接,Safari 压根不弹出确认框,不用说,配置根本没生效。核查顺序:第一步看 Info.plist 里CFBundleURLTypes是否存在,Bundle 里面的CFBundleURLSchemes是否和链接里的协议头完全一致;第二步确认这个链接是在真机上点的——模拟器对部分唤起流程支持不完整,但 URL Scheme 在模拟器上一般可以唤起;第三步检查是否注册了同名的协议,如果测试机上装了多个注册了同一 scheme 的 App,系统会随机选择一个,表现就是时灵时不灵。
如果确认框弹出来了,点击“打开”后游戏也起来了,但参数没到,那就是引擎层的问题。用我在 4.2 节说的缓存机制排查即可。这里我还想提供一个辅助手段:在HandleDeepLink里对 URL 做一个粗略的Debug.Log,连原始字符串一起打印,不要只打印解析后的参数,这样能确认到底哪一步丢了。
6.3 Universal Links 验证失败:文件格式、路径、域名一个都不能错
Universal Links 验证失败的高频原因我在前面已经零散说过,这里集中整理成速查表。我在接不同发行商 SDK、不同归因平台时被这些问题反复折磨过,每一条都是真金白银换来的经验。
| 问题现象 | 大概率原因 | 解决方案 |
|---|---|---|
| 首次配置后 30 分钟内不生效 | CDN 缓存未刷新 | 等待 1~2 小时再验证 |
| Safari 打开链接直接进网页,不唤起 App | 文件未下发或paths不匹配 | 检查服务器文件可访问性,核对paths |
| 唤起有一段时间有效,之后失效 | CDN 边缘节点缓存了旧的文件 | 给文件名或路径增加版本号,或直接更新源文件 |
| 其它域名同名 scheme 冲突 | 系统选择了错误 App | 审查协议名唯一性 |
| App 已安装但仍跳转 App Store | fallback 逻辑误判 | 检查 H5 页面判断逻辑,可能被微信内置浏览器干扰 |
特别想提的是paths的设计。如果你的 Universal Link 路径是https://m.example.com/dl?scene=hall,那paths应该写成类似["/dl/*"]或直接写["*"](谨慎,接管全站路径会影响网站的正常访问)。如果你用["/dl"]而不写*,某些系统版本下带 query 的链接匹配成功率不稳定。所以推荐统一使用带*的路径通配写法。
6.4 iOS 14 后剪贴板耗弹、参数乱码与归因平台冲突
最后聊聊剪贴板。早年很多营销链路为了规避 Deep Link 的限制,会把参数写到系统剪贴板,App 启动后读剪贴板拿到参数。iOS 14 之后系统会自动弹“粘贴”权限提示,处理不当用户体验血崩。而且 App Store 审核对剪贴板读取有严格限制描述,如果你的 App 在审核中涉及剪贴板 API,会被要求披露用途,得不偿失。新的流程中,如果不是极端兼容需求,建议放弃剪贴板方案,专心把 Universal Links 和 URL Scheme 链路做好。
参数乱码的问题比较隐蔽,多发生在 H5 页面拼接链接时没有对参数做encodeURIComponent。比如 uid 是数字、scene 是英文还好,一旦出现昵称、签名这类中文字段,直接拼到 URL 里,某些场景下系统会自动把中文转成固定的编码或丢弃,导致 C# 层解析出错。解决方式是在生成链接时统一用标准 URL 编码并在解析时用System.Uri.UnescapeDataString解码。这个约定要在前后端、运营发链、投放第三方都定义清楚,一人一份规范文档,比开发侧到处打补丁强。
归因平台冲突也值得多说一句。像 AppsFlyer、Adjust 这些归因 SDK 会自己接管 Universal Links,如果你同时使用了这些平台,那么你的 C# 层拿到的参数可能不是原始链接,而是归因平台二次封装后的对象。遇到这种情况,要在归因 SDK 的配置里声明不拦截 Deep Link,或者在 Unity 侧的数据通道上同时监听两个来源,取最先到达且合法的参数使用。我试过在同一个游戏里同时接归因 SDK 和自己写的 Deep Link 监听,结果出现参数重复处理,后来是通过在归因 SDK 的回调中增加一个“仅用于归因上报,不做页面跳转”的开关解决的。
7. 扩展与取舍:这些方案在真实项目中怎么组合
对于上线的 Unity iOS 手游,完整的 Deep Link 方案通常不是只靠文章里这几个 Unity API 就能贴合全部业务,而是需要结合推送、分享、运营甚至 App 内活动页来通盘设计。个人经验是可以把 Deep Link 能力抽象成一个小型的“路由中心”,所有入口来源(链接、推送、扫码、内部公告)最终都转化为同一套参数,由路由中心统一分派到各个模块。这样做的好处是业务方不用关心来源是短信还是二维码,只需要监听“带着场景和参数进入了 App”这一个入口即可。
推送也是一个容易和 Deep Link 混在一起的场景。本地推送和远程推送的 payload 里通常可以带launchOptions信息,点击推送横幅进入 App 时,App 需要读取推送里的自定义字段,再根据字段内容做页面跳转。这和 Deep Link 参数投递几乎是同一套逻辑。如果你独立接入了极光、个推或自家推送服务,可以先统一拿到 payload 的 JSON,再按 Deep Link 参数格式投递给路由中心。对了,iOS 的推送点击唤起在某些版本下也不会可靠地触发deepLinkActivated,因为推送不是通过 URL 唤醒的,而是通过application:didReceiveRemoteNotification:回调进入,所以推送链路建议单独走一条 JSON 参数通道,不要硬塞进 Deep Link 的统一接口。
从成本收益角度看,小团队建议第一步只做 URL Scheme,因为配置最快,也能支持存量玩家的活动召回;如果在做买量投放或需要拉新场景,再上 Universal Links。尤其注意 Universal Links 的服务器配置和排障周期普遍比预期长,一定要留出提前量,不要在投放预热前一天才开始配置。我的团队最开始第一版只做了 URL Scheme,第二版才补齐 Universal Links,整体踩坑量少了很多,因为等业务对 Deep Link 的判定逻辑成熟后,再切换主链路时排查方向会更清楚。
最后再分享一个小技巧:所有外部来源参数在投递给业务逻辑前,最好统一过一个“清洗层”,把来自不同来源的可疑字段(超长字符串、脚本字符、特殊控制符)过滤掉。我的团队在接线上活动后不久就遇到过有人恶意构造scene=hall&roomid=../../etc/passwd类似的参数来探测服务端,清洗层上线后这类异常请求基本消灭。外部输入永远不值得信任,这句话在客户端同样适用。