Unity手游Deep Link全流程:URL Scheme与Universal Links参数投递
发布时间:2026/10/1 5:23:54来源:尧图网络
做 Unity 手游Deep Link 这个需求早做早省心项目越大越会体会这句话的分量。不管是买量归因、老用户召回还是外部分享回流、网页活动直接拉起游戏进指定界面总绕不开 iOS 上 URL Scheme 和 Universal Links 这两条路。但我看过太多项目卡在最后一道坎链接明明能把 App 唤醒参数却在原生层到 C# 的桥接环节丢掉或者冷启动时压根接不到。这篇把从原生配置讲到 C# 参数投递的完整流程拆给你看所有踩过的坑和能直接抄的代码都会写出来。1. 场景分析与链路设计1.1 手游里最典型的 Deep Link 使用场景做 Unity 手游的联运和发行Deep Link 不是锦上添花是很多业务线的硬前提。最常见的场景我整理成四类每一类的参数和落地动作都不一样接口设计要提前留好扩展位。第一类是买量归因。广告平台在用户点击广告后会回传一个点击 ID拉起游戏时需要把这个 ID 和渠道信息透传给 SDK用于激活归因和后续的广告价值评估。这类 Deep Link 的特点是字段固定、时效性强通常要求冷启动也能准确拿到如果参数只在热启动时才能到达很多激活就被算成自然量了。第二类是老用户召回。运营通过短信、消息推送、邮件或者外部网页发出一个带活动参数的链接用户点击后唤起游戏同时带上 campaign、coupon、rta 之类的参数。游戏内根据这些参数直接弹签到、发邮件奖励、进活动页面转化率比普通打开高不少这类需求经常伴随运营节奏临时加字段C# 侧的解析结构必须够灵活。第三类是社交裂变和邀请。玩家 A 分享一个带邀请码的链接给 BB 点击后进入游戏C# 层拿到 inviteCode 之后绑定关系A 获得奖励。这个场景对参数失真的容忍度极低邀请码丢了等于白做活动而且链路往往要从微信内置浏览器再跳一次原生回调的时序会比普通 Safari 更复杂。第四类是内外联动。游戏在网页上做预约、问卷、赛事签到用户从网页直接拉起游戏到对应模块例如从网页的“阵容查看”跳转到游戏内的阵容页面。这类需求往往还会带上登录态的 token深链参数可能包含敏感信息处理的时候要格外小心不能随便打进日志里。这四个场景有一个共同点链接本身只是入口真正的价值在链接里那串参数能否安全、完整、准时地到达 C# 层业务逻辑。所以设计 Deep Link 系统时我建议把参数投递当作第一优先级唤醒只是手段。1.2 一条完整的跳转链路拆解一次完整的跳转链路可以拆成五段。第一段是入口用户在 Safari、微信、短信、其他 App 或者系统搜索里点击一个链接。第二段是系统匹配iOS 根据链接类型分包处理自定义协议的 URL Scheme 会直接唤起注册过该协议的游戏 AppHTTPS 的 Universal Links 会先由系统确认域名关联确认有效后直接拉起 App没有确认弹窗。第三段是原生回调URL Scheme 进入 AppDelegate 的 openURL 回调Universal Links 进入 continueUserActivity 回调冷启动时则可能埋在 launchOptions 里。第四段是桥接层原生代码把 URL 里的 scheme、host、query 参数解析出来组织成统一结构通过 UnitySendMessage 投递给 Unity 引擎里的某个 GameObject或者先缓存下来等 C# 来拉。第五段是业务分发C# 脚本接收参数后做校验、解码、事件广播最终由业务模块决定是弹窗、切场景还是调归因 SDK。很多人只看前两段以为有了链接能唤起 App 就完事了结果死在第四段和第五段热启动时 UnitySendMessage 目标 GameObject 还没创建冷启动时 launchOptions 里的 URL 没人去取参数就静默丢掉了。后面我会重点讲这两段的时序处理那才是全流程真正容易翻车的地方。1.3 URL Scheme 与 Universal Links 的选型对比在 iOS 上两种方案不是替代关系是互补关系。我习惯的做法是 Universal Links 作为主链路URL Scheme 作为兜底和兼容链路。每条链接点击后iOS 会先判断域名配置的 Universal Links 是否有效有效就直接拉起 App如果 Universal Links 因为网络、AASA 文件缺失等理由失败自定义 Scheme 仍可以充当备用唤醒方式。对比维度URL SchemeUniversal Links系统要求iOS 4 起支持iOS 9 起支持域名服务器不需要需要 HTTPS 域名 AASA 文件用户体验有系统确认弹窗无弹窗直接进入可信度自定义协议易被占用展示真实域名用户信任度高网络依赖不依赖网络依赖 AASA 文件在线可访问回调方式application:openURLcontinueUserActivity适合场景兜底、兼容旧系统、三方 SDK 要求主流投放、运营活动链路具体到手游项目广告平台归因和运营活动链接统一用 Universal Links游戏内分享卡片、SDK 的旧链路、以及需要兼容 iOS 9 以下或某些不支持 Universal Links 的 WebView 场景保留 URL Scheme。两种方式最终都要走同一个原生桥接入口C# 层不区分来源只处理统一结构的数据。2. 原生层配置与系统侧细节2.1 在 Xcode 里正确注册 URL Scheme注册 URL Scheme 的本质是往工程的 Info.plist 里写一段名为 CFBundleURLTypes 的结构。这个结构在 Xcode 里虽然有可视化入口但我们在 Unity 导出的工程里频繁改动时手工维护反而更清晰。不管是界面添加还是直接改 plist最终生成的 XML 都长成下面这样keyCFBundleURLTypes/key array dict keyCFBundleURLName/key stringcom.example.game/string keyCFBundleURLSchemes/key array stringmygame/string stringmygamejoin/string /array /dict /array这里有几个容易踩的细节。第一URL Scheme 的协议名只能以字母开头后面可以接字母、数字和一些特殊符号不能用下划线开头最好也别用纯数字否则系统解析时大概率出问题。第二Scheme 是全系统唯一的如果别的 App 已经占用了同名协议iOS 有时候会随机唤起其中一个测试时一脸懵。第三如果项目同时接多个三方 SDK诸如某些广告平台也要求占一个 Scheme一定要核对冲突别直接照抄官方文档里的 myapp。注册完成后在 iOS 自带的 Safari 地址栏输入 mygame://test直接回车正常会弹出“在‘我的游戏’中打开”的确认框。如果 App 在后台它会被拉到前台如果没反应大概率是 URL Scheme 没写对或者类型没匹配上先回 Xcode 检查这步再继续往下做。2.2 Universal Links 的三件套配置Universal Links 需要三样东西同时到位Apple Developer 后台的 App ID 开启 Associated Domains 能力、Xcode 工程里配置关联域名、线上 HTTPS 服务器放一个 AASA 文件也就是 apple-app-site-association。少一样整个链路都是坏的而且这种坏经常是静默的配置完很久才发现不行。在 Xcode 的 Signing Capabilities 里点击添加 Associated Domains注意不要手滑加进 App Groups然后填入 applinks:game.example.com。如果没有独立子域名直接用主域名也可以但我个人强烈建议为游戏单独准备一个子域名别和官网其他页面混在一起后面控制 paths 匹配规则会轻松很多。接下来在服务器根目录或者知名路径放一个文件名不带后缀的 apple-app-site-association 文件。iOS 两个标准的探测路径分别是 https://domain/.well-known/apple-app-site-association 和 https://domain/apple-app-site-association很多云厂商会拦掉 .well-known 路径建议两个位置都放一份。AASA 内容格式如下{ applinks: { apps: [], details: [ { appIDs: [TEAMID1234.com.example.game], paths: [*, NOT /blocked/*] } ] } }appIDs 的格式必须是 Team ID 加 Bundle ID也就是你在开发者后台能看到的那串完整 application-identifier。Team ID 不是 Bundle ID别写反。paths 支持通配符和 NOT 前缀匹配规则是从上到下依次尝试第一个匹配的生效例如 * 表示所有路径NOT 开头的路径会被排除。部署完成后用 curl 验证一下响应内容和 Content-Type这一步很容易遇到第二个大坑有些后端会强制把无后缀文件识别成 application/octet-stream或者返回 JSON 时带 BOM、加了 charset 后缀这些都可能影响 iOS 的解析。最稳妥的响应头是 Content-Type: application/json文件不要有任何多余空白。2.3 回调收到 URL 之后放在哪里URL Scheme 和 Universal Links 的回调入口不同。URL Scheme 在主线程走 application:openURL:options:Universal Links 走 application:continueUserActivity:restorationHandler:这两个方法在 Unity 导出工程里默认都不是现成的需要自己往 AppDelegate 里加或者用 Category 挂上。如果你手动维护工程直接在 AppDelegate 实现两个方法然后统一转到一个 DeepLinkBridge。如果工程不是自己维护每次重新导出 Xcode 工程都会覆盖 AppDelegate那就用 Objective-C 的分类把两个代理方法写进 Category这样能避免每次导出后重复修改。- (BOOL)application:(UIApplication *)app openURL:(NSURL *)url options:(NSDictionaryUIApplicationOpenURLOptionsKey, id *)options { [DeepLinkBridge handleURL:url]; return YES; } - (BOOL)application:(UIApplication *)application continueUserActivity:(NSUserActivity *)userActivity restorationHandler:(void (^)(NSArrayidUIUserActivityRestoring * _Nullable))restorationHandler { if ([userActivity.activityType isEqualToString:NSUserActivityTypeBrowsingWeb]) { NSURL *url userActivity.webpageURL; if (url ! nil) { [DeepLinkBridge handleURL:url]; return YES; } } return NO; }还需要补一个冷启动兜底。如果 App 被杀掉之后通过深链启动URL 会出现在 didFinishLaunchingWithOptions 的 launchOptions 里虽然 openURL 和 continueUserActivity 在多数冷启动场景下也会被调用但为了稳妥我习惯在 didFinishLaunching 里读取 launchOptions把 UIApplicationLaunchOptionsURLKey 和 UserActivityDictionaryKey 里的 URL 再转一次 handleURL。多跑一次没副作用单例桥接内部会做重复过滤。同时要注意 iOS 13 的 Scene 生命周期。如果工程支持 SceneDelegateUniversal Links 回调可能落到 scene:continueUserActivity: 方法里。Unity 导出的单窗口工程通常还是 AppDelegate 体系但如果团队自己改过两个地方都要接或者直接用一个自定义通知统一转发。2.4 配置验证的实操手段配置完不要凭感觉上线我每次联调都会走一套固定验证流程。先验证 AASA 文件在终端执行 curl确认 HTTP 状态码是 200Content-Type 是 application/json内容 JSON 解析不报错appIDs 和 paths 和工程配置一致。再验证 Universal Links最简单的办法是复制链接到系统自带的备忘录点一下正常体验是备忘录顶部直接出现“打开‘我的游戏’”的小卡片App 自动被唤起如果在 Safari 里测试链接首次打开可能会先停留在网页上页面顶部出现横幅点击横幅拉起 App这也算正常。验证 URL Scheme 不需要真机模拟器和真机都能测。在 Xcode 里用菜单 Debug Simulate Open URL中文环境是“调试/模拟/模拟 URL”输入 mygame://invite?fromtestcode123回车后看 App 是否被拉到前台。验证参数能否到达 C# 层就在 DeepLinkBridge 的 handleURL 里加一个临时 NSLog在 C# 的接收函数里加一个 Debug.Log两边同时打点确认同一串 JSON 都出现过再进入下一环节。这个验证会贯穿后面每一步改动。3. 原生到 C# 的桥接与参数投递3.1 UnitySendMessage 的真实使用方法Unity 原生侧向 C# 侧投递消息标准手段是 UnitySendMessage。它会按名字找到一个 GameObject然后调用这个 GameObject 上某个 MonoBehaviour 的公开方法参数是一个 C 字符串。函数签名是void UnitySendMessage(const char *objName, const char *methodName, const char *param);因为它是 C 接口Objective-C 里可以直接调Swift 里需要借助一个 Objective-C 壳来对接。调用时 objName 就是场景里真实存在的 GameObject 名字methodName 是挂在它身上的脚本里定义的 void 方法名param 会被原样当作一个字符串传给那个方法。用起来有几个纪律。第一接收方法的参数类型只认 string如果 C# 写的是 OnDeepLinkNativeMessage(string json)就传字符串写 int 或 bool 不会有任何转换反而可能直接没反应。第二接收 GameObject 必须处于激活状态脚本必须挂载且方法必须是 public。第三UnitySendMessage 内部会对同一个 GameObject 上的多个脚本尝试调用同名方法如果多个脚本都有这个方法会出现多次调用或者调错方法的情况所以接收函数名要起得足够专一比如 OnDeepLinkNativeMessage不要叫 OnMessage 这种到处可能重名的名字。另外还有 IL2CPP 的坑发布开启代码裁剪后接收方法可能被当成未被引用的代码剥离掉。解决方式是在方法上挂 [Preserve] 特性或者用 RuntimeInitializeOnLoadMethod 让它被动引用后面 C# 侧示例会写出来。但 UnitySendMessage 不是万能的。它只适合热启动阶段 Unity 已经跑起来的情况冷启动时引擎还没 ready直接调用的结果就是消息丢或者进程崩。所以桥接层必须设计成“热启动走推送冷启动走缓存拉取”的双通道模式。3.2 URL 解析与统一 JSON 格式无论 URL Scheme 还是 Universal Links到达原生层的最终形态都是一个 NSURL里面包含 scheme、host、path、query 等部分。为了让 C# 侧只面对一种结构我建议原生把 URL 解析成固定 JSON而不是直接丢一个 URL 字符串过去。一个推荐结构长这样{ url: mygame://invite?fromwechatcodeabc123, scheme: mygame, host: invite, path: /invite, items: [ { name: from, value: wechat }, { name: code, value: abc123 } ] }items 用数组而不用字典是因为 Unity 内置的 JsonUtility 不支持 Dictionary直接解析字典要写额外逻辑。用对象数组就可以 JsonUtility.FromJson省掉引入第三方的麻烦。如果项目里本来就装了 Newtonsoft Json.NET 或 LitJson也可以让原生直接输出字典对象这里没有绝对标准团队保持一致就好。原生侧解析用 NSURLComponents 比手工 split 字符串可靠得多。别图省事对 URL 做字符串截取query 里一旦出现 URL 编码的空格、中文、 符号手工切分百分之百出 bug。NSURLComponents 会自动处理 percent-encoding 的解码。在桥接代码里我还会做一层容错如果某个 query item 没有 value给默认空字符串如果 URL 没有 host不写死 nil 而是转成空串。这样 JSON 序列化不会因为 nil 挂掉C# 侧解析时压力也小。3.3 冷启动、热启动、后台恢复的时序差异这是整个深链方案里最核心的一段翻车基本都翻在这里。先说热启动App 已经跑起来、Unity 场景已经加载完用户点击链接原生回调触发这时候 UnitySendMessage 可以直接投递C# 接收函数立即执行链路最顺畅。后台恢复的情况类似App 在后台但进程还活着Unity 也没退出基本可以按热启动处理。唯一要注意的是有些业务希望回到前台后再弹页面C# 侧可以对 OnApplicationFocus 做一次延迟处理避免一被拉起就直接盖 UI 弹窗玩家体验会好很多。冷启动是最难搞的App 进程被系统杀掉或者第一次安装启动用户点链接后系统帮你拉起进程此时 Unity 引擎还没初始化场景里的接收 GameObject 也不存在。如果这时原生侧直接调 UnitySendMessage轻则消息丢失重则崩溃。正确做法是先把参数缓存到原生单例里等 Unity 场景跑起来、C# 侧主动来拉。为了统一时序我在桥接层维护一个 isUnityReady 标记。C# 侧在接收对象 Awake 时调用原生接口通知引擎已就绪之后原生再收到新 URL 就直接推送在 Awake 之前收到的 URL 全部缓存Awake 后 C# 主动拉取一次。这个设计简单可控冷启动热启动都不会丢参数。3.4 可复用的桥接实现示例下面给一套完整的 Objective-C 桥接代码我把 DeepLinkBridge 做成单例内部用静态字符串缓存未投递消息。static NSString *g_cachedDeepLinkJson nil; static BOOL g_unityReady NO; implementation DeepLinkBridge (void)setUnityReady:(BOOL)ready { g_unityReady ready; if (ready g_cachedDeepLinkJson.length 0) { NSString *json g_cachedDeepLinkJson; g_cachedDeepLinkJson nil; UnitySendMessage(DeepLinkProxy, OnDeepLinkNativeMessage, [json UTF8String]); } } (NSString *)consumeCachedDeepLink { NSString *json g_cachedDeepLinkJson; g_cachedDeepLinkJson nil; return json ?: ; } (void)handleURL:(NSURL *)url { NSMutableDictionary *info [NSMutableDictionary dictionary]; info[url] url.absoluteString ?: ; info[scheme] url.scheme ?: ; info[host] url.host ?: ; info[path] url.path ?: ; NSMutableArray *items [NSMutableArray array]; NSURLComponents *components [NSURLComponents componentsWithURL:url resolvingAgainstBaseURL:NO]; for (NSURLQueryItem *item in components.queryItems) { NSMutableDictionary *pair [NSMutableDictionary dictionary]; pair[name] item.name ?: ; pair[value] item.value ?: ; [items addObject:pair]; } info[items] items; NSError *error nil; NSData *data [NSJSONSerialization dataWithJSONObject:info options:0 error:error]; if (error nil) { NSString *json [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]; if (g_unityReady) { UnitySendMessage(DeepLinkProxy, OnDeepLinkNativeMessage, [json UTF8String]); } else { g_cachedDeepLinkJson json; } } } end为了给 C# 侧提供入口需要再加一层 C 函数包装。这里用静态缓冲区返回给托管层避免内存生命周期问题。extern C { void DeepLink_UnityReady(void) { [DeepLinkBridge setUnityReady:YES]; } const char *DeepLink_ConsumeCachedDeepLink(void) { static char buffer[8192]; buffer[0] \0; NSString *json [DeepLinkBridge consumeCachedDeepLink]; if (json.length 0) { const char *utf8 json.UTF8String; if (utf8 ! NULL) { strncpy(buffer, utf8, sizeof(buffer) - 1); buffer[sizeof(buffer) - 1] \0; } } return buffer; } }如果团队以 Swift 为主Objective-C 也可以只做一个薄壳把 C 函数暴露给 Swift。Swift 侧用 URLComponents 解析的写法如下let components URLComponents(url: url, resolvingAgainstBaseURL: false) let queryItems components?.queryItems ?? [] var pairs: [[String: String]] [] for item in queryItems { pairs.append([name: item.name, value: item.value ?? ]) }然后同样转成 JSON 交给同一个桥接入口。C# 侧只用 DllImport 调用上面两个 C 函数不关心底层是 Swift 还是 Objective-C。4. C# 接收端的健壮性设计4.1 接收入口与生命周期管理C# 侧建议只保留一个静态入口避免多个脚本抢收参数。具体做法是写一个 DeepLinkDispatcher MonoBehaviour构造时用单例模式Awake 里调用原生通知引擎就绪然后立刻拉一次缓存。为了防止切场景时接收对象被销毁导致 UnitySendMessage 找不到目标必须把它挂在一个不随场景销毁的对象上DontDestroyOnLoad 是标配。还可以用 RuntimeInitializeOnLoadMethod 在场景加载完成后自动创建即使主场景里忘记挂脚本代码一样能跑。using System; using System.Runtime.InteropServices; using UnityEngine; using UnityEngine.Scripting; [Preserve] public class DeepLinkData { public string url; public string scheme; public string host; public string path; public ParamItem[] items; } [Preserve] [Serializable] public class ParamItem { public string name; public string value; } public class DeepLinkDispatcher : MonoBehaviour { public static event ActionDeepLinkData OnDeepLinkReceived; private static DeepLinkDispatcher _instance; public static DeepLinkDispatcher Instance _instance; [DllImport(__Internal)] private static extern void DeepLink_UnityReady(); [DllImport(__Internal)] private static extern string DeepLink_ConsumeCachedDeepLink(); [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.AfterSceneLoad)] private static void AutoCreate() { if (_instance null) { var go new GameObject(DeepLinkProxy); _instance go.AddComponentDeepLinkDispatcher(); DontDestroyOnLoad(go); } } private void Awake() { if (_instance null) { _instance this; DontDestroyOnLoad(gameObject); } #if UNITY_IOS !UNITY_EDITOR DeepLink_UnityReady(); string cached DeepLink_ConsumeCachedDeepLink(); if (!string.IsNullOrEmpty(cached)) { OnDeepLinkNativeMessage(cached); } #endif } [Preserve] public void OnDeepLinkNativeMessage(string json) { try { var data JsonUtility.FromJsonDeepLinkData(json); if (data null) { Debug.LogWarning([DeepLink] json parse failed: json); return; } OnDeepLinkReceived?.Invoke(data); } catch (Exception e) { Debug.LogError([DeepLink] handle error: e); } } }这里要注意平台宏。编辑器下没有 __Internal所以必须用 UNITY_IOS !UNITY_EDITOR 包住 DllImport 的调用否则在编辑器里运行就会因为找不到符号报错。4.2 参数校验、解码与安全过滤参数到达 C# 侧之后第一件事不是立刻用而是校验。深链参数是从外部环境进来的理论上不可信。我会做四件事。第一是合法性校验。解析 JSON 前先用 IsNullOrEmpty 判断原生传过来的字符串解析用 try/catch 包住解析失败打日志并丢弃。第二是 URL 解码。如果原生侧传的是完整 url 字符串C# 侧需要用 UnityWebRequest.UnEscapeURL 或 Uri.UnescapeDataString 处理 percent-encoding避免中文参数变成乱码但如果原生已经帮你解析好了 items 数组就不要对 value 再解一次否则会把原本已解码的值又转一遍造成脏数据。第三是白名单过滤。比如只接受 scheme 等于 mygame 或者域名属于你配置域名的链接其他一律忽略。这一步能挡住从恶意网页乱调的协议也能避免其他 App 伪造参数干扰游戏逻辑。第四是内容长度限制。深链参数一般不超过几 KB出现超长字符串大概率是异常输入直接丢弃。最后是日志脱敏。深链里如果带 token、user_id、auth_code 这类敏感字段Debug.Log 输出完整 URL 会让日志很容易泄露给玩家和第三方。建议只打印 scheme、host 和参数 key 列表不打印 value线上排查好用又不惹麻烦。4.3 业务分发与事件广播解析完成后的数据会通过静态事件广播出去。业务侧只需要在订阅回调里判断 host 和具体参数分派到自己的模块。举个例子假设游戏有两个深链诉求一个是邀请码一个是活动页。C# 里可以写成private void Dispatch(DeepLinkData data) { switch (data.host) { case invite: if (TryGetParam(data, code, out var code)) { InviteSystem.Instance.BindInviteCode(code); } break; case activity: int pageId 0; if (TryGetParam(data, page, out var pageValue)) { int.TryParse(pageValue, out pageId); } UIManager.Instance.OpenPage(pageId); break; } }业务模块在各自 Awake 或 Start 时订阅 OnDeepLinkReceived拿到数据后做自己的逻辑。不要把 UI 逻辑直接写在 dispatcher 里否则每次新需求都要改这个类迟早变成一堆谁都看不懂的意大利面。这里要注意时机。如果深链在场景还没就绪时就到达业务事件别急着执行 UI 操作。我会给 dispatcher 加一个简单的事件缓存队列先把 OnReceived 事件存进队列等 UI 和逻辑模块完成初始化后统一 flush否则游戏刚启动时能看到一堆界面互相抢入栈画面很奇怪。4.4 Android 端如何对齐虽然标题是 iOS但手游项目基本都会双端我在设计这套协议时会把 Android 的结构一起同步。Android 侧的 Deep Link 主要靠 Intent Filter对应 mygame://invite 的配置像这样intent-filter action android:nameandroid.intent.action.VIEW / category android:nameandroid.intent.category.DEFAULT / category android:nameandroid.intent.category.BROWSABLE / data android:schememygame android:hostinvite / /intent-filterUnity Android 侧拿到 intent 数据比较直接在 C# 里用 AndroidJavaObject 反射拿 Activity 的 Intent再取 dataString#if UNITY_ANDROID !UNITY_EDITOR using (var player new AndroidJavaClass(com.unity3d.player.UnityPlayer)) { var activity player.GetStaticAndroidJavaObject(currentActivity); var intent activity.CallAndroidJavaObject(getIntent); string data intent.Callstring(getDataString); if (!string.IsNullOrEmpty(data)) { // data 形如 mygame://invite?fromwechatcodeabc123 } } #endifAndroid 的冷启动时序也类似UnityPlayerActivity 的 intent 在 Application 启动时就能拿到但 C# 脚本可能需要等待第一个场景创建后才能读取所以同样推荐懒拉取加缓存。我用这套思路做过双端统一原生端负责把 URL 解析为同结构 JSONUnity 侧共用同一套 C# 解析和分发代码iOS 和 Android 只是桥接实现不同业务层完全无感。这对后续增加新的深链入口非常友好。5. 排查实录与验收清单5.1 高频问题与解决方案做深链集成这一年多我把遇到最多的真实问题整理成了表格比官方文档里的 troubleshooting 贴近实际得多。症状大概率原因处理方式Universal Links 点击后直接打开网页AASA 文件路径错误或内容不匹配curl 验证文件检查 appIDs 和 pathsUniversal Links 时好时坏服务器返回了非 application/json 的响应统一返回头去掉 BOM 和多余空白URL Scheme 唤起时弹两个 App 选项Scheme 被其他应用占用换一个更长更唯一的协议名冷启动后 C# 拿不到参数只用了 UnitySendMessage 推送没有缓存拉取补上 isUnityReady 和缓存消费IL2CPP 下接收函数被裁剪方法没有保留标记加上 [Preserve] 或用别的引用方式防止裁剪热启动收到两次同样的参数推送和拉取同时执行拉取后清空缓存推送和拉取共用一个消费标记中文参数变成乱码原生没有处理 percent-encoding用 NSURLComponents 解析 queryC# 别二次解码跳转后游戏崩溃在 Unity 未 ready 时调用 UnitySendMessage全部改走缓存ready 后再投递这些问题里冷启动丢参数是我见到最多、也最容易被忽视的。很多人测试时都是 App 挂在后台点链接那是热启动当然一切正常一旦杀掉 App 再从外部点链接参数就消失了。所以验收时三种时序都必须测不能让热启动的顺利掩盖冷启动的问题。5.2 调试与日志追踪技巧每次联调我都会开一个专门的 DeepLink 日志通道。原生侧在 handleURL 里打印原始 URL在 setUnityReady 里打印是否送出了缓存C# 侧在 OnDeepLinkNativeMessage 入口打印收到的是哪种来源Push 还是 Pull解析完成后打印最终对象。三处日志一对照链路哪个环节断了立刻清楚。Xcode 断点调试时直接给 application:openURL 和 continueUserActivity 下断点然后在 lldb 里执行 po url 看原始链接。系统跳转如果没触发回调优先怀疑 URL 类型和配置如果触发了但 C# 没反应把断点挪到 UnitySendMessage 附近确认 objName 是否写错了。还有一个接地气的土办法在 iOS 备忘录里粘贴深链长按链接看菜单如果出现“在‘xxx’中打开”说明 scheme 或 universal link 的注册是成功的如果只有“拷贝链接”那就是系统压根没识别到你的 App。这个方法能快速定位问题不用反复打包。5.3 上线前的自查清单最后是一张自查清单我每次打包提审前都会过一遍确认 Xcode 里 Associated Domains 已开启且域名以 applinks: 开头确认 AASA 文件线上可访问Content-Type 为 application/json确认 appIDs 是 TeamID 加 Bundle ID 完整拼接确认 URL Scheme 全局唯一且与三方平台不冲突确认 AppDelegate 和 SceneDelegate 的 openURL、continueUserActivity 都有接确认原生桥接有内存缓存和消费机制冷启动能拉取确认接收 GameObject 挂在 DontDestroyOnLoad 对象上确认 IL2CPP 下接收方法没有被裁剪真机分别验证冷启动、热启动、后台恢复三种时序真机验证带中文、特殊符号参数的深链乱码情况确认敏感参数没有完整打进日志我个人实际做下来的体会是Deep Link 这套东西技术上并不难难的是把时序和兼容性做扎实。链接能唤起来只是下半场参数能不能完整走到 C# 业务层才是真正分高下的地方。如果项目刚起步先把冷启动缓存拉取这个机制定好后面接多少渠道、加多少活动链接都只是在 JSON 里多几个字段的事不用反复返工。最后再分享一个小技巧所有回调入口都对 url 做一次日志打印平时看不上眼线上排查时这三行日志能帮你少熬好几个神经衰弱的晚上。
网站建设高端定制企业官网