ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

Unity手游iOS Deep Link实战:URL Scheme与Universal Links配置及C#参数投递

2026/10/2 4:31:03 拓冰建站 浏览量
Unity手游iOS Deep Link实战:URL Scheme与Universal Links配置及C#参数投递 1. 为什么手游团队绕不开 Deep Link 这件事做过手游投放或者拉新活动的兄弟应该都有体会买量买来的用户点开广告之后如果只是被丢到 App Store 下载页装完打开游戏却停在登录界面那这个转化链路基本就废了一半。用户明明是被某个具体活动、某个具体礼包、某个具体好友邀请吸引过来的结果进游戏之后什么都看不到还得自己去找入口流失率能不高吗。Deep Link要解决的就是这个问题。简单说它让一条链接能够穿透到 App 内部把用户直接送到指定页面并且把链接里携带的参数一并交给游戏逻辑去处理。在 iOS 上这套机制主要有两条技术路线URL Scheme和Universal Links。前者是老牌方案配置简单但体验有硬伤后者是苹果主推的方案体验好但配置链路长、坑也多。而 Unity 手游的特殊性在于iOS 原生层拿到链接之后还需要跨过 Objective-C/Swift 与 C# 的边界把参数准确投递到游戏逻辑层。这一步如果没处理好就会出现原生层明明收到了链接游戏里却拿不到参数的经典问题。我见过不少团队在这一步卡了好几天最后发现是回调时机或者字符串编码的问题。这篇内容适合三类人看一是正在做手游拉新、召回、活动投放需要打通 Deep Link 链路的开发同学二是负责 Unity 与 iOS 原生桥接的客户端工程师三是对 iOS 唤起机制感兴趣、想搞清楚 URL Scheme 和 Universal Links 到底差在哪里的技术负责人。我会从方案选型讲到原生配置再讲到 C# 层的参数投递把整条链路拆开揉碎尽量让每个环节都能直接抄作业。2. 方案选型URL Scheme 和 Universal Links 到底怎么选2.1 两种机制的本质区别URL Scheme的思路很直接给 App 注册一个自定义协议头比如mygame://系统收到这个协议的链接时就会把 App 拉起来。它的优点是配置极其简单只需要在 Xcode 的 Info 配置里加一项就行而且不依赖域名和服务器。缺点也很明显任何 App 都能注册同名 Scheme存在被劫持的风险在浏览器里打开时很多场景会弹一个是否打开某某 App的确认框体验割裂更麻烦的是如果用户没装 App这个链接就直接报错了没有任何兜底。Universal Links是苹果后来推出的方案它用标准的https://链接通过在你自己的域名下放一个apple-app-site-association简称 AASA文件来声明哪些路径归哪个 App 处理。用户点击这种链接时如果装了 App 就直接进 App没装就正常打开网页天然具备降级能力。而且它不弹确认框体验顺滑也不存在被其他 App 抢注的问题。我把两者的关键差异整理成一张表选型的时候对着看就行对比维度URL SchemeUniversal Links链接形式自定义协议如mygame://标准 https 链接配置复杂度低改 Info 即可高需要域名、AASA 文件、证书未安装 App 时报错无兜底自动打开网页可做引导安全性与防劫持弱可被抢注强域名归属校验唤起确认弹窗部分场景有无从其他 App 内唤起支持较好部分 App 内需用户手动操作参数传递直接拼在链接里拼在链接里走同一套解析2.2 实际项目里的组合策略纯用 Universal Links 是不是就够了理论上是但实际投放场景里我建议两条腿走路。原因有几个一是有些第三方渠道或者老旧的 H5 页面对 Universal Links 的支持并不稳定尤其是从某些 App 的内嵌浏览器里点击时系统可能不会触发 Universal Links 而是直接当普通网页打开二是 Universal Links 的生效依赖 AASA 文件的缓存苹果的 CDN 缓存更新有延迟新配置的域名可能要等一段时间才生效测试阶段容易误判。所以比较稳妥的做法是主链路用 Universal Links同时保留 URL Scheme 作为兜底。当 Universal Links 没触发时网页端可以通过 JavaScript 尝试用 Scheme 唤起再配合超时检测跳转到 App Store。这样既保证了正常情况下的顺滑体验又覆盖了边缘场景。提示如果你的游戏同时有多个渠道包或者马甲包Universal Links 的 AASA 文件里可以用通配符或者多个 App ID 来声明但要注意每个 App 的 Team ID 和 Bundle ID 必须准确写错一个字符整条链路就废了。3. iOS 原生层的配置实操3.1 URL Scheme 的配置步骤URL Scheme 的配置相对简单在 Unity 导出的 Xcode 工程里操作即可。打开Info.plist找到URL Types这一项添加一个新的 URL Type。关键字段是URL Schemes填你的自定义协议名比如mygame。注意这里不要带://只填协议名本身。如果你是用 Unity 的 PostProcessBuild 脚本自动改 Xcode 工程可以在构建后处理里用PlistDocument或者直接操作project.pbxproj来注入。我个人的习惯是写一个构建后处理脚本把 URL Scheme、Universal Links 的 Associated Domains 权限、以及必要的 Info 配置一次性搞定避免每次出包都手动改。配置完成之后验证方式很简单在 Safari 地址栏输入mygame://如果系统弹出是否打开并成功拉起 App说明 Scheme 注册成功。这里有个细节iOS 对 Scheme 的调用有频率限制短时间内反复调用可能会被系统忽略测试的时候别一直狂点。3.2 Universal Links 的完整配置链路Universal Links 的配置是整条链路里最容易出问题的部分我把它拆成几个必须完成的动作。第一步是在苹果开发者后台开启 Associated Domains 能力。进入 App ID 的配置页面勾选 Associated Domains。这一步不做后面 Xcode 里的配置是不生效的。第二步是在 Xcode 工程里添加 Associated Domains。在 Signing Capabilities 里加上 Associated Domains然后添加一条记录格式是applinks:你的域名。注意这里只写域名不要带https://也不要带路径。比如applinks:link.example.com。第三步是在域名根目录部署 AASA 文件。这个文件的路径必须是https://你的域名/.well-known/apple-app-site-association注意没有后缀名Content-Type 应该是application/json。文件内容大致长这样{ applinks: { apps: [], details: [ { appID: TEAMID.com.company.mygame, paths: [/open/*, /activity/*] } ] } }appID是 Team ID 加 Bundle ID 的组合中间用点连接。paths声明哪些路径归这个 App 处理可以用通配符。这里有个大坑AASA 文件必须通过 HTTPS 访问不能有重定向证书必须有效。我遇到过因为 CDN 配置了 301 跳转导致 AASA 校验失败的情况排查了很久。第四步是验证配置是否生效。苹果提供了一个诊断工具在 App Store Connect 里可以查看 Associated Domains 的状态。另外在设备上可以长按一个符合规则的链接如果弹出的菜单里有在某某 App 中打开说明配置生效了。测试阶段建议用真机模拟器对 Universal Links 的支持不完整。3.3 一个容易忽略的细节AASA 缓存AASA 文件不是实时拉取的苹果的 CDN 会缓存它。首次安装 App 时系统会去拉取一次之后如果文件更新了系统不会立刻重新拉取。这意味着你改了 AASA 之后已经装了 App 的设备可能还是用旧配置。解决办法有两个一是测试阶段卸载重装 App强制重新拉取二是在 AASA 的响应头里设置合适的缓存策略。生产环境里如果确实需要更新 AASA要做好用户侧可能延迟生效的心理准备别指望改完立刻全量生效。4. 原生层到 C# 层的参数投递4.1 链接到达原生层的两个入口iOS 原生层接收 Deep Link 有两个关键回调分别对应 App 的两种状态。当 App还没启动是被链接冷启动时回调走的是application:didFinishLaunchingWithOptions:链接信息在launchOptions里的UIApplicationLaunchOptionsURLKeyURL Scheme或者UIApplicationLaunchOptionsUserActivityDictionaryKeyUniversal Links。当 App已经在后台或前台运行被链接热启动时URL Scheme 走application:openURL:options:Universal Links 走application:continueUserActivity:restorationHandler:。这两个入口必须都处理否则就会出现冷启动能拿到参数热启动拿不到或者反过来的问题。我见过有团队只处理了热启动的回调结果用户从完全关闭状态点链接进来参数全丢了。4.2 Unity 与原生通信的几种方式Unity 和 iOS 原生通信常见的有这么几种方式DllImport 直接调用原生函数在 C# 里用[DllImport(__Internal)]声明外部函数原生侧用 C 风格导出。这种方式直接、性能好适合简单的函数调用。UnitySendMessage原生侧通过UnitySendMessage(GameObjectName, MethodName, message)向 Unity 发消息。这是最常用的方式简单可靠但只能传字符串且 GameObject 必须存在。回调函数指针C# 侧把委托转成函数指针传给原生原生在合适时机回调。适合需要异步返回结果的场景。对于 Deep Link 参数投递我推荐组合使用原生层收到链接后先把参数缓存起来然后通过UnitySendMessage通知 Unity 层来取。为什么要缓存因为冷启动时原生回调触发得比 Unity 场景初始化早如果直接发消息Unity 侧的 GameObject 可能还没创建消息就丢了。4.3 冷启动参数丢失的经典问题与解法这是 Deep Link 接入里最高频的坑。冷启动的时序大致是系统拉起 App → 原生didFinishLaunchingWithOptions触发 → Unity 引擎初始化 → 场景加载 → GameObject 创建。原生回调发生在最前面而UnitySendMessage需要目标 GameObject 已经存在。解法就是原生侧先存Unity 侧主动取。具体做法是原生层用一个静态变量或者单例把链接参数存下来同时提供一个导出的 C 函数供 C# 调用比如GetLaunchDeepLinkParams。Unity 侧在场景初始化完成后主动调用这个函数把参数取回来。如果是热启动原生层在收到链接时既缓存又发消息Unity 侧收到消息后处理同时也可以再取一次缓存做去重。这里要注意去重。冷启动时如果既走了主动取又因为某种原因收到了消息参数可能被处理两次。我的做法是给每个链接生成一个唯一标识比如时间戳加随机数Unity 侧记录已处理的标识重复的直接忽略。5. C# 层的参数解析与业务分发5.1 链接参数的解析拿到原始链接字符串之后第一步是解析。URL Scheme 的链接形如mygame://open/activity?actId123fromadUniversal Links 形如https://link.example.com/open/activity?actId123fromad。两者的解析逻辑可以统一先取出 path 部分判断业务类型再解析 query 参数。C# 里可以用System.Uri来解析但要注意 Unity 的某些平台对System.Uri的支持有差异。更稳妥的做法是自己写一个轻量解析器按?分割出 query 部分再按和拆分键值对最后做 URL 解码。URL 解码这一步千万别省中文参数或者特殊字符如果不解码业务层拿到的就是乱码。public static Dictionarystring, string ParseQuery(string url) { var result new Dictionarystring, string(); int qIndex url.IndexOf(?); if (qIndex 0) return result; string query url.Substring(qIndex 1); foreach (var pair in query.Split()) { if (string.IsNullOrEmpty(pair)) continue; int eq pair.IndexOf(); if (eq 0) continue; string key Uri.UnescapeDataString(pair.Substring(0, eq)); string val Uri.UnescapeDataString(pair.Substring(eq 1)); result[key] val; } return result; }5.2 业务路由的设计解析出参数之后需要根据 path 或者某个特定参数把用户路由到对应页面。我建议设计一个路由表把 path 映射到具体的处理函数而不是写一堆 if-else。这样后续新增活动入口时只需要注册一条新路由不用改动核心逻辑。路由表大概长这样path业务含义关键参数/open/activity打开活动页actId, from/open/gift领取礼包giftCode/open/friend好友邀请inviterId/open/notice公告详情noticeId路由分发的时候要注意时机问题。如果链接指向的是某个需要登录后才能访问的页面而用户此时还没登录就不能直接跳转得先把目标记下来等登录完成后再执行。这个延迟执行的机制在召回场景里特别重要因为召回用户往往需要重新登录。5.3 参数投递的完整时序把整条链路串起来冷启动的时序是这样的系统拉起 App → 原生didFinishLaunchingWithOptions收到链接 → 原生缓存参数 → Unity 引擎初始化 → 场景加载 → 游戏启动逻辑调用GetLaunchDeepLinkParams取参数 → 解析 → 判断登录状态 → 已登录直接路由未登录则暂存 → 登录完成后执行暂存的路由。热启动的时序是原生openURL或continueUserActivity收到链接 → 原生缓存并UnitySendMessage→ Unity 侧收到消息 → 取参数 → 解析 → 路由。热启动时游戏通常已经登录直接路由即可但也要处理当前正在某个界面跳转前是否需要关闭弹窗这类细节。6. 常见问题与排查技巧实录6.1 高频问题速查表问题现象可能原因排查方向点击链接没反应Scheme 未注册或 AASA 未生效检查 Info.plist 和 AASA 部署冷启动拿不到参数原生回调早于 Unity 初始化改为原生缓存、Unity 主动取热启动参数重复处理缓存和消息双通道未去重加唯一标识去重参数中文乱码未做 URL 解码解析时调用解码函数Universal Links 时好时坏AASA 缓存或 CDN 重定向检查缓存策略和响应头从某些 App 内点链接无效该 App 内嵌浏览器限制用 Scheme 兜底6.2 几个踩过的坑坑一AASA 文件路径写错。必须是.well-known/apple-app-site-association很多人写成.well-known/apple-app-site-association.json多了后缀系统就不认。而且这个文件不能有 BOM 头用某些编辑器保存时可能自动加了 BOM导致解析失败。坑二UnitySendMessage 的 GameObject 名字写错。这个方法是通过字符串找 GameObject 的名字大小写敏感写错了消息就静默丢失没有任何报错。建议把 GameObject 名字定义成常量原生和 C# 两侧共用。坑三测试环境用模拟器。模拟器对 Universal Links 的支持不完整很多问题在模拟器上根本复现不出来。Deep Link 相关的测试一律用真机而且要覆盖冷启动、热启动、后台唤醒三种状态。坑四忽略了链接的时效性。活动链接往往有有效期如果用户点了过期链接业务层要给出友好提示而不是直接报错或者跳到空白页。这个体验细节容易被忽略但直接影响用户对活动的观感。6.3 调试技巧原生层的日志可以用 Xcode 的 Console 查看重点看didFinishLaunchingWithOptions和openURL有没有被触发。Unity 侧的日志走Debug.Log在 Xcode Console 里也能看到。如果怀疑参数在跨层传递时丢了可以在原生侧打印原始链接在 C# 侧打印解析后的字典两头一对比就知道问题出在哪一段。另外Universal Links 的调试可以用苹果的诊断工具输入你的域名和路径它会告诉你 AASA 文件是否可访问、格式是否正确、路径是否匹配。这个工具能省掉大量瞎猜的时间。7. 一些实战经验补充关于参数投递的时机我再补充一个细节。有些团队为了图省事在原生层收到链接后直接UnitySendMessage然后 Unity 侧在Awake里注册监听。但Awake的执行顺序是不确定的如果消息在监听注册之前就发出来了照样丢。所以更稳的做法是监听注册放在尽可能早的地方比如用一个RuntimeInitializeOnLoadMethod标记的静态方法它在场景加载前就会执行能最大程度保证不漏消息。还有一点是关于多场景的。如果游戏有多个 Unity 场景Deep Link 的处理逻辑不要散落在各个场景里最好做成一个常驻的单例用DontDestroyOnLoad保持。这样无论用户在哪个场景链接参数都能被正确处理。最后说下测试覆盖。Deep Link 的测试用例至少要有这些冷启动带参数、热启动带参数、后台唤醒带参数、无参数正常启动、参数缺失关键字段、参数包含特殊字符、链接过期、未登录状态点链接。把这些场景都跑一遍基本就能覆盖绝大多数线上问题了。