ARTICLE DETAIL

资讯详情

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

Unity手游iOS端Deep Link接入实战:URL Scheme与Universal Links全解析

Unity手游iOS端Deep Link接入实战:URL Scheme与Universal Links全解析 做手游运营的同学应该都有过这种体验投放后台看着点击率还行结果玩家点了广告页的立即下载/打开游戏却没被唤起白白流失一批本来有兴趣的用户。这时候十有八九是 Deep Link 链路出了问题。Deep Link 就是那个把外部链接、广告位、邀请信息跟 App 内部打通的关键机制在 Unity 手游的 iOS 端尤其绕不开 URL Scheme 和 Universal Links 两条技术路线而且链路一深参数能不能准确落到 C# 层做业务分发又是一道坎。这篇文章我直接把我踩过的坑、改过的代码、验证过的流程全捋一遍从 iOS 原生侧怎么接、AASA 文件怎么放到 Unity 工程里 OC 回调怎么写、C# 层怎么做延迟投递和参数解析一次性讲清楚。如果你是 Unity 手游的客户端开发或者负责买量归因、分享邀请这类业务这篇文章很适合你。我不会只贴一份能跑的代码就完事更多是解释每一步为什么要这么做以及线上最容易炸的几个位置在哪里。1. 为什么手游必须打通 Deep Link场景拆解与核心链路1.1 没有 Deep Link 的时候手游用户路径断在哪先想一个特别常见的场景玩家 A 把游戏里的一个邀请好友得奖励链接分享到微信好友 B 在微信里点开这个链接链接其实是一套 H5 落地页H5 里面有个大大的打开App按钮。如果没有 Deep LinkB 手机上恰好装了游戏点这个按钮大概率只是换了 App 到前台但游戏并不知道 B 是从哪个邀请链接进来的更不知道链接里带着的 inviter 参数。如果 B 没装游戏点按钮就会进 App Store 下载页装完之后打开也只是一次普通冷启动邀请关系彻底丢了。这类外部环境把 App 唤起来同时把参数传进 App 内部的能力就是 Deep Link 的核心价值。手游里的典型场景包括买量归因点击广告后唤起或下载归因平台需要拿到点击 ID、分享邀请跨平台传邀请人 ID、活动落地页唤醒带着活动 ID 直达对应玩法页面、以及 Web 到 App 的微信/支付宝支付回跳订单号投递。任何一条断了轻则用户体验断裂重则归因数据全丢、推广费用白烧。1.2 从链接点击到 C# 层收到参数整条链路要过几道关卡完整链路拆开看大概是这样外部点击一个链接可能是自定义 scheme 的 URL也可能是 https 域名下的 Universal Link→ iOS 系统解析目标 → 如果匹配已安装 App 的注册信息系统唤醒对应 App 并把链接数据交给 AppDelegate/UnityAppController → 原生层在这个回调里拿到 URLString 和参数 → 通过 UnitySendMessage 或 C# 调用的原生接口把数据投递给 Unity 层 → C# 侧解析参数再通过自己的事件机制转发给业务模块。这里每一步都有坑。iOS 系统是否真的允许这个链接唤醒 App取决于你的注册方式App 是冷启动还是热启动回调的入口完全不同原生层把字符串丢给 C# 之后如果 C# 侧没有在合适的时机注册监听器参数就会静默丢失。更麻烦的是Universal Links 的校验文件 AASA 如果配置不对Safari 里点了链接只会打开网站而不是你的 App而且 iOS 对这类文件有缓存策略排查起来非常折磨人。下面我把两条路线分开讲。2. 方案选型URL Scheme 与 Universal Links 怎么选、要不要都接2.1 URL Scheme老牌方案的天生便利与先天弊端URL Scheme 是最老的唤起方案原理就是向 iOS 系统注册一个自定义协议比如mygame://。当用户点击mygame://open?roomid10086的时候iOS 看到协议头是 mygame就去查有没有 App 注册过这个 scheme有就直接唤起。它最大的优点是配置极其简单Xcode 里 Info.plist 加几行客户端不需要服务器配合也不需要域名和 HTTPS 证书适合快速验证链路。但它有几个绕不开的问题系统不会有任何确认打开的友好提示这是历史遗留的设计问题用户从网页里点击这类链接时iOS 有时会弹一个略显突兀的确认框体验不够顺畅。如果 App 没安装scheme 链接直接无效无法自动跳到 App Store。scheme 之间的冲突不好解决如果你的游戏用了mygame://市面上别的 App 也可能注册同样的 scheme结果谁唤起谁看运气。所以我的经验是URL Scheme 用来做 App 内部模块跳转、以及测试环境的快速验证是很好的但作为线上买量、分享转化的主力通道只靠它远远不够。2.2 Universal Links苹果官方认可的标准化唤起方式Universal Links 是苹果在 iOS 9 推出的方案。它的思路是你拥有一个 HTTPS 域名在域名根目录放一个 apple-app-site-associationAASA文件在 Xcode 里把域名绑定到 App 的 Associated Domains 里。iOS 系统定期去拉取这个文件校验通过后用户在 Safari、微信、或者自带浏览器里点击这个 HTTPS 链接如果手机装了你的 App系统就直接唤起 App没装就打开对应的网页去做落地。Universal Links 的体验顶了不少默认不会有那个突兀的确认弹窗、可以在 App 未安装时回落到网页、域名是 HTTPS 的所以不容易被仿冒、还有系统级缓存机制确保了唤起动作的一致性。但它也带来了新麻烦必须有 HTTPS 且证书有效的服务器、AASA 文件必须格式正确并能被公网正常抓到、Associated Domains 必须在 App 的 Entitlements 文件里出现、首次部署后还要等系统缓存刷新不能马上验证成功。2.3 我的建议双注册 双接收我的做法从来不是二选一而是两条都接。入口侧以 Universal Links 作为对外投放和分享链接的主方式URL Scheme 作为老版本 App、以及某些屏蔽 Universal Links 的 App 内浏览器环境下的兜底比如有些 WebView 用户在 iOS 13 之前对 UL 的解析支持并不理想。Unity 工程里同时注册两种回调参数统一组装后丢给 C#业务侧不用关心来源是哪种。维度URL SchemeUniversal Links配置成本低Xcode 改 Info.plist中高域名 AASA EntitlementsApp 未安装无法处理直接失败可回落到网页系统提示弹窗部分环境有基本无安全性易冲突、易被仿冒HTTPS 域名校验较安全回调入口openURL:系列continueUserActivity:适用场景内部跳转、兜底买量、分享、公开投放如果你问我只做一套行不行只要你的游戏要上正规买量渠道Universal Links 是必须的。URL Scheme 则建议保留原因很简单微信内置浏览器对 Universal Links 的支持在不同 iOS 版本上有过反复波折很多老用户的手机可能就卡在某个中间版本双通道能最大程度保证唤起成功率。3. iOS 原生侧配置Xcode 注册与 AASA 文件部署细节3.1 在 Xcode 里给 Unity 工程注册 URL Scheme 和 Associated DomainsUnity 手游的工程结构比较特殊通常是一个 Unity 导出的 Xcode 工程我们需要在 Unity 导出后再去改 Xcode或者配置 Unity 的 PostProcessBuild 脚本自动改。先说最终结果长什么样。URL Scheme 的配置在 Target 的 Info 面板里实际操作对应到 Info.plist核心是CFBundleURLTypes数组keyCFBundleURLTypes/key array dict keyCFBundleTypeRole/key stringEditor/string keyCFBundleURLName/key stringcom.yourcompany.yourgame/string keyCFBundleURLSchemes/key array stringyourgame/string /array /dict /array这里yourgame就是你的 scheme 名字外部链接就是yourgame://开头。CFBundleURLName 一般填 Bundle ID 或者随意一个标识主要用来在系统里标识这个注册项。Universal Links 则是在 Xcode 的 Signing Capabilities 面板里添加 Associated DomainsDomain 填applinks:yourdomain.com不要加 https 前缀不要加路径。完成后工程里会多出一个 entitlements 文件内容大致是这样dict keycom.apple.developer.associated-domains/key array stringapplinks:yourdomain.com/string /array /dict然后你需要一个 HTTPS 服务器域名用这个上面填的。关于 AASA 文件的正确姿势看下一节。还有一个容易踩的坑Unity 每次重新导出 Xcode 工程你手动改的 URL Scheme 和 entitlements 可能被覆盖。保险的做法是写一个 Unity 的IPostProcessBuild脚本在构建完成后自动往 Xcode 工程里注入配置。我习惯在导出后先用 Xcode 手动确认一遍方案可行再回头把改动固化成脚本避免每次发布都重复手改。3.2 AASA 文件标准内容和常见格式错误AASA 文件的全称是 apple-app-site-association必须放在你域名的两个位置之一https://yourdomain.com/apple-app-site-associationhttps://yourdomain.com/.well-known/apple-app-site-association两个路径 iOS 系统都会去抓不一定非要建 .well-known 目录但注意文件本身不要加任何.json后缀响应头 Content-Type 建议是application/json。文件内容格式如下{ applinks: { details: [ { appIDs: [ ABCDE12345.com.yourcompany.yourgame ], components: [ { /: /open/*, comment: 匹配以 /open/ 开头的路径 }, { /: /invite/*, exclude: true, comment: 排除不需要唤起 App 的路径 } ] } ] } }appIDs 的格式是 Team ID 加 Bundle Identifier。很多人第一次配置容易把 Team ID 写成证书的常用名称或者把 Bundle ID 写错成 App 显示名。可以这样确认登录苹果开发者后台找到你的 App 的 App ID 前缀就是 Team ID。组件里的/和exclude支持通配符但注意 iOS 对中文字符路径、含.的路径处理并不友好线上建议用全 ASCII 的路径段做匹配。另外很多人以为 AASA 文件带上paths字段就行。从 iOS 13 开始苹果推荐用components但老系统也识别paths你可以同时保留两个以兼容。3.3 AASA 部署后的自检方法以及系统缓存的坑文件部署完成之后可以用命令行确认 AASA 是否可正常访问curl -s -o /dev/null -w %{http_code} https://yourdomain.com/apple-app-site-association返回 200 只是第一步。真正的坑在于iPhone 上 Safari 打开你的链接后行为可能跟预想不一致因为 iOS 对 AASA 有缓存而且缓存刷新不是立刻的最长可能需要几天。如果刚部署完测不到不要慌可以尝试开关一次 Wi-Fi切换网络环境会触发重新拉取或者等隔天再测。Debug 阶段也可以用 Xcode 里安装 App 后手动用 Safari 输入完整链接验证。提示AASA 文件必须是 HTTP 200 且经 HTTPS 证书校验通过后才能生效如果服务器配置了 CDN要确保全球节点都能取到最新文件不然海外玩家点了链接可能全部唤醒失败。4. Unity C# 层实现原生回调桥接与参数投递的完整代码4.1 在 UnityAppController 里拦截 Deep Link 回调Unity 导出的 iOS 工程中入口类一般继承自UnityAppController类它是 UnityAppController 也是 UIApplicationDelegate。我们需要处理的事件分为两类热启动App 已经在运行或者后台被链接唤起和冷启动App 进程被杀掉后被链接唤起。在热启动时URL Scheme 会把回调送进application:openURL:options:方法Universal Links 则送进continueUserActivity:方法。冷启动时由于 App 进程还没起来统一要在application:didFinishLaunchingWithOptions:里从 launchOptions 中取出对应的 key。我用一张表说明要处理的回调入口状态URL Scheme 的回调Universal Links 的回调热启动openURL:options:continueUserActivity:restorationHandler:冷启动didFinishLaunchingWithOptions:中取UIApplicationLaunchOptionsURLKey同样在 launchOptions 中取UIApplicationLaunchOptionsUserActivityDictionaryKey实际代码里我们会创建一个继承 UnityAppController 的类在里面重写这几个方法。为什么不是直接在 UnityAppController 上改因为 Unity 引擎每次导出都会重新生成这个文件手动修改容易丢而新建子类可以保持持久。在 Build Settings 里把il2cppCode Generation 相关的 Override Class Name 填成你的子类名即可Unity 会优先使用这个类作为 AppController 的子类。4.2 OC 代码拦截、组装、投递三步走我在工程里维护一个 DeepLinkNativeBridge 的 Objective-C 实现外层挂一个 UnityAppController 的子类。直接贴核心方法// DeepLinkAppController.h #import UnityFramework/UnityFramework.h interface DeepLinkAppController : UnityAppController (void)processDeepLink:(NSString *)urlString; end// DeepLinkAppController.m #import DeepLinkAppController.h implementation DeepLinkAppController - (BOOL)application:(UIApplication *)application openURL:(NSURL *)url options:(NSDictionaryUIApplicationOpenURLOptionsKey, id *)options { // URL Scheme 热启动回调 [DeepLinkAppController processDeepLink:url.absoluteString]; return YES; } - (BOOL)application:(UIApplication *)application continueUserActivity:(NSUserActivity *)userActivity restorationHandler:(void (^)(NSArrayidUIUserActivityRestoring *restorationHandler))restorationHandler { // Universal Links 热启动回调 if ([userActivity.activityType isEqualToString:NSUserActivityTypeBrowsingWeb]) { [DeepLinkAppController processDeepLink:userActivity.webpageURL.absoluteString]; } return YES; } - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { // 冷启动URL Scheme NSURL *url launchOptions[UIApplicationLaunchOptionsURLKey]; if (url ! nil) { [DeepLinkAppController processDeepLink:url.absoluteString]; } // 冷启动Universal Links NSDictionary *userActivityDict launchOptions[UIApplicationLaunchOptionsUserActivityDictionaryKey]; if (userActivityDict ! nil) { NSUserActivity *activity userActivityDict[UIApplicationLaunchOptionsUserActivityKey]; if ([activity.activityType isEqualToString:NSUserActivityTypeBrowsingWeb]) { [DeepLinkAppController processDeepLink:activity.webpageURL.absoluteString]; } } return [super application:application didFinishLaunchingWithOptions:launchOptions]; } (void)processDeepLink:(NSString *)urlString { if (urlString nil || urlString.length 0) return; // 这里的关键是App 可能冷启动C# 层的 GameObject 还没创建出来 // 直接 UnitySendMessage 会丢消息。所以要做延迟处理。 dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ const char *urlC [urlString cStringUsingEncoding:NSUTF8StringEncoding]; extern void UnitySendMessage(const char *obj, const char *method, const char *msg); UnitySendMessage(DeepLinkManager, HandleDeepLink, urlC); }); }这里有几个关键点我要强调延迟 1 秒投递是个笨但有效的办法。在冷启动场景下UnityC# 侧的DeepLinkManagerGameObject 是由场景加载创建的didFinishLaunching 被调用时场景还早着呢。延迟投递能保证发消息的时候接收方已经存在。如果你不想硬等 1 秒可以在 C# 侧主动向原生发起拉取请求比如 C# 启动完成后再调用GetPendingDeepLink()。我最终采用的是原生暂存 C# 主动拉取双重保障两者不冲突。代码里我用了一个静态变量暂存最新的 URLC# 随时可以取static NSString *g_pendingDeepLink nil; (void)processDeepLink:(NSString *)urlString { if (urlString.length) { g_pendingDeepLink [urlString copy]; } // 原有的 UnitySendMessage 逻辑保留 // 如果 C# 侧还没就绪发送失败也无所谓因为 C# 会来主动 pull。 } (NSString *)takePendingDeepLink { NSString *link g_pendingDeepLink; g_pendingDeepLink nil; return link; } (const char *)getPendingDeepLink { return g_pendingDeepLink ? [g_pendingDeepLink UTF8String] : ; }4.3 C# 侧接收先拉取再等推送双保险不丢参数配套 C# 类我挂在名为 DeepLinkManager 的 GameObject 上。它做了三件事场景 Awake 时注册原生回调拉取接口、接收 UnitySendMessage 推送、把 URL 解析成结构化参数并通过 C# 事件广播。using UnityEngine; using System; using System.Collections.Generic; public class DeepLinkManager : MonoBehaviour { public static DeepLinkManager Instance { get; private set; } public event Actionstring OnDeepLinkReceived; [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)] private static void AutoCreate() { var go new GameObject(DeepLinkManager); DontDestroyOnLoad(go); go.AddComponentDeepLinkManager(); } private void Awake() { Instance this; #if UNITY_IOS !UNITY_EDITOR // 1. 主动拉取原生侧暂存的 Deep Link解决冷启动丢消息问题 PullPendingDeepLink(); #endif } private void OnDestroy() { if (Instance this) Instance null; } // 原生通过 UnitySendMessage 调用的方法 public void HandleDeepLink(string url) { ProcessUrl(url); } #if UNITY_IOS !UNITY_EDITOR [System.Runtime.InteropServices.DllImport(__Internal)] private static extern string GetPendingDeepLink(); private void PullPendingDeepLink() { string pending GetPendingDeepLink(); if (!string.IsNullOrEmpty(pending)) { ProcessUrl(pending); } } #endif private void ProcessUrl(string url) { if (string.IsNullOrEmpty(url)) return; Debug.Log($[DeepLink] raw url: {url}); // 标准化协议对齐不同入口可能传入的 scheme/applink 差异 OnDeepLinkReceived?.Invoke(url); } }把 DontDestroyOnLoad 和 RuntimeInitializeOnLoadMethod 放在一起是为了让这个管理器不依赖场景里某个对象是否存在。即便 Unity 场景切换、GameObject 被销毁重建也能保证单例可靠。4.4 参数解析Query String 解码与协议字段提取拿到 URL 字符串之后通常需要转成业务参数。常见链接例子yourgame://open?roomid10086inviter9527fromsharehttps://yourdomain.com/open/room?roomid10086inviter9527前面那个用自定义 scheme后面那个是 Universal Links最后落到 App 内的 URL 是同一个 HTTPS 链接。C# 端两种都可以统一用 System.Uri 来解析但注意 Unity 的 System.Uri 对自定义 scheme 解析也是支持的public static Dictionarystring, string ParseQueryString(string url) { var dict new Dictionarystring, string(); if (string.IsNullOrEmpty(url)) return dict; string queryPart string.Empty; int queryIndex url.IndexOf(?); if (queryIndex 0 queryIndex url.Length - 1) { queryPart url.Substring(queryIndex 1); } string[] pairs queryPart.Split(); foreach (var pair in pairs) { if (string.IsNullOrEmpty(pair)) continue; string[] kv pair.Split(new[] { }, 2); string key Uri.UnescapeDataString(kv[0]); string value kv.Length 1 ? Uri.UnescapeDataString(kv[1]) : string.Empty; dict[key] value; } return dict; }这里有个我踩过的坑直接使用 WWW.EscapeURL / WWW.UnEscapeURL 处理某些特殊字符比如 URL 里的号会出问题。号在 query string 里按规范代表空格但真实业务里有人把当作普通加号来编码。处理时最好明确约定参数值统一走 URL Encode客户端统一用 UnescapeDataString 解码不要在链接里直接拼原始 JSON 或者带空格的中文文本。5. 参数投递的时序与业务落地冷启动、热启动和延迟回调5.1 冷启动时序为什么最容易丢参数用户点击邀请链接时如果 App 进程已经在跑回调是即时的C# 侧几乎立等可取。但如果是杀进程后的冷启动整个时序是这样的系统唤起 App 进程 → didFinishLaunching 回调执行 → Unity 引擎初始化 → 场景加载 → C# 脚本 Awake 执行 → C# 侧才具备接收参数的条件。与此同时原生侧 didFinishLaunching 里如果用 UnitySendMessage 立刻把 URL 发出去消息在 C# 还没注册接收方时就直接丢了。我最早踩这个坑就是因为在 didFinishLaunching 里直接调用了 UnitySendMessage结果冷启动场景下参数十次丢九次。后来改成延迟 1 秒加 C# 主动拉取之后才稳定。另外注意如果场景里使用了 AB 包加载或者资源更新流程C# 侧注册接收方的时间可能更晚延迟 1 秒不一定保险主动拉取才是最稳的保底方案。5.2 热启动时序需要小心覆盖式投递热启动又分两种一种是 App 挂在后台被链接唤起这时回调也会进入openURL:或continueUserActivity:另一种是 App 已经在前台点击链接后系统直接把 URL 回调过来这时用户可能正在游戏里操作。热启动的主要风险不是丢消息而是消息覆盖。如果用户在短时间内从多个入口点进来比如先点了分享链接又点了广告链接你存的g_pendingDeepLink只有最新一条。业务上要做的是每条 URL 到达后立刻投递并且 C# 侧在事件回调里根据业务去重而不是简单用最新一条覆盖未处理的邀请逻辑。代码层面我做了个简单队列private Queuestring _pendingLinks new Queuestring(); public void HandleDeepLink(string url) { lock (_pendingLinks) { _pendingLinks.Enqueue(url); } // 处理第一帧避免同一帧多次入队重复处理 } private void Update() { string url null; lock (_pendingLinks) { if (_pendingLinks.Count 0) { url _pendingLinks.Dequeue(); } } if (url ! null) { OnDeepLinkReceived?.Invoke(url); } }入队后放到 Update 里统一处理保证所有外部回调都在 Unity 主线程的帧循环里被消化不会出现线程安全问题。iOS 回调本身在主线程做Unity 的执行也在主线程理论上是同一个线程但为了以后扩展到 Android 或者其他平台统一走主线程消息队列更安心。5.3 业务侧怎么接把 Deep Link 映射成游戏内的行为参数接到手之后业务逻辑通常要处理这几种情况邀请类参数里有 inviter 或者 channel拉起后弹窗展示你已被 XX 邀请买量归因点击 URL 里带 ad_country、campaign_id 等客户端上报给归因 SDK活动页直达参数里有 activityId拉起后直接跳转对应玩法界面支付回跳参数里有 orderId拉起后调用服务端查订单状态我一般会在 C# 侧做一个DeepLinkRouter它订阅DeepLinkManager.OnDeepLinkReceived然后做协议分发的第一步public void Route(string url) { Uri uri new Uri(url); string host uri.Host; // 兼容 Universal Links 的域名 string scheme uri.Scheme; // yourgame 或 https var query DeepLinkParser.ParseQueryString(url); if (query.TryGetValue(activityId, out string activityId)) { UIManager.Instance.OpenActivity(activityId); } else if (query.TryGetValue(roomid, out string roomid)) { GameRoomManager.Instance.EnterRoom(int.Parse(roomid)); } else { // 无有效参数回到大厅 } }需要注意的是不要一股脑在主线程里执行复杂跳转。场景没准备好时跳转会跟 Loading 流程打架。我通常会先检查当前游戏状态登录态、主场景是否就绪如果状态不对就把这次路由缓存起来等全局状态机切换到 Ready 之后再执行。6. 常见问题与排查技巧实录6.1 Universal Links 点击无反应先查这四件事线上遇到点了链接没唤起 App的频率相当高。我会按下面的顺序排查基本几分钟内能锁定问题范围排查项操作常见失败原因AASA 文件可访问性curl 检查确认 200 和内容格式服务器 CDN 缓存旧文件、Content-Type 不对、带 BOM 头appIDs 匹配对比 Team ID Bundle ID 与开发者后台Team ID 写错、Bundle ID 大小写不一致Associated DomainsXcode entitlements 里是否包含 applinks 域名域名多了 http:// 前缀、漏了 applinks: 前缀缓存开关 Wi-Fi 或次日重试iOS 长期缓存旧 AASA 未过期其中 AASA 的 Content-Type 问题比较隐蔽。如果服务器配置成application/octet-stream某些 iOS 版本仍然能识别但也有版本解析异常。统一响应头设置为application/json最稳。6.2 微信内点击链接的异常行为微信内置浏览器对上 Universal Links 的处理一直有点微妙。早期版本支持有限现在大版本好一些但如果用户微信里打开了分享链接点了之后没有拉起 App、反而在微信内打开了一个网页大概率是两种原因一是该链接没有正确配置为 Universal Link微信直接当普通网页打开二是微信的域名屏蔽策略被判定为诱导或外链风险后被拦截。为了稳线上的分享链接我一般做两步跳转H5 页面先加载然后页面通过 JS 调用window.location.href跳转到 Universal Link 地址。如果微信内识别不了页面会提供在浏览器打开的按钮用户只要跳到系统 Safari 后再点击一次就能走通系统级的唤起逻辑。6.3 收不到参数冷启动丢消息与 C# 侧时序问题如果 App 能唤起、但业务参数始终没拿到先不要怀疑原生配置更多是 C# 侧时序问题。快速定位的三种手段在原生 code 的 processDeepLink 里加 NSLog确认系统回调有没有执行在 C# 的 Awake 和 HandleDeepLink 里分别加 Debug.Log确认 UnitySendMessage 是否到达使用主动拉取接口在 C# 场景 Load 完成后手动拉一次我见过一个项目就是 C# 侧把接收函数写成了私有方法UnitySendMessage 动态调不到私有方法于是怎么都收不到。UnitySendMessage 要求目标 GameObject 上挂的组件里方法必须是可以被反射触发的C# 默认方法访问权限如果是 private反射是能找到的但 Unity 的 UnitySendMessage 实际走的是引擎的消息派发机制部分版本对 private 方法支持不稳定所以写 public 最保险。另一个隐蔽问题某个 iOS 版本下UnitySendMessage 的第一个参数填 GameObject 名字但场景同层级存在重名 GameObject 时消息可能被派发到不确定的那个对象上。Unity 官方说这种情况未定义行为所以 DeepLinkManager 这个对象名全局唯一很重要。6.4 URL 参数里的中文、JSON、特殊字符怎么处理参数编码是每个新接手 Deep Link 项目的人都会反复踩的坑。正确姿势是参数值拼接前做一次 URL Encode解析时再 URL Decode。// 拼邀请链接时参数值不能裸放 string inviterName 老陈; string encoded Uri.EscapeDataString(inviterName); string link $https://yourdomain.com/invite?inviter{encoded}; // 解析端 string raw query[inviter]; // 老陈Json 字符串要嵌入 URL 时必须先 EscapeDataString 整段 JSON并且客户端解码后再 Newtonsoft.Json 反序列化不要手动字符串切割。如果业务允许我更推荐把数组、嵌套对象这类结构拆成单个 query 参数比如itemIds1,2,3客户端按逗号切分省去 JSON 编解码出错的概率。7. 后续扩展与个人体会7.1 从 iOS 扩展到 Android 的一点启发Unity 手游在 iOS 接完 Deep Link 之后面向 Android 的适配通常会紧接着提上日程。Android 上不建议直接用自定义 scheme 硬扛Google Play 对 App Links 的校验方式跟 iOS 的 Universal Links 很相似需要放一个assetlinks.json到.well-known目录。如果你服务端的 AASA 文件已经搭好了Android 的 assetlinks 只是在同一个 Web 根目录多放一个静态文件成本不高。代码层面 C# 侧的事件分发和参数解析完全可以复用只需在原生层再实现一份 Java/Kotlin 的 intent filter 接收逻辑。7.2 归因平台与 Deep Link 的配合做买量归因的时候Deep Link 不仅仅是拉起 App它更重要的身份是归因平台回传数据的运输工具。Adjust、AppsFlyer、Branch 这类 SDK 内部都有自己的一套 Deep Link 接收逻辑如果你的工程同时接了归因 SDK 和自己写的桥接层要留意两者不能互相覆盖回调。市面上大部分归因 SDK 会要求你在 AppDelegate 的回调方法里先调他们的接口再处理自己的逻辑顺序错了可能产生归因数据漏采。这里的建议是原生回调入口做统一收口先调用归因 SDK 的接口再调自己的 processDeepLink避免重复代码也避免冲突。我在实际项目中踩完一圈之后最大的体会是Deep Link 不是贴一段代码就能跑通的东西链接曝光、点击数、唤起成功率、参数到达率这四层数据最好都埋点监控。唤起失败不一定是代码问题可能是某个省份的网络访问不了你的 AASA 文件或者老版本 iOS 对微信内跳转行为不一致。没有监控的情况下运营报用户进不来、归因平台报缺失点击、客户端代码看着又很正常最终只能是多方互相甩锅。上线前至少要在 TestFlight 环境里跑一遍冷启动、热启动、网页回跳、微信内打开、分享链接到 Safari 这五类场景并记录日志到文件方便出问题时从用户手里抓日志回溯。最后分享一个小技巧Debug 模式下在 C# 层把收到的原始 URL 打印到屏幕上的一个 Text 组件测试的时候拿两台手机互相点链接、肉眼确认参数是否完整比连 Xcode 看 console 高效得多特别适合让不熟技术的运营同学帮你回归测试。
返回列表