资讯动态

Unity iOS Deep Link实战:URL Scheme与Universal Links参数投递C#层

发布时间:2026/9/28 9:10:56 来源:尧图企业网站定制
1. 为什么手游团队绕不开 Deep Link 这件事做过手游投放或者运营的兄弟应该都有体会买量成本一天比一天高用户点进来之后如果还要手动去 App Store 搜名字、下载、打开、再找房间这个漏斗每多一步就掉一层人。尤其是那种朋友分享一个房间链接你点一下就能直接进同一个房间的场景如果做不到体验直接掉一个档次。Unity 做的 iOS 手游要接 Deep Link本质上就是解决从 App 外部把用户精准送进 App 内部某个具体页面的问题同时把链接上带的参数房间号、邀请码、渠道标识、活动 ID一路传到 C# 业务层。标题里提到的两个关键词——URL Scheme和Universal Links——是 iOS 上实现这件事的两条主要路径。前者是老牌方案配置简单但体验有瑕疵后者是苹果主推的方案体验好但配置链路长、坑也多。而参数投递到 C# 层这一步恰恰是很多 Unity 项目最容易翻车的地方原生层明明收到了链接C# 层却拿不到或者冷启动和热启动两条路径行为不一致参数丢失、重复触发、时序错乱各种问题层出不穷。这篇内容适合两类人看一类是 Unity 客户端开发需要把 Deep Link 从原生层打通到业务逻辑层另一类是负责投放、分享、活动页的技术同学需要理解链接到底是怎么被系统识别、被 App 接收的。我会把整条链路拆开讲——从 iOS 系统怎么识别链接到 Unity 原生插件怎么写再到 C# 层怎么安全地接收参数中间穿插我自己踩过的坑和实测有效的处理方式。看完你应该能直接照着搭一套能跑通的方案而不是停留在知道有这么个东西的层面。2. URL Scheme 与 Universal Links 的选型逻辑2.1 两种方案到底差在哪先把概念说清楚不然后面配置容易懵。URL Scheme是给 App 注册一个自定义协议头比如mygame://。系统看到这个开头的链接就知道该交给哪个 App 处理。它的优点是配置极其简单Xcode 里加一项就行任何地方浏览器、其他 App、短信都能唤起。缺点也很明显任何 App 都能注册同一个 Scheme存在被劫持的风险在 Safari 里如果当前页面已经打开过你的 App再点同 Scheme 链接可能没反应而且从 iOS 的一些版本开始在部分场景下会弹一个是否打开的确认框体验割裂。Universal Links是苹果后来推的方案用的是标准的https://链接比如https://game.example.com/invite?room123。它的原理是你在 App 里声明自己信任某个域名同时在那个域名的服务器上放一个apple-app-site-association简称 AASA文件声明哪些路径归这个 App 处理。系统在安装 App 时会去拉这个文件并缓存。用户点链接时如果装了 App 就直接进 App没装就正常打开网页。体验上几乎无感也不会弹确认框安全性也好得多。我一般给团队的建议是能上 Universal Links 就上 Universal LinksURL Scheme 作为兜底。原因很实际——投放落地页、分享卡片这些场景用户大概率是在 Safari 或者微信内置浏览器里点的Universal Links 的转化路径最短。但有些老系统版本、某些第三方浏览器对 Universal Links 支持不完整这时候 Scheme 兜底能救一命。2.2 选型时容易忽略的三个现实因素第一个因素是域名归属。Universal Links 要求你把 AASA 文件放在一个 HTTPS 域名的根目录或者.well-known目录下这个域名最好是你自己能控制的。如果公司市场部用的是第三方短链服务那 Universal Links 基本没法配只能退回 Scheme。所以立项阶段就要跟运营确认清楚链接是谁生成的。第二个因素是微信生态的限制。微信内置浏览器对 Universal Links 的支持一直比较微妙不同版本行为不一致。很多团队的做法是在微信里引导用户点右上角在浏览器中打开或者干脆用 Scheme 唤起。这块没有银弹得实测。第三个因素是冷启动和热启动的差异。冷启动指 App 没在后台运行点链接直接拉起热启动指 App 已经在后台点链接把它切到前台。这两条路径在 iOS 原生层的回调入口是不一样的如果只处理了一条就会出现第一次能进第二次没反应的诡异现象。这个后面会详细讲。3. iOS 原生层的配置与回调入口3.1 URL Scheme 的配置步骤在 Unity 导出的 Xcode 工程里找到Info.plist添加URL Types。用 Xcode 图形界面操作的话就是选中 Target在 Info 标签页里找到 URL Types点加号Identifier 填个反向域名比如com.example.mygameURL Schemes 填mygame。这样mygame://xxx就能唤起你的 App 了。用代码或者脚本改Info.plist的话结构是这样的keyCFBundleURLTypes/key array dict keyCFBundleURLName/key stringcom.example.mygame/string keyCFBundleURLSchemes/key array stringmygame/string /array /dict /array这里有个细节Scheme 名字尽量用全小写避免大小写混用带来的匹配问题。另外别用太通用的词比如game、app这种很容易跟别的 App 撞车。3.2 Universal Links 的完整配置链路Universal Links 的配置分三块缺一不可。第一块是Xcode 里的 Associated Domains。在 Target 的 Signing Capabilities 里加上 Associated Domains然后添加一条applinks:game.example.com。注意这里不要带https://也不要带路径就写域名。第二块是服务器上的 AASA 文件。放在https://game.example.com/.well-known/apple-app-site-association注意这个文件不能有.json后缀Content-Type 要是application/json。内容大概长这样{ applinks: { apps: [], details: [ { appID: TEAMID.com.example.mygame, paths: [/invite/*, /activity/*] } ] } }appID是 Team ID 加 Bundle ID中间用点隔开。paths声明哪些路径归你的 App 处理*是通配符。这里有个大坑AASA 文件在 App 安装时才会被系统拉取并缓存如果你改了文件已经装了 App 的用户不会立即生效得等系统重新拉取或者用户重装。调试阶段经常遇到我明明改了怎么没反应八成是这个缓存问题。第三块是App 内的处理入口。iOS 13 之后Universal Links 的回调走的是SceneDelegate的continueUserActivity而 URL Scheme 走的是openURLContexts。如果你的工程还是老的AppDelegate结构那入口就是application:continueUserActivity:restorationHandler:和application:openURL:options:。Unity 导出的工程默认可能没有 SceneDelegate得看具体版本。3.3 冷启动与热启动的回调差异这是最容易出问题的地方我单独拎出来说。冷启动时App 进程还没起来系统拉起 App 后链接信息会在启动完成后通过回调传进来。这时候如果你在 C# 层注册监听太晚回调可能已经过去了参数就丢了。热启动时App 在后台系统把链接信息通过回调传给已经运行的进程这时候 C# 层一般已经初始化好了接收相对容易。我的处理思路是原生层收到链接后先存到一个静态变量或者单例里不管 C# 层有没有准备好先存下来。C# 层初始化完成后主动去取一次把冷启动期间攒下的链接取走。同时原生层继续保留回调用于热启动的实时投递。这样冷热两条路径就统一了。4. Unity 原生插件与 C# 层的桥接实现4.1 原生层如何把参数传给 UnityUnity 和 iOS 原生交互主流有两种方式一种是UnitySendMessage原生层主动调用 Unity 里某个 GameObject 上的方法另一种是通过DllImport声明外部函数C# 层主动调用原生层的方法。UnitySendMessage的签名是UnitySendMessage(GameObjectName, MethodName, message)只能传字符串而且 GameObject 必须处于激活状态方法必须是单参数 string。它的优点是原生层可以主动推送适合热启动这种事件驱动的场景。缺点是如果 GameObject 没准备好消息就丢了。DllImport方式适合 C# 层主动拉取比如冷启动时去取攒下的链接。声明大概是这样#if UNITY_IOS !UNITY_EDITOR [DllImport(__Internal)] private static extern string _GetPendingDeepLink(); #endif原生层用 C 风格导出函数extern C const char* _GetPendingDeepLink() { // 返回攒下的链接字符串没有就返回空串 }注意返回字符串的内存管理Unity 这边会拷贝一份原生层返回的指针要保证在返回时有效一般用静态缓冲区或者strdup后由 Unity 侧释放实际 Unity 会自己处理拷贝用静态 buffer 最省事。4.2 C# 层的接收与分发设计C# 层我建议做一个DeepLinkManager单例职责有三块接收、解析、分发。接收部分要同时处理两条路径。一条是UnitySendMessage推过来的热启动链接另一条是启动时主动调用_GetPendingDeepLink拉取的冷启动链接。两条路径最终都汇到同一个HandleDeepLink(string url)方法里。解析部分就是把 URL 拆成 scheme、host、path、query 参数。Unity 自带的WWW或者UnityWebRequest不太适合干这个我一般手写一个轻量解析或者用System.Uri注意 iOS 上System.Uri对自定义 scheme 的解析有时会抽风得测。query 参数解析要处理 URL 编码%20这种要还原成空格。分发部分就是根据 path 或者参数里的action字段决定跳转到哪个界面。这里要注意时序如果链接指向的界面依赖某些初始化数据比如用户登录态、配置表得等这些准备好再跳否则会跳到一个空界面或者报错。我的做法是维护一个待处理链接队列业务层初始化完成后发个信号再把队列里的链接逐个消费掉。4.3 一个容易忽略的线程问题UnitySendMessage是从原生主线程调过来的而 Unity 的 C# 逻辑跑在主线程上所以直接调用一般没问题。但如果你在原生层用了异步回调比如网络请求后再处理链接回调线程可能不是主线程这时候直接UnitySendMessage会有隐患。稳妥的做法是dispatch_async(dispatch_get_main_queue(), ^{ ... })切回主线程再发。C# 这边HandleDeepLink里如果要做耗时操作比如加载场景记得用协程或者异步别阻塞主线程。我见过有人在HandleDeepLink里同步SceneManager.LoadScene结果卡顿明显用户体验很差。5. 参数投递的完整实操流程5.1 从点击链接到 C# 收到参数的时序把整条链路串起来看一次完整的 Deep Link 投递大概是这样的用户在 Safari 或某个 App 里点击https://game.example.com/invite?room123fromshareiOS 系统识别这是 Universal Link检查已安装 App 的 Associated Domains 配置匹配成功系统拉起 App冷启动或切到前台热启动原生层回调continueUserActivity被触发拿到NSUserActivity从中取出webpageURL原生层把 URL 字符串存到静态变量同时尝试UnitySendMessage通知 C# 层C# 层如果已初始化直接处理如果没初始化等初始化后主动拉取C# 层解析 URL提取room123和fromshare业务层根据参数跳转到房间界面并记录来源渠道这条链路里第 5 步和第 6 步的配合是关键。我一般让原生层维护一个pendingURL字符串UnitySendMessage发一个有新链接的通知C# 层收到通知后调用_GetPendingDeepLink取走并清空。这样即使通知丢了C# 层启动时主动拉一次也能兜住。5.2 原生层代码的关键片段Objective-C 里处理 Universal Links 的核心代码大概是这样- (BOOL)application:(UIApplication *)application continueUserActivity:(NSUserActivity *)userActivity restorationHandler:(void (^)(NSArrayidUIUserActivityRestoring * _Nullable))restorationHandler { if ([userActivity.activityType isEqualToString:NSUserActivityTypeBrowsingWeb]) { NSURL *url userActivity.webpageURL; if (url) { [self handleIncomingURL:url.absoluteString]; } } return YES; } - (void)handleIncomingURL:(NSString *)urlString { self.pendingURL urlString; UnitySendMessage(DeepLinkReceiver, OnDeepLinkReceived, [urlString UTF8String]); }处理 URL Scheme 的入口- (BOOL)application:(UIApplication *)app openURL:(NSURL *)url options:(NSDictionaryUIApplicationOpenURLOptionsKey,id *)options { [self handleIncomingURL:url.absoluteString]; return YES; }导出给 C# 调用的函数extern C const char* _GetPendingDeepLink() { NSString *url DeepLinkHandler.sharedInstance.pendingURL; if (!url) return ; DeepLinkHandler.sharedInstance.pendingURL nil; return strdup([url UTF8String]); }注意strdup分配的内存Unity 侧会拷贝但严格来说这块内存需要释放。实际项目中为了省事很多人用静态 buffer但静态 buffer 有长度限制和线程安全问题。如果链接不长一般不会超过 2KB静态 buffer 够用。5.3 C# 层的接收代码using UnityEngine; using System.Runtime.InteropServices; public class DeepLinkManager : MonoBehaviour { public static DeepLinkManager Instance { get; private set; } private string _pendingUrl; #if UNITY_IOS !UNITY_EDITOR [DllImport(__Internal)] private static extern string _GetPendingDeepLink(); #endif void Awake() { if (Instance ! null) { Destroy(gameObject); return; } Instance this; DontDestroyOnLoad(gameObject); } void Start() { // 冷启动主动拉取 PullPendingDeepLink(); } // 热启动由原生层 UnitySendMessage 调用 public void OnDeepLinkReceived(string url) { HandleDeepLink(url); } private void PullPendingDeepLink() { #if UNITY_IOS !UNITY_EDITOR string url _GetPendingDeepLink(); if (!string.IsNullOrEmpty(url)) { HandleDeepLink(url); } #endif } private void HandleDeepLink(string url) { Debug.Log($[DeepLink] Received: {url}); var parsed DeepLinkParser.Parse(url); DeepLinkDispatcher.Dispatch(parsed); } }这个DeepLinkReceiver这个 GameObject 名字要和原生层UnitySendMessage里的名字一致而且它必须在场景里存在且激活。我一般把它挂在一个DontDestroyOnLoad的管理器上确保全程都在。5.4 参数解析的细节处理URL 解析看起来简单实际有不少坑。比如mygame://invite?room123fromshare和https://game.example.com/invite?room123fromshare前者是 Scheme后者是 Universal Link解析逻辑要能同时兼容。query 参数的解析要注意值可能经过 URL 编码%E6%88%BF%E9%97%B4这种要还原成中文参数可能重复比如tagatagb得决定是取第一个还是全部可能没有值比如?debug这种一般当作debugtrue处理。我写过一个简单的解析器核心逻辑是先按?拆出 query 部分再按拆成键值对每个键值对按第一个拆然后对 key 和 value 分别做 URL 解码。URL 解码用System.Uri.UnescapeDataString但要注意它对的处理有些场景代表空格有些代表加号本身得根据你的链接生成规则来定。6. 常见问题与排查技巧实录6.1 Universal Links 不生效的排查顺序这是被问得最多的问题。我整理了一个排查顺序基本能覆盖九成情况排查项检查方法常见问题AASA 文件可访问性浏览器直接访问https://域名/.well-known/apple-app-site-association404、返回 HTML、Content-Type 不对AASA 文件格式用 JSON 校验工具检查多了.json后缀、JSON 语法错误appID 是否正确对比 Team ID 和 Bundle IDTeam ID 写错、Bundle ID 大小写不一致Associated Domains 配置Xcode 里检查 Capabilities域名带了https://、拼写错误系统缓存卸载重装 App改了 AASA 但系统没重新拉取链接路径匹配对比 paths 配置和实际链接通配符没覆盖到、路径大小写问题其中系统缓存这条最坑。苹果的 AASA 缓存机制不透明有时候几分钟就更新有时候要等很久。调试阶段最可靠的办法就是卸载重装别浪费时间在等缓存上。6.2 参数丢失的几种典型场景场景一冷启动参数丢失。原因是 C# 层注册监听太晚原生层的UnitySendMessage发出去没人接。解决办法就是前面说的原生层存 pendingURLC# 层启动后主动拉。场景二热启动重复触发。原因是原生层既发了UnitySendMessageC# 层又主动拉了一次同一条链接处理了两遍。解决办法是拉取后清空 pendingURL并且加一个去重逻辑比如记录最近处理过的 URL 和时间戳短时间内相同的 URL 忽略。场景三参数被截断。原因是链接里有特殊字符没编码比如、#、空格。生成链接时一定要对参数值做 URL 编码#尤其要注意它后面的内容会被当成 fragment不会传给服务端也不会进 query。场景四中文乱码。原因是编码不一致原生层用 UTF-8C# 层按其他编码解。统一用 UTF-8解析时先UnescapeDataString再使用。6.3 实操心得与避坑建议第一条心得在原生层加详细日志。NSLog把收到的原始 URL、解析出的参数、调用UnitySendMessage的时机都打出来。C# 层也打日志。两边日志对不上问题就定位了一半。我见过太多人只在一端打日志然后靠猜。第二条心得做一个测试用的链接生成页面。手动拼链接容易出错写个简单的 HTML 页面输入参数自动生成正确编码的链接测试效率高很多。这个页面还能顺便验证 Universal Links 的路径匹配。第三条心得区分测试环境和生产环境的域名。测试用test.game.example.com生产用game.example.comAASA 文件分别配置。别在测试环境用生产域名容易污染缓存也容易误触发线上逻辑。第四条心得注意 App 首次安装后的首次启动。有些系统版本在 App 首次安装后AASA 文件还没拉取完成这时候点 Universal Link 可能不生效。这不是 bug是系统行为。测试时先正常启动一次 App再测链接。第五条心得微信、QQ 等第三方 App 内的链接行为要单独测。这些 App 的内置浏览器对 Universal Links 的支持各不相同有的直接拦截有的需要用户手动操作。别假设在 Safari 能跑就行。7. 链接安全与参数校验的补充说明Deep Link 的参数是外部输入的绝对不能直接信任。我见过有项目直接把链接里的roomId拿去请求服务器结果被人构造恶意参数刷接口。基本的校验包括参数类型校验数字就是数字别拿字符串直接拼 SQL 或 URL、长度限制防止超长参数打爆内存、白名单校验action字段只允许预定义的几个值。另外如果链接里带了敏感操作比如加好友、领奖励最好加一个签名机制。服务端生成链接时对参数做签名App 收到后校验签名防止参数被篡改。签名算法用 HMAC 就行密钥存在 App 里虽然理论上能被逆向但能挡住大部分低级攻击。还有一点Universal Links 的 AASA 文件里paths尽量写精确别用*通配所有路径。通配所有路径意味着你域名下任何链接都会尝试唤起 App如果用户点了你官网的其他页面链接也会被拉进 App体验很怪。8. 后续可以扩展的方向这套方案跑通之后还有几个可以继续深挖的点。一个是延迟深度链接Deferred Deep Link解决用户没装 App点了链接去下载装完打开后还能恢复到原来那个页面的问题。这个需要配合服务端做设备指纹或者剪贴板匹配复杂度高不少但转化价值很大。另一个是多平台统一Android 的 App Links 和 iOS 的 Universal Links 配置逻辑类似但细节不同可以抽象一层统一的链接处理层业务层不用关心平台差异。还有就是链接的埋点与归因把 Deep Link 的来源、参数、转化路径都记录下来对投放优化很有帮助。我在实际项目里踩过的坑基本都写进来了尤其是冷热启动参数丢失和 AASA 缓存这两个几乎每个接 Deep Link 的团队都会遇到。照着这个流程走能省不少调试时间。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑