尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

Unity手游iOS Deep Link接入指南:URL Scheme与Universal Links全流程

Unity手游iOS Deep Link接入指南:URL Scheme与Universal Links全流程 做 Unity 手游接 iOS Deep Link这个需求我在项目里前前后后调了一周多从最开始产品提“分享链接能直接拉起游戏进指定页面”到后来把 URL Scheme、Universal Links、冷启动时序、C# 参数投递整条链路彻底打通中间踩的坑比预想多得多。今天把完整流程沉淀下来希望后来的人少走点弯路。这篇内容主要围绕四条主线两种唤起方案的配置区别、原生层如何捕获链接、参数怎么安全投递到 C#、以及联调阶段最容易翻车的几个点。1. Deep Link 在 Unity 手游里到底解决什么问题1.1 业务场景分享裂变、广告归因、跨应用协同Unity 手游和原生 App 最大的差异在于游戏本身有一套独立的场景和 UI 体系外部链接打进来之后真正要做的不只是“把 App 打开”而是要把链接里带的业务参数准确送进游戏逻辑层再驱动游戏跳到对应界面。最常见的三类场景第一类是分享裂变玩家分享一个邀请链接给好友好友点击后希望能直接进入邀请页或者领取奖励第二类是广告投放市场渠道在链接后面拼上 campaign ID、渠道 ID 和点击时间App 启动后要能拿到这些参数完成归因上报第三类是跨应用协同比如从社区、直播、伴侣 App 跳转到游戏内的指定房间或赛事页面。这三类场景的核心诉求是一样的链接要能唤起 App唤起后业务参数要能正确传递传递之后游戏要能准确路由。听起来简单实际做起来涉及 iOS 系统层能力、原生工程配置、Unity 与原生通信、C# 数据解析四个环节任何一环出问题链路就断。我在这个项目里就分别踩过“链接能打开游戏但参数没传进去”“参数进去了但游戏还没初始化完”“模拟器能跳真机却调不起”三种典型故障下面逐个展开说。1.2 URL 的“浅层唤醒”与“深层路由”到底指什么很多人聊 Deep Link 容易混淆两个层面。浅层唤醒指的是系统根据链接信息直接拉起 App 进程这一层由 iOS 系统完成Unity 开发者只需要把原生配置做好深层路由指的是 App 起来之后根据链接里的路径和参数决定进哪个页面、执行什么逻辑这一层必须自己去实现。URL Scheme 和 Universal Links 解决的是前者C# 层的参数解析与分发解决的是后者两者缺一不可。对一个 Unity 项目来说“路由”这个词可能有点陌生但它对应的就是游戏里的 ModuleManager、SceneManager 或者自己维护的事件总线。我习惯把 Deep Link 解析出来的结果做成一个统一的路由对象里面包含页面标识、来源标记、业务参数然后交给游戏现有的跳转系统去消费。这样无论链接是从哪个渠道进来的到了 C# 层都是一个标准结构后续要新增一个页面入口只需要在路由表里加一行。1.3 两种唤起方案的定位差异URL Scheme 是 iOS 早期就有的自定义协议比如mygame://open?pageinvite。它的优点是配置简单缺点是系统会弹确认框体验打折而且如果其他 App 注册了同样的 scheme 就会冲突。Universal Links 是 iOS 9 之后苹果主推的方式本质上是把标准 HTTPS 链接和 App 的 Bundle ID 绑定用户点击域名下的链接时系统直接唤起 App不弹窗更安全。URL Scheme 和 Universal Links 并不是二选一的关系。Universal Links 需要服务器部署关联文件存在一定的时效和兼容问题老版本系统或者用户浏览器环境特殊时不一定百分百可靠所以业内普遍的做法是两者都接入Universal Links 优先URL Scheme 兜底。我在项目里把两条链路都打通之后还专门验证过同一个链接在微信内置浏览器、Safari、QQ 浏览器里的表现发现微信里 Universal Links 的拦路问题最明显这个后面会在排查环节详细讲。2. 原生侧配置从 Info.plist 到 AASA 文件2.1 动手前的准备清单先说清楚这一整节都是 iOS 原生工程层面的配置需要在 Xcode 里操作。Unity 开发者不用畏惧这部分因为只需要配置一次之后只要 Bundle ID 和团队 ID 不变基本不用再动。动手前确认四件事第一Apple Developer 账号已付费且具备创建 App ID、配置 Associated Domains 的权限第二确认游戏的 Bundle IdentifierXcode 里 Unity 生成的工程默认会从 Player Settings 同步过去但要注意真机调试时如果替换过签名Bundle ID 可能不是你预期的那个第三准备一个 HTTPS 域名且服务器支持上传静态文件因为 Universal Links 需要一个关联文件第四真机设备Deep Link 很多行为模拟器和真机表现不一致后面验证阶段会说明。2.2 URL Scheme 的 Info.plist 配置在 Xcode 里找到 Info.plist添加CFBundleURLTypes数组里面每一项是一个 URL type。每个 type 需要两个关键字段CFBundleURLName和CFBundleURLSchemes前者可以填 Bundle Identifier后者是自定义 scheme 数组。比如游戏叫 MyGame我会把 scheme 定义为mygame最终配置看起来是这样keyCFBundleURLTypes/key array dict keyCFBundleURLName/key stringcom.company.mygame/string keyCFBundleURLSchemes/key array stringmygame/string /array /dict /array这里的 scheme 命名有两个经验。第一是尽量短但别短到容易被别的应用抢注我见过有人用单字母 scheme结果另一款游戏也注册了同样的名字测试时经常唤起错 App第二是建议全小写iOS 的 URL Scheme 解析虽然不区分大小写但分享出去的链接如果大小写不统一用户从日志里复盘时会看得很痛苦。配置完成后用 Xcode 跑一次真机Safari 地址栏输入mygame://test能弹出 App 就说明这一半 OK。2.3 Universal Links 的三段式配置Universal Links 配置比 URL Scheme 复杂一共三个环节开发者后台开启、Xcode 添加关联域名、服务器部署 AASA 文件。开发者后台进入 Certificates, Identifiers Profiles找到对应 App ID勾选 Associated Domains 能力。这一步容易忽略漏了之后 Xcode 里怎么加 Capability 都没用系统根本不认。Xcode 里的操作是在 Signing Capabilities 面板点击加号选择 Associated Domains在 Domains 一栏填入applinks:example.com注意前缀是applinks:不是https://。如果有多域名就一行一个不要逗号分隔。服务器端需要在域名的.well-known目录下放一个名为apple-app-site-association的文件注意没有.json后缀。文件内容是一个 JSON 结构核心是把团队 ID 加 Bundle ID 的格式与路径匹配规则绑定{ applinks: { apps: [], details: [ { appID: TEAMID.com.company.mygame, paths: [ * ] } ] } }这里有个容易出问题的点appID必须是大写团队 ID 加 Bundle ID中间用点连接拼错一个字符整个链路直接失效。paths我建议不要一上来就用*因为*意味着这个域名下所有路径都触发唤起 App如果域名同时还在跑别的业务会有误唤起风险。比较稳的做法是把 Deep Link 专门收敛到一个前缀路径下比如/game/配置成[ /game/* ]这样非游戏路径可以平稳走网页。AASA 文件部署完第一件事不是急着真机测试而是用浏览器或命令行先验证文件本身能不能访问。这个文件必须通过 HTTPS 提供证书要有效且服务器不能对路径做重定向某些 CDN 会自动 302iOS 对这个很敏感重定向之后文件就失效了。我遇到过测试环境和生产环境 CDN 配置不一致测试环境返回 200生产环境跳了 302结果线上 Universal Links 全挂排查了很久才发现。2.4 Associated Domains 生效延迟的隐性成本很多人不知道AASA 文件不是改了立即生效。Apple 会缓存这个文件缓存时间随系统和网络环境变化短则几分钟长则一两天。开发者联调阶段最烦的就是改完文件后“没变化”以为配置错了。我的经验是每次改完 AASA 之后先用 curl 确认服务器返回的内容是对的再打开飞行模式重连或者重启设备能在一定程度上绕过系统缓存。真机实测还有一个土办法在备忘录里输入链接并长按如果弹出“在 App 中打开”说明系统已经识别到关联关系否则就继续等或者查配置。顺带说一句Xcode 里 Capability 配置后项目会自动生成Project.entitlements文件这个文件不要随便删它跟签名关联。我在一次版本迭代时清理工程误删过结果 Universal Links 直接失效重跑测试才发现是 entitlements 丢了。3. 原生到 Unity 的参数投递链路3.1 原生层的 URL 捕获入口方法不止一个原生层接收 Deep Link 不是只有一个回调根据链接类型和 App 启动状态系统会调用不同的方法。我把需要覆盖的入口总结成一张表场景URL SchemeUniversal Links冷启动application:didFinishLaunchingWithOptions:里的 launchOptions同一个 launchOptions取 UserActivity 字典热启动前台或后台application:openURL:options:application:continueUserActivity:restorationHandler:iOS 13 Scene 生命周期scene:openURLContexts:scene:continueUserActivity:如果工程没有 SceneDelegate只处理 AppDelegate 的方法就行如果有 SceneDelegate必须在两个地方都处理否则 iOS 13 的机器上走 Scene 生命周期时会漏接。Unity 自动生成的 iOS 工程默认没有 SceneDelegate但如果外包或后续二次开发加过 SwiftUI 生命周期就要特别注意。冷启动是这里最容易丢参数的场景。App 进程还没跑起来时系统把链接信息塞在didFinishLaunchingWithOptions的字典里如果只实现了openURL:冷启动就捕获不到。Universal Links 冷启动的取法比 URL Scheme 更隐蔽它存在UIApplicationLaunchOptionsUserActivityDictionaryKey这个 key 里里面再嵌套一个UIApplicationLaunchOptionsUserActivityKey对应的NSUserActivity实例取值时要做两层解包。3.2 原生侧的参数整理与暂存机制我推荐在原生层直接把 URL 解析成结构化字典再转成 JSON 字符串投给 Unity而不是把原始字符串丢给 C# 自己解析。原因有两点一是NSURLComponents对 URL 的解析比手写字符串截取可靠能自动处理 query 和 fragment 的编码问题二是原生侧解析后 C# 的工作量更小Unity 层只需要反序列化一次。为了方便理解我给一个 Objective-C 的示例Unity 工程里一般用.mm后缀的 ObjC 文件既能写 C 也能调用 Unity 的 C 接口static NSString *lastPayload nil; static NSString *PayloadFromURL(NSURL *url) { NSMutableDictionary *dict [NSMutableDictionary dictionary]; if (url.scheme) dict[scheme] url.scheme; if (url.host) dict[host] url.host; if (url.path.length 1) { dict[path] [url.path substringFromIndex:1]; } if (url.query) dict[query] url.query; dict[rawUrl] url.absoluteString; NSData *data [NSJSONSerialization dataWithJSONObject:dict options:0 error:nil]; if (!data) return nil; return [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]; }之所以要加一个静态变量lastPayload是为了解决冷启动时序问题。Unity 引擎初始化需要几秒原生层在didFinishLaunchingWithOptions里拿到链接时Unity 的 C# 侧还没准备好接收回调。即使立刻调用 UnitySendMessage那个 GameObject 可能还不存在。所以正确姿势是先把 payload 暂存到静态变量等 Unity 侧主动注册回调时再取出投递。3.3 投递注册C# 主动问、原生被动给这里一定要强调参数投递不要做成“原生推、C# 收”的广播模式要让 C# 侧在初始化完成后主动来拿。做法是原生暴露一个注册接口C# 在游戏逻辑准备好后调用原生收到注册请求后把暂存的 payload 一次性投出去同时清空暂存。原生侧暴露的接口看起来像这样extern C void _UnityDeepLink_RegisterListener() { if (lastPayload) { const char *bytes [lastPayload UTF8String]; UnitySendMessage(DeepLinkManager, OnNativePayload, bytes); lastPayload nil; } }C# 侧用 DllImport 声明并完成注册#if UNITY_IOS !UNITY_EDITOR [DllImport(__Internal)] private static extern void _UnityDeepLink_RegisterListener(); #endif private void Awake() { name DeepLinkManager; #if UNITY_IOS !UNITY_EDITOR _UnityDeepLink_RegisterListener(); #endif }注意一个核心前提UnitySendMessage 的第一个参数是场景中某个 GameObject 的名字第二个参数是挂在它身上的脚本里的方法名两个都对不上消息就静默丢失。所以我上面把 GameObject 的 name 在 Awake 里强制赋值防止场景里名字被改掉这是一个很实用的防御性写法。3.4 C# 层解析与路由分发投递到 C# 的 payload 是 JSON 字符串我习惯定义一个可序列化的数据类[Serializable] public class DeepLinkPayload { public string scheme; public string host; public string path; public string query; public string rawUrl; }反序列化之后query 还是原始形式需要二次解析。如果用的是JsonUtility注意它要求类标记[Serializable]才能正确转换。二次解析我直接写了一个简单工具把keyvaluekey2value2拆成字典public static Dictionarystring, string ParseQuery(string query) { var result new Dictionarystring, string(); if (string.IsNullOrEmpty(query)) return result; foreach (var pair in query.Split()) { var idx pair.IndexOf(); if (idx 0) continue; var key pair.Substring(0, idx); var value pair.Substring(idx 1); result[key] Uri.UnescapeDataString(value); } return result; }拿到字典后进入路由层。我通常维护一个page - Action的映射表考虑到游戏场景加载是异步的真正的路由动作一般不是直接跳转而是把目标场景名和参数存进一个全局的“待处理事件”等当前场景加载完成后再消费。比如活动页入口游戏主界面还没起来时强行跳转会导致空场景所以路由层要感知当前的场景状态。路由分发再强调一个细节业务参数一定要做合法性校验。外链里的 page、itemId、reward 都可以被外部构造直接拼进游戏逻辑存在安全风险。我做的第一版就把 URL 里的pagegiftcount99999直接读出来给奖励系统发奖还好 QA 在测试环境试出来了。接入 Deep Link 后参数入口变多了校验绝不能省。4. 全流程联调验证与高频问题排查4.1 调试工具链模拟器、命令行与抓包整个链路配置完之后不能靠肉眼测试要有一套可重复的验证流程。我最常用的是模拟器加命令行。先说 URL Scheme用xcrun simctl openurl booted mygame://open可以指定模拟器打开协议链接适合快速验证配置文件是否有误。Universal Links 也差不多不过域名解析需要网络通模拟器和真机行为在 AASA 识别上有一点点差异最终必须真机收口。命令行验证 AASA 文件是每次必做的curl -i https://example.com/.well-known/apple-app-site-association重点关注三件事响应是否 200有没有重定向内容里的 appID 是否匹配当前工程。另外建议用 Charles 抓包看真机上的链接请求因为 iOS 在匹配 Universal Links 时会有网络请求到域名拉取 AASA如果抓包能看到请求但返回体不对问题基本锁定在服务器配置。4.2 高频问题速查表我把联调阶段遇到的高频问题整理成一张表方便对号入座现象可能原因解决方向点击链接 Safari 打开网页而不是 AppAASA 未生效、appID 拼错、Associated Domains 未开重新验证文件检查团队 ID 和 Bundle IDUniversal Links 第一次点击有效之后失效iOS 16 后链接方式变化可能引入“Universal Links with Full Experience”需要确认 App 内跳转是否符合预期或 AASA 被缓存刷新在备忘录中长按链接确认识别状态重启设备再试冷启动后参数收不到只实现了 openURL没处理 didFinishLaunchingWithOptions补全 launchOptions 入口参数里的中文或特殊字符乱码URL 编解码不一致统一用 NSURLComponents 解析C# 端 UnescapeDataStringUnitySendMessage 提示找不到 GameObject场景里没有对应名字的对象Awake 里强制 name 赋值微信内点击链接没拉起 App微信内置浏览器对 Universal Links 有限制需要走应用宝或 JS 桥配置 fallback 页面提示用户用 Safari 打开修改 AASA 后长时间不生效系统缓存、CDN 缓存清理设备缓存curl 确认源站内容正确连续点击多个深链只响应第一个原生暂存变量被覆盖设计带时间的队列多个场景逐个投递4.3 编码与参数健壮性链接参数很容易在流转过程中变形尤其是中文、emoji、加号这些字符。一个常见的坑是分享链接里用户昵称是中文在生成 URL 时没做编码到了原生层NSURL直接返回 nil整个 Deep Link 链路静默失败。我后来统一要求运营后台分享的链接必须用URLComponents构建参数值先做percentEncodingAllowedCharacters编码C# 这边再用Uri.UnescapeDataString还原两边闭合就不会再出现中文乱码。另外query 里如果包含服务端解析时很可能被当成空格字符集里又有、这些保留字符时更要小心。在原生层整个 URL 交给NSURLComponents.queryItems处理会让系统自动处理这些细节少很多心智负担。4.4 测试用例清单联调阶段的测试用例至少要覆盖冷启动进入、热启动前台进入、App 处于后台时进入、多个链接连续进入、链接带中文参数、链接带特殊字符、无效 scheme 进入、无参数链接进入、AASA 未更新时旧链接进入、弱网环境进入。以上每一条最好都在真机和模拟器上各跑一遍。我在项目里就是靠这套用例在一周内把链路里百分之九十的雷排掉了。5. 一些我踩过的坑与最后想说的经验5.1 冷启动时序是隐性的头号杀手冷启动丢参的问题在我接手时已经出现好久了现象是分享链接在游戏完全退出后点击能打开 App但收不到参数如果 App 在后台热着一切正常。很多人第一反应是原生没传其实原生传了只是传太早。Unity 引擎加载过程中场景里的 GameObject 还没创建UnitySendMessage 无从绑定消息就丢了。这个问题用“C# 主动注册、原生暂存待投”的模式才能彻底解决靠延迟几秒投递是不可靠的因为不同设备上 Unity 初始化耗时不固定。5.2 千万别把 route 逻辑写死在原生层有些团队习惯在原生层就把跳转目标算好只把结果字符串传给 C#。我强烈不推荐。原因有两个第一原生层拿不到 Unity 场景是否加载完成的状态强行跳转的时机永远不准第二以后加新页面、改路由规则如果逻辑在原生层改了就要重新发版本。把原始参数完整传到 C#路由规则收敛在游戏侧后续迭代只发一个热更包就能改。5.3 线上环境建议加开关和日志最后分享一个偏工程化的经验Deep Link 入口要在线上环境做好开关控制。因为 Deep Link 暴露的是系统级的唤起能力万一路由逻辑写错导致误跳转影响面是全量用户。我通常在 C# 路由层加一个配置项默认打开运营可以远程关闭同时在原生层和 C# 层各打一条日志线上才能定位是系统没拉起还是参数没解析成功。有了日志再遇到那些“用户说点链接没反应但本地复现不出来”的投诉至少能少猜一半。
返回列表