尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Unity手游iOS Deep Link全链路实战:URL Scheme与Universal Links配置及参数传递

Unity手游iOS Deep Link全链路实战:URL Scheme与Universal Links配置及参数传递 1. 为什么手游团队绕不开 Deep Link 这件事做过手游投放或者运营的兄弟应该都有体会买量成本一年比一年高用户点进来一次不容易结果从浏览器点开广告、跳转到 App Store、下载、首次启动这一整条链路上只要有一环断了这个用户基本就白买了。而 Deep Link 要解决的核心问题就是让用户从外部环境浏览器、短信、社交 App、H5 活动页点一个链接能精准落到 App 内部的某个具体页面而不是永远停在登录页或者首页。在 Unity 手游 iOS 这个组合里Deep Link 的落地比纯原生项目要绕一层。原因很简单iOS 系统层把链接事件交给的是原生 AppDelegate 或者 SceneDelegate而你的业务逻辑、页面跳转、参数消费全都在 C# 层。中间隔着 Objective-C / Swift 和 C# 的桥接参数怎么从 URL 一路传到 MonoBehaviour这里面的坑非常多。我见过不少团队原生那边明明收到了回调日志也打出来了但 Unity 这边就是拿不到参数最后排查半天发现是桥接函数签名对不上或者初始化时机太早回调打到了一个还没注册的监听器上。这篇内容我打算把整条链路从头到尾捋一遍URL Scheme 和 Universal Links 两种方案各自怎么配、iOS 原生层怎么接、Unity C# 层怎么收、参数怎么投递、冷启动和热启动两种场景怎么区分处理以及实际项目里踩过的那些坑。适合正在做 iOS 手游投放归因、活动页跳转、分享回流、Push 通知跳转的开发和运营同学参考。哪怕你之前没接触过 iOS 原生开发只要跟着流程走也能把这条链路搭起来。2. 两种 Deep Link 方案的本质区别与选型2.1 URL Scheme简单直接但体验有硬伤URL Scheme 是最早、最经典的方案。你在 iOS 项目的 Info.plist 里注册一个自定义协议比如mygame://然后任何地方只要打开mygame://shop?item123这样的链接系统就会尝试唤起你的 App并把完整 URL 传进来。它的优点非常明显配置简单不需要服务器配合不需要域名Android 和 iOS 逻辑基本一致测试起来也快。对于内部跳转、Push 通知、自家 App 之间的互相唤起URL Scheme 完全够用。但它的硬伤也很致命。第一如果用户没装 App点这个链接会直接报错浏览器弹一个无法打开页面用户体验极差而且这个用户就流失了。第二在微信、QQ、部分浏览器里URL Scheme 会被拦截或者需要用户手动确认转化率大打折扣。第三任何人都能注册同样的 Scheme存在被劫持的风险。所以纯靠 URL Scheme 做买量归因基本是不现实的。2.2 Universal Links体验最好但配置最重Universal Links 是苹果后来推出的方案本质上是把普通的https://链接和你的 App 绑定起来。用户点一个https://game.example.com/open?item123的链接如果装了 App 就直接进 App 并带上参数如果没装就正常打开这个网页通常是 App Store 下载页或者 H5 落地页。这个方案完美解决了 URL Scheme 的两个痛点没装 App 时不会报错而是优雅降级到网页链接是标准 https微信、浏览器都不会拦截。对于买量投放、分享回流这种场景Universal Links 几乎是唯一选择。代价就是配置复杂。你需要一个支持 HTTPS 的域名、在域名根目录放一个apple-app-site-association简称 AASA文件、在 Xcode 里开启 Associated Domains 能力、在苹果开发者后台配置 App ID 的关联域名。任何一环出错链接都不会生效而且苹果的报错信息非常少排查起来很折磨人。2.3 实际项目里的组合策略我的建议是两者都配各司其职。Universal Links 负责所有对外投放、分享、H5 回流的场景因为它体验最好、最不容易被拦截。URL Scheme 作为兜底和内部使用比如 Push 通知跳转、App 内部模块跳转、以及 Universal Links 在某些极端环境下失效时的备用方案。判断逻辑可以这样设计外部链接优先用 Universal Links如果检测到当前环境不支持比如某些 App 内置浏览器再尝试用 URL Scheme 唤起。这个降级逻辑后面会详细讲。对比维度URL SchemeUniversal Links配置复杂度低改 Info.plist 即可高需要域名 AASA 后台配置未安装 App 时报错体验差优雅降级到网页被微信/浏览器拦截经常被拦基本不拦安全性低可被劫持高域名绑定适用场景内部跳转、Push投放、分享、H5 回流3. iOS 原生层的接入与配置实操3.1 URL Scheme 的配置步骤打开 Unity 导出的 Xcode 工程找到Info.plist添加URL Types。在 Xcode 的图形界面里就是 Target - Info - URL Types点加号Identifier 填个唯一标识一般用反域名比如com.example.gameURL Schemes 填你的协议名比如mygame。配置完之后系统在收到mygame://xxx的链接时就会唤起你的 App。这里有个细节要注意协议名不要用太通用的词比如game、app这种很容易和其他 App 冲突一旦冲突系统唤起哪个是不确定的。用带品牌前缀的比如mygame、awesomegame这种。3.2 Universal Links 的完整配置链路Universal Links 的配置分四块缺一不可我按顺序说。第一块是苹果开发者后台。进入 Identifiers找到你的 App ID开启 Associated Domains 能力。这一步是前提不开的话后面 Xcode 里配了也没用。第二块是 Xcode 工程。在 Target - Signing Capabilities 里添加 Associated Domains然后添加一条applinks:game.example.com。注意这里不要带https://也不要带路径就是applinks:加域名。第三块是服务器上的 AASA 文件。这个文件必须放在域名的根目录路径是https://game.example.com/.well-known/apple-app-site-association注意没有后缀名Content-Type 必须是application/json。文件内容大概长这样{ applinks: { apps: [], details: [ { appID: TEAMID.com.example.game, paths: [/open/*, /share/*, /activity/*] } ] } }appID是团队 ID 加 Bundle ID中间用点连接。paths是允许唤起 App 的路径可以用通配符。这里有个大坑苹果对 AASA 文件的缓存非常激进你改了文件之后设备可能几天都不更新。测试阶段建议在设备上删除 App 重装或者用开发者模式强制刷新。第四块是验证。配置完之后在 iPhone 的备忘录里输入你的链接长按看有没有在 App 中打开的选项。或者用swcutil命令行工具查缓存状态。这一步能过说明配置基本没问题了。3.3 Unity 导出工程的原生代码挂载点Unity 导出的 iOS 工程入口在Classes/UnityAppController.mm老版本或者UnityAppController.mm配合AppDelegate。Deep Link 的回调有两个关键入口URL Scheme 的回调是application:openURL:options:Universal Links 的回调是application:continueUserActivity:restorationHandler:。这两个方法你需要在 UnityAppController 的子类里重写或者直接在 UnityAppController.mm 里加。我的做法是新建一个DeepLinkManager的 Objective-C 类专门处理链接解析和向 Unity 传参然后在 UnityAppController 里调用它。这样逻辑清晰也方便后续维护。不要把所有代码都堆在 UnityAppController 里那个文件本来就够乱了。4. 原生到 C# 的参数投递机制4.1 桥接函数的设计与签名约定原生向 Unity C# 传参标准做法是用UnitySendMessage。它的签名是UnitySendMessage(const char* obj, const char* method, const char* msg)三个参数分别是 GameObject 名字、方法名、参数字符串。这里有几个硬性约束必须记住。第一GameObject 必须存在且激活否则消息发不到。第二方法必须是public void且只有一个 string 参数。第三参数只能是字符串复杂结构要自己序列化一般用 JSON。我一般会约定一个固定的接收对象比如叫DeepLinkReceiver挂在场景里一个常驻的 GameObject 上方法名固定为OnDeepLink。原生那边解析完 URL 之后把参数整理成 JSON 字符串通过UnitySendMessage(DeepLinkReceiver, OnDeepLink, jsonString)发过来。4.2 冷启动与热启动的区分处理这是最容易出问题的地方。冷启动指的是 App 没运行用户点链接把它拉起来热启动指的是 App 已经在后台用户点链接把它切到前台。这两种情况下回调的时机和 Unity 的初始化状态完全不同。冷启动时application:didFinishLaunchingWithOptions:会先执行然后才是openURL或continueUserActivity。但这个时候 Unity 引擎可能还没初始化完UnitySendMessage发过去C# 那边根本没注册好监听消息就丢了。我的处理方案是原生层维护一个待处理队列。冷启动时收到的链接先存起来等 Unity 那边主动来取通过一个UnitySendMessage反向调用原生或者原生在 Unity 初始化完成的回调里再发。热启动就简单了Unity 已经跑着直接发就行。具体实现上我会在原生层加一个pendingDeepLink变量冷启动时把参数存进去。然后在 C# 层的Awake或Start里主动调用一个原生的GetPendingDeepLink方法把参数取回来。这样无论启动多快多慢参数都不会丢。4.3 参数格式的规范化设计参数传递建议统一用 JSON结构里至少包含这几个字段type链接类型比如 shop、activity、share、params业务参数键值对、source来源区分是冷启动还是热启动、rawUrl原始 URL方便排查。{ type: shop, params: {itemId: 123, from: ad}, source: cold, rawUrl: https://game.example.com/open?typeshopitemId123 }为什么要带rawUrl因为线上出问题的时候你光看解析后的参数很难判断是链接本身有问题还是解析逻辑有问题带上原始 URL 一对比就清楚了。这个字段在排查线上问题时救过我好几次。5. C# 层的接收、解析与业务分发5.1 接收器的实现与生命周期管理C# 这边我建一个DeepLinkReceiver的 MonoBehaviour挂在一个 DontDestroyOnLoad 的 GameObject 上。核心方法就是public void OnDeepLink(string json)原生发过来的消息会打到这里。public class DeepLinkReceiver : MonoBehaviour { public static event ActionDeepLinkData OnDeepLinkReceived; void Awake() { DontDestroyOnLoad(gameObject); } public void OnDeepLink(string json) { var data JsonUtility.FromJsonDeepLinkData(json); OnDeepLinkReceived?.Invoke(data); } }用事件的方式分发业务模块自己订阅接收器不关心具体业务。这样解耦之后加新的链接类型不用改接收器代码。5.2 参数解析的健壮性处理解析这块一定要做防御。URL 参数可能被篡改、可能缺字段、可能编码有问题。我的习惯是每个字段都做空值和格式校验解析失败就记日志并走默认逻辑绝不让异常往上抛导致崩溃。URL 编码也是个坑。中文、特殊字符在 URL 里是编码过的原生层解析的时候要用stringByRemovingPercentEncoding解码否则 C# 拿到的是%E5%95%86%E5%93%81这种乱码。我一般让原生层解码完再传C# 层拿到的就是干净字符串。5.3 业务路由的分发策略拿到type之后怎么路由到具体页面我推荐用一个注册表模式。每个业务模块启动时注册自己关心的 type 和对应的处理函数接收器收到消息后查表分发。public class DeepLinkRouter { private Dictionarystring, ActionDeepLinkData handlers new Dictionarystring, ActionDeepLinkData(); public void Register(string type, ActionDeepLinkData handler) { handlers[type] handler; } public void Dispatch(DeepLinkData data) { if (handlers.TryGetValue(data.type, out var handler)) handler(data); else Debug.LogWarning($未注册的 DeepLink 类型: {data.type}); } }这样设计的好处是新增业务类型只需要注册不用动核心代码。而且未注册的类型会打警告方便发现配置遗漏。6. 常见问题排查与实战避坑清单6.1 Universal Links 不生效的排查顺序Universal Links 不生效是最常见的问题排查要按顺序来别乱试。第一步确认 AASA 文件能通过https://域名/.well-known/apple-app-site-association直接访问且 Content-Type 是application/json。第二步确认 appID 里的 TeamID 和 BundleID 完全正确大小写敏感。第三步确认 Xcode 里 Associated Domains 配的是applinks:开头。第四步删除 App 重装清除 AASA 缓存。第五步用真机测试模拟器对 Universal Links 支持不完整。我遇到过最坑的一次是 AASA 文件放在了/.well-known/目录下但服务器做了重定向苹果的爬虫不跟重定向导致一直不生效。后来把重定向去掉就好了。所以服务器配置也要检查。6.2 参数丢失的几种典型场景参数丢失一般有这几个原因冷启动时机问题前面讲过用待处理队列解决、UnitySendMessage 的 GameObject 名字拼错、方法不是 public void、参数里有特殊字符没转义。排查的时候先在原生层把要发的 JSON 打出来再在 C# 层把收到的打出来两头一对比就知道是哪一段丢的。还有一种情况是 Unity 场景切换导致接收器被销毁。所以接收器一定要 DontDestroyOnLoad而且要在第一个场景就创建好。6.3 线上环境的监控与日志线上出问题最难的是没法复现。我的做法是在原生层和 C# 层都加详细日志关键节点都打点原生收到链接、解析完成、发送到 Unity、C# 收到、解析完成、路由分发。这些日志上报到自己的日志系统出问题的时候一查链路就清楚了。另外建议加一个兜底如果链接解析失败或者路由找不到不要静默失败而是跳到一个默认页面比如首页并上报异常。用户至少不会卡在空白页。问题现象可能原因排查方向链接完全没反应AASA 配置错误 / 域名不匹配检查 AASA 可访问性和 appID唤起 App 但没参数冷启动时机 / 桥接失败检查待处理队列和 UnitySendMessage参数乱码URL 编码未解码原生层加 percent decoding热启动正常冷启动失败初始化顺序问题改用主动拉取模式微信内无法唤起被拦截引导用系统浏览器打开7. 一些实战中攒下来的经验关于 AASA 缓存我再强调一次测试阶段一定要删 App 重装不要指望它自己更新。苹果的 CDN 缓存有时候要几个小时甚至一天。如果实在等不及可以在设备上关闭再开启网络有时候能触发刷新但不保证。关于桥接性能UnitySendMessage是同步调用参数太大会有性能开销。Deep Link 的参数一般不大问题不大但如果你要传很长的数据建议先存到原生层只传一个 key 过去C# 再反向调原生取。关于测试我强烈建议做一个测试页面把各种链接都列出来点一下就能测。比手动输链接快多了而且不容易出错。这个页面在开发和 QA 阶段能省大量时间。最后说个心态问题。Deep Link 这条链路涉及 iOS 系统、原生代码、Unity 引擎、C# 业务四层任何一层出问题都会导致失败。排查的时候一定要有耐心按链路一段一段验证不要跳步。我见过太多人一上来就怀疑 Unity 层结果折腾半天发现是 AASA 文件路径写错了。从最外层开始查往往最快。
返回列表