ARTICLE DETAIL

资讯详情

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

Unity手游iOS深链全解析:从URL Scheme到C#参数投递

Unity手游iOS深链全解析:从URL Scheme到C#参数投递 用户点进一条广告链接iPhone 上却什么也没发生App 没弹起来浏览器停在落地页运营第二天拿着漏斗数据找过来转化掉了将近一半。这种问题我见过太多次。做 Unity 手游的 iOS 客户端越久越清楚Deep Link 这件事表面上归 iOS 原生层管但真正要做到召回、买量、分享回流这些链路不丢用户就必须从 URL Scheme / Universal Links 的注册开通、系统验证、原生回调一路打通到 C# 层的参数投递。中间任何一环偷懒用户就会在某个静默的角落流失。这篇文章以 Unity 手游为背景完整走一遍 iOS 拉起 App、C# 收到深链参数并派发到业务界面的全过程包含原生侧配置、AASA 校验、冷启动和热启动两种时序的处理姿势以及我在项目里真实踩过的坑适合正在接深链、或者准备做用户回流和买量归因的朋友参考。1. 一条分享链接决定生死Deep Link 到 C# 层到底解决了什么问题1.1 三个真实场景没有好的唤醒链路会怎样先说最常见的买量归因场景。用户在抖音、快手或者广告联盟里看到一条素材点了一下广告平台会带着一串投放参数跳到一个落地页。如果这个落地页能直接拉起 App并且把 campaign、adset、creative 这些参数传给 C# 层客户端就能在用户进来后上报归因渠道方就能确认这个用户是它带来的。没有深链用户点了广告后只能先去 App Store 下载装完一打开就是空白首页谁也不记得被哪条广告勾来的。投放侧的数据一旦断了买量优化就变成了盲人摸象。第二个是社交分享和邀请。玩家把“帮我砍一刀”或者“加入公会领钻石”的链接甩到微信群里好友点开如果系统没有把 link 里的邀请人 ID 透传给游戏内逻辑新用户注册后就没法自动绑定关系运营包里的“邀请有礼”就只能靠玩家手动填写邀请码。手动填写这一步转化率基本要打三折。第三个是 Push 和邮件召回。运营发一条“七日回归福利”的推送用户点开通知理想情况是直接回到游戏内任务中心并且把这条 push 的 campaign 号带进来。如果深链参数没法送到 C# 层客户端就只能弹首页用户还要自己找半天入口。别小看这一步连续两次点进去都是首页第三轮召回邮件基本就没人打开。这三个场景的共同点是把“外部世界的一个意图”翻译成“App 内部的一个入口动作”。说白了就是用户在外面点了一个链接系统需要知道启动哪个 AppApp 需要知道进去之后干什么。深链做得好的产品用户从点击到看到指定界面能在三秒内完成做得差的用户点完链接还要经历弹窗确认、落地页跳转、App 首页漫无目的地划拉流失就是这么一点点磨掉的。1.2 先理清需求边界你需要的到底是“唤起到站”还是“参数到界面”在动手之前我建议团队先把需求拆成两个层级。层级 A只要唤起。用户点链接App 能打开就行进去之后统一走首页不关心上一跳是谁。这个最简单配一个 URL Scheme 或者 Universal Links 就完事了。层级 B参数到界面。不仅要把 App 拉起来还要把落地页里携带的邀请人 ID、物料编号、活动码、来源渠道等参数解析出来转给游戏内某个界面直接跳转或弹窗。绝大多数手游做到后面都需要层级 B。原因很现实买量归因平台、分享系统、运营后台都会往链接后面追加参数如果客户端只实现了“唤起不读参数”等于只修了一半的桥。与其以后返工不如一开始就把“收到原始 URL、解析参数、按参数路由”这条链路搭好。另外要注意的是深链方案的复杂度和你的浏览器投放矩阵强相关只投 Safari 和海外渠道Universal Links 比较省心如果国内微信、抖音、QQ 大量内嵌浏览器分享那就不能只靠 Universal Links通常还要保留一套 URL Scheme 降级兜底。这个取舍后面细说。2. URL Scheme 和 Universal Links 在 iOS 上的运转逻辑以及怎么选2.1 URL Scheme老牌方案快但糙URL Scheme 的原理和你在电脑上输入mailto:会唤起邮件客户端差不多。iOS 有一套已注册的协议表每个 App 通过 Info.plist 里的CFBundleURLTypes声明自己可以处理哪些协议。系统收到一个以mygame://开头的链接时会去协议表里找哪个 App 声明过这个协议找到就把它拉起来然后把整条 URL 作为参数交给 App。这个方案的好处是接入成本极低。Unity 工程里在 Player Settings 填几个 scheme构建出来就能用不需要服务器不需要域名甚至连苹果开发者后台都不用额外配置。缺点是体验有限在 Safari 里通过网页 JS 跳转 scheme 时iOS 会弹一个“此 App 想要打开”的确认框多一次点击就多一批流失。微信、QQ 这类内嵌浏览器对 scheme 跳转拦截得很凶经常要用户点右上角“在浏览器打开”链路非常曲折。如果 scheme 和系统协议或其它 App 冲突匹配行为不可控。即便如此URL Scheme 在手游里依然是兜底方案。理由很朴素Universal Links 在国内部分 webview 里并不万能关键时刻还得靠 scheme 硬拉一次虽然带着一个确认弹窗总比完全拉不起来强。2.2 Universal Links苹果亲儿子验证链路长Universal Links 是 iOS 9 之后苹果推荐的深链方案。它的思路是你的域名和你的 App 做一次绑定以后用户在 Safari 或其它支持的系统组件里点开这个域名下的某条 HTTPS 链接时如果手机上装了你的 App系统直接无缝唤起 App不弹确认框如果没装就正常打开网站落地页。这个“顺滑降级”能力对买量尤其友好——已安装用户直接进游戏未安装用户看网页不会被一个弹窗卡在中间。要让 Universal Links 生效需要满足三个条件Xcode 工程里为 App 开启 Associated Domains并写好applinks:这个域名关联。苹果开发者后台里这个 App ID 必须也开启 Associated Domains capability。域名根目录的https://你的域名/.well-known/apple-app-site-association位置放一个 AASA 文件文件里写明 team ID 和 bundle ID以及哪些路径允许唤起 App。这套东西的所有验证都在苹果服务器和系统内部完成链路长出问题的时候排查起来也比 scheme 麻烦。首次部署时系统还会要求用 Safari 实际打开一遍域名才能触发 AASA 下载很多新人在这一步就把时间耗掉了后面我会专门写排查顺序。2.3 选型建议按产品和预算判断我自己项目的经验绝大多数手游最终都会是两条腿走路Universal Links 做主力体验URL Scheme 做降级兜底。下面这个对比表可以帮助团队快速拍板对比维度URL SchemeUniversal Links接入成本低一个 scheme 字符串高需要域名、HTTPS、AASA、entitlements用户体验有确认弹窗部分 webview 拦截无感唤起直接进 App未安装 App无处理能力通常只报错流畅回落网页归因参数携带可以但容易丢完整且系统层面更稳定国内微信/抖音 webview经常拦截同样经常拦截需要 scheme 兜底排查成本低高AASA 和 entitlements 经常出问题如果项目只是想做一个简单的“从邮件里点链接进首页”那就只配 URL Scheme半天搞定。如果要做买量归因、活动分享、Push 跳转这些正经场景我建议直接上 Universal Links Scheme 双通道因为归因平台比如 AppsFlyer、Adjust的 iOS 启动方案基本都要求 Universal Links你绕不开。3. iOS 工程侧配置实操从 Info.plist 到 AASA 部署3.1 URL Scheme 配置Unity 侧最省事的做法说到配置先澄清一个容易踩的坑Unity 项目构建 Xcode 工程后Info.plist每次都会重新生成你如果每次构建完手动去 Xcode 里改 CFBundleURLTypes下一次构建又会被覆盖早晚有一天忘记。最稳的做法是直接在 Unity 的 Project Settings 里做。在 Unity 打开Edit Project Settings Player切到 iOS 页签找到Other Settings Supported URL schemes把你想要的协议名填进去多个协议用英文逗号分隔例如mygame,mygamepay构建之后生成的 Xcode 工程 Info.plist 里就会出现下列内容keyCFBundleURLTypes/key array dict keyCFBundleURLName/key stringcom.yourcompany.yourgame/string keyCFBundleURLSchemes/key array stringmygame/string stringmygamepay/string /array /dict /array这里有个细节scheme 建议全部用小写字母iOS 匹配虽说不区分大小写但某些第三方 SDK 解析 URL 的时候会按大小写敏感处理与其后面排查玄学不如一开始统一小写。另外不要用http、https、tel、sms这类已被系统或系统应用占用的协议注册了也没用。3.2 Universal Links 配置Associated Domains 与 AASA 文件Universal Links 的配置比 scheme 繁琐我给你按步骤走一遍。第一步到苹果开发者后台找到你的 App ID勾选 Associated Domains capability。这个如果没开Xcode 里配了关联域名也白搭而且这个选项改动后需要重新下载配置文件或者直接重新生成 provisioning profile真机调试时要注意。第二步在 Xcode 工程能力里添加关联域名。选中 target切到 Signing Capabilities点添加 Associated Domains填写applinks:yourgame.com注意是applinks:前缀后面跟你的域名不需要加https://。加了这条之后Xcode 会自动生成 entitlements 文件里面长这样keycom.apple.developer.associated-domains/key array stringapplinks:yourgame.com/string /array如果项目使用 CI 打包建议把 entitlements 文件纳入版本管理并在构建脚本里做校验避免开发者本地 Xcode 因为签名配置不同把这条能力弄丢。第三步部署 AASA 文件。把下面的 JSON 存成apple-app-site-association放到域名根目录下的.well-known文件夹里{ applinks: { apps: [], details: [ { appID: ABCDE12345.com.yourcompany.yourgame, paths: [*] } ] } }appID的格式是团队ID.bundleID团队 ID 在苹果开发者后台的 Membership 页面找bundle ID 是 App 的准确标识这两个拼写错任何一个Universal Links 都起不来而且不会有任何直观报错。paths决定域名下哪些路径可以唤起 App。[*]最粗暴所有的路径都能唤起也可以精确限定比如[/gift/*, /invite/*]不在列表里的路径就让 Safari 正常开网页。我建议线上正式环境用精确路径开发环境先放通配符方便排查。文件命名有讲究apple-app-site-association没有.json后缀服务器 MIME 类型应该返回application/json文件大小不能超过 150KB而且整个域名必须是 HTTPS不能带重定向链。很多 CDN 默认会 301 跳转http://或跳转到带www.的域名iOS 对 AASA 的重定向容忍度很差最好让运维把 HTTPS 直连和常见后缀都配好。3.3 验证手段curl、Safari 与模拟器 openurl配置完以后别急着写 C#先确认系统能不能拿到 AASA 文件。在终端直接执行curl -i https://yourgame.com/.well-known/apple-app-site-association看返回码是不是 200Content-Type 是不是application/json内容里 appID 和 paths 是否符合预期。如果返回 404、301 或者内容被 JS 包裹就要先解决服务器问题。接着在真机上做一次“首次激活”。用 Safari 地址栏手动输入https://yourgame.com/gift?codeSUMMER2024这一步的目的不是测试跳转而是触发系统下载这个域名的 AASA。第一次访问之后系统才真正知道你这个域名和 App 是绑定的之后你再通过其它入口打开同域名链接才有机会走 Universal Links。开发期这个顺序特别容易漏很多人配置全对就是因为没有先让 Safari 访问过域名结果一直拉不起来。模拟器调试的话可以直接用 simctl 命令唤起某个 scheme 或 universal linkxcrun simctl openurl booted mygame://open?pagegiftcodeSUMMER2024 xcrun simctl openurl booted https://yourgame.com/gift?codeSUMMER2024缺点是模拟器对 Universal Links 的兼容偶尔有玄学AASA 下载有时不触发所以我更推荐真机作为最终验收环境。4. Unity C# 层桥接冷启动、热启动与参数交付4.1 官方内置 API 的完整使用姿势很多教程会教你写 Objective-C 代码重写UnityAppController去拦截application:openURL:options:那次 iOS 原生层的正统做法但对 Unity 开发者来说从 Unity 2018.2 开始官方已经把这件事封装好了。你只需要关心 C# 里两个 APIApplication.absoluteURLApp 启动时携带的完整 URL冷启动场景用。Application.deepLinkActivated深链触发事件热启动和 Universal Links 唤起时会被调用。我的做法是写一个专门的DeepLinkManager单例把所有深链 URL 收进来再统一抛给业务层。下面这个代码可以直接放到 Unity 工程里跑using System; using System.Collections.Generic; using UnityEngine; public sealed class DeepLinkManager : MonoBehaviour { public static event Actionstring DeepLinkReceived; private static readonly Queuestring PendingUrls new Queuestring(); private static bool _isReady; private void Awake() { Application.deepLinkActivated OnDeepLinkActivated; // 冷启动时如果系统已经把 URL 给了 Unity string coldStartUrl Application.absoluteURL; if (!string.IsNullOrEmpty(coldStartUrl)) { EnqueueUrl(coldStartUrl); } } private void Start() { // 场景已经初始化可以开始派发 _isReady true; FlushPendingUrls(); } private void OnDeepLinkActivated(string url) { EnqueueUrl(url); } private void EnqueueUrl(string url) { if (_isReady) { DeepLinkReceived?.Invoke(url); } else { PendingUrls.Enqueue(url); } } private void FlushPendingUrls() { while (PendingUrls.Count 0) { string url PendingUrls.Dequeue(); DeepLinkReceived?.Invoke(url); } } }这段代码看着简单但解决了两个关键问题第一冷启动和热启动两个路径都收录第二URL 到达和场景初始化完成这两个事件并不保证先后用队列兜底避免 URL 到了但 UI 还没建好导致参数丢失。业务层只需要订阅DeepLinkReceived事件就够了不用关心到底从哪条链路来的。4.2 参数暂存与延迟派发别让 UI 还没建好就收参数深链参数丢失最隐蔽的场景是冷启动时系统唤起 AppUnity 引擎初始化、首个场景加载、业务模块初始化这三件事几乎是并行的。如果拿到 URL 的瞬间就去FindObjectOfType找某个弹窗组件大概率拿到 null然后你的代码就静默 return 了日志里什么都没有运营那边看到的反馈就是“点链接进游戏什么都没有”。所以一定不要在你的业务回调里直接操作未初始化的 UI。稳妥的做法是收到 URL 后先解析、先缓存等核心模块登录、活动系统、消息中心都启动完毕再统一派发解析时如果发现参数格式不对要打日志不要吞掉。我见过团队把深链逻辑写在某个界面的OnEnable里结果那个界面还没跳到参数已经过去了白接。更合理的做法是干脆为深链做一层“路由表”比如void HandleDeepLink(string url) { DeepLinkData data DeepLinkParser.Parse(url); if (data null) { Debug.LogError($[DeepLink] 无法解析: {url}); return; } switch (data.Page) { case gift: // 等礼包系统初始化后弹出 GiftManager.Instance.OpenGiftPanel(data.Code); break; case shop: ShopManager.Instance.OpenItemDetail(data.ItemId); break; default: Debug.Log($[DeepLink] Unknown page: {data.Page}); break; } }这里有几个运营侧的习惯要注意。很多链接里的参数是中文、甚至有 emoji直接Uri解析出来可能是乱码所以要统一做一次 URL decode。我一般用WWW.UnEscapeURL或UnityWebRequest.UnEscapeURL和 iOS 原生传给 Unity 的转义格式是对应的。4.3 参数解析与统一协议把 scheme 和 UL 收敛成同一种 URL双通道方案带来的一个麻烦是URL Scheme 长这样mygame://open?pagegiftcodeSUMMER2024Universal Links 长这样https://yourgame.com/gift?codeSUMMER2024。如果业务层分别解析两种格式写出来的代码会越来越乱而且渠道方一换参数又得改。我推荐的做法是在 C# 层做一个“统一深链协议”。无论进来是什么都先抽成 页面名 参数字典再往下传入站 URL页面名参数mygame://open?pagegiftcodeSUMMER2024open{ page: gift, code: SUMMER2024 }https://yourgame.com/gift?codeSUMMER2024/gift{ code: SUMMER2024 }然后内部再定义一个映射关系把/gift、pagegift、gift这些不同表达全部归一成page_type gift。这样后续不管买量平台扔过来什么奇形怪状的 URL客户端只要改映射表不动业务代码。具体解析可以直接用 .NET 的Uriusing UnityEngine; public static class DeepLinkParser { public static DeepLinkData Parse(string rawUrl) { string decoded UnityEngine.Networking.UnityWebRequest.UnEscapeURL(rawUrl); Uri uri new Uri(decoded); string page uri.Host; string query uri.Query; // scheme 格式: mygame://gift?codexxx if (uri.Scheme mygame || uri.Scheme mygamepay) { page uri.Host; } else if (uri.Scheme https) { // universal link 格式: https://yourgame.com/gift?codexxx page uri.AbsolutePath.TrimStart(/); } var data new DeepLinkData { Page page, Query uri.Query }; return data; } } public class DeepLinkData { public string Page; public string Query; public string GetQueryValue(string key) { // 用 Uri 解析 query 中的 key-value string cut Query.TrimStart(?); foreach (string pair in cut.Split()) { string[] kv pair.Split(); if (kv.Length 2 kv[0] key) { return UnityEngine.Networking.UnityWebRequest.UnEscapeURL(kv[1]); } } return null; } }如果项目用了 AppsFlyer 或 Adjust它们的 SDK 在自己那套回调里也会把深链 URL 透传回来。这时候要小心双路重复回调SDK 传一次 URLUnity 的deepLinkActivated又传一次业务层不幂等就会弹两次礼包。我处理的办法是给每个 URL 算一个 hash按 hash 做去重并且把归因 SDK 的回调作为主链路、系统深链回调作为备用链路优先级统一收敛到一个入口。5. 实战中绕不开的坑从“打不开”到“参数丢了”5.1 Universal Links 白屏回 Safari 的排查链路如果用户点了链接App 没被唤起反而一直停在网页里九成是 Universal Links 链路断了。这类问题第一次排查时很容易盲目我建议按下面这个顺序走基本能覆盖 80% 的原因第一检查 Xcode entitlements 里有没有applinks:yourgame.com。有时候团队交班后有人重新生成过 project这一条被丢了。第二检查苹果开发者后台 App ID 有没有开 Associated Domains。这一步的问题最隐蔽因为 Xcode 里配了 entitlements真机安装也能成功但系统拉取 AASA 时校验的就是后台这个开关。第三用 curl 看 AASA 是否可以被 iOS 无重定向访问。很多公司域名和静态资源分离运维为了方便在 CDN 上做了 301iOS 认不出来。第四确认首次 Safari 激活已经做过。Universal Links 不是配置完就立刻生效系统下载 AASA 有延迟开发期换了新设备、新系统都需要先用 Safari 打开一次域名。第五确认 appID 没有写错 team ID 和 bundle ID。这个错得也隐蔽因为旧版本的 AASA 系统会缓存你改对之后要等一段时间或者删掉 App 重装。有一套我常用的快速自测在 Xcode 里跑 App然后给系统发一条同域名的 Universal Link看控制台有没有systemState、openURL这类日志再用curl https://yourgame.com/.well-known/apple-app-site-association | jq .看 AASA 是否在线。两分钟就能判断大概是 native 侧还是服务端的问题。5.2 热启动、去重与归因参数穿透热启动是另一个高发坑。用户已经打开 App切到后台再从某个链接点进来这时候Application.absoluteURL往往不是新的深链地址必须靠deepLinkActivated事件接收。不少开发者只在Awake里读一次 absoluteURL导致热启动场景下参数永远进不来。另外归因平台的参数往往很长。比如 AppsFlyer 的 links 到端后可能变成https://yourgame.com/gift?codeSUMMER2024af_siteidxxxaf_sub1yyyC# 层拿到后需要对参数做白名单过滤只把业务字段透传给运营活动其余归因字段交给归因 SDK 消费。不要一刀切全用也不要全部丢掉。参数去重必须做。我遇到过一次比较典型的线上事故运营在后台批量发 Push点击量集中爆发某个活动弹窗被连续触发就是因为深链回调在客户端布了两条监听同一个 URL 被deepLinkActivated和归因 SDK 各上报一次业务层没有去重。补一个简单的 hash 去重就能解决private readonly HashSetstring _seenUrlHashes new HashSetstring(); void OnDeepLinkReceived(string url) { string hash url.GetHashCode().ToString(); if (!_seenUrlHashes.Add(hash)) return; HandleDeepLink(url); }5.3 调试工具与检查清单深链问题最怕两眼一抹黑。我一般会根据现象先归类再针对性调试现象优先排查项参考手段URL Scheme 点击后无反应协议名是否注册、是否有大小写问题模拟器simctl openurlURL Scheme 点击后弹确认框正常现象体验问题评估是否补 Universal LinksUniversal Links 不唤起 Appentitlements、AASA、首次 Safari 激活curl AASA Xcode 控制台日志参数进入了 C# 但乱码URL 未做 Unicode 解码UnityWebRequest.UnEscapeURL参数收到多次归因 SDK 与系统回调重复URL hash 去重冷启动后拿不到参数只读了 absoluteURL未等场景就绪参数队列延后派发微信内点击链接无法唤起内嵌浏览器拦截引导“右上角浏览器打开”或接微信开放标签开发期我还会在DeepLinkManager里加一段调试模式URL 可以通过 Unity 菜单栏模拟注入这样不依赖真机也能在编辑器里测试界面表现。6. 一些项目教训权当课后作业深链接入最容易被轻视的部分是“参数协议设计”。很多项目一开始只想着把 App 拉起来等买量平台要做活动聚合页的时候才发现链接格式各家各味C# 层解析代码写得像打补丁。所以我特别建议在立项阶段就把自家的深链链接形态固定下来不管外面怎么变内部统一成page query的结构。其次Universal Links 一旦换域名旧域名要保留一段时间的 AASA不要马上删否则线上老版本客户端的召回活动会直接断掉。我经历过一次因为域名迁移没考虑老包老版本全都点不开链接运营紧急回滚非常狼狈。最后分享一个实用小技巧在开发环境给深链加一个专用入口比如用mygame://debug/open?pagegiftcodeTEST客户端收到这个 URL 时打印完整日志并显示测试面板能让你在接归因 SDK、调试活动弹窗时省掉大量等待真机触发的时间。等线上稳定后把调试分支的日志级别关掉就行也不用删代码。深链到 C# 参数投递这条路本质上就是一条“把外界意图接进游戏内路由”的管道。管道做得越顺滑买量、回流、邀请这些业务跑起来就越省心。真机调试时一定别怕麻烦把冷启动、热启动、Safari 跳转、微信浏览器降级这几种场景反复跑几遍链路通了后面团队做事都会轻松很多。
返回列表