接手 iOS 买量项目时,市场丢过来一个需求:Safari 广告页点一下,装了 App 的直接打开进活动页,没装的就先去下载页。我当时以为这只是配置一个链接的事,结果从 iOS 系统到原生壳再到 Unity 引擎,每一层都有各自的脾气,配置差一点就“点了没反应”,参数传错环节就“冷启动丢消息”。这篇文章我把 Unity iOS 手游接 Deep Link 的完整链路拆开讲:URL Scheme 和 Universal Links 两套机制怎么选、苹果后台和 AASA 文件怎么配、原生层拿到链接后怎么安全投递给 C#,以及冷启动、热启动两类场景分别怎么处理。内容基于我在 iOS 包上实际接过的流程,适合正在接 Deep Link、或者接完总感觉不稳定的 Unity 手游项目参考。
1. URL Scheme 与 Universal Links,差的不是一点半点
1.1 URL Scheme 的启动链路与最大短板
URL Scheme 是你最早在 iOS 上能用到的唤起方式,原理就是给 App 注册一个自定义协议,系统看到一个mygame://这样的请求,就去查有没有 App 声明了这个 scheme,有就直接唤起来。
mygame://open?activityId=123&source=safari这个方案在早年几乎是唯一选择,实现也简单,在 Info.plist 里配一下CFBundleURLTypes就行。但它的短板在真实业务场景里非常致命:
第一,用户从 Safari 点 scheme 链接,系统会弹确认框。iOS 从很早的版本开始,对外部 App 的 scheme 唤起就加了“是否打开”的二次确认。手游买量链路最讲究顺畅,弹一下框,转化率就掉一截,这还没算误触“取消”直接流失的。
第二,用户没装 App 时,scheme 链接会被 Safari 直接判定为“无法打开网页”。它是真的打不开,而不是优雅地转到你的下载落地页。你最多通过 JS 在页面侦测失败后再跳 App Store,这种“先报错、后补救”的体验本身就是用户流失点。
第三,微信等常用 App 内置浏览器会拦截第三方 scheme。很多买量链接是在微信公众号、朋友圈里传播的,点开之后微信只放行自家业务和白名单,你的 scheme 会被当成非法跳转直接拦掉。
所以 scheme 不是不能用,而是只能当兜底。真正的主链路,得靠 Universal Links。
1.2 Universal Links 的校验原理与 AASA 文件
Universal Links 是 iOS 9 引入的能力,核心思路是:用一条普通的 HTTPS 链接同时代表“网页地址”和“App 唤起点”。系统检测到你点的 URL 属于某个 App 声明的关联域名,就不再打开 Safari,直接唤醒 App,并把这个完整 URL 原样传给 App 侧。
它的合法性建立在“双向声明”上:
- App 侧:Xcode 里开启 Associated Domains,声明
applinks:yourdomain.com。 - 服务器侧:在域名根目录或
.well-known目录放一个apple-app-site-association文件,里面写明TeamID.BundleID和允许唤起的路径。
这两个声明都对上,系统才会信任这条链接。文件格式是纯 JSON,长这样:
{ "applinks": { "apps": [], "details": [ { "appID": "TEAM12345.com.yourcompany.yourapp", "paths": [ "NOT /admin/*", "/activity/*", "*" ] } ] } }这里有几个细节很容易被忽略:
appID必须是TeamID.BundleID的组合,不是苹果后台里看到的 App 前缀。TeamID 在开发者账号后台右上角查看,BundleID 要和 App 实际打包用的完全一致,一个字符都不能错。paths匹配规则支持*匹配任意字符、?匹配单个字符,NOT前缀表示排除。系统按数组顺序匹配,命中任意一条就停止,所以要把排除项放前面。我上面写法里/admin/*是排除的,/activity/*是业务页面,*兜底其他所有路径。- iOS 13 之后苹果对 AASA 文件的抓取加了 SSRF 防护,文件必须静态提供,不能根据请求参数动态生成,也不能有 301/302 重定向链,更不能指向内网地址。CDN 上配了动态内容的,基本都会被判定无效。
- 系统会缓存关联文件,改完配置不是立刻生效。实测缓存时间不稳定,有人十几分钟就刷新,有人等了大半天。最快的强制刷新办法,是卸载重装 App,装好之后系统会重新拉一次。
1.3 我的选型结论:主攻 Universal Links,Scheme 只做兜底
在项目里我最终定下来的方案是:Universal Links 作为主唤起链路,URL Scheme 保留但只用于特定兜底场景。
如果你嘴上说不支持老版本 iOS,Universal Links 是 iOS 9 才有,2025 年还考虑 iOS 8 的机型没什么意义,完全可以放弃 scheme 主链路。但 scheme 没法全删,原因有两个:
- 部分第三方归因平台在特定场景和旧版 SDK 回调里,可能触发 scheme 唤起。
- 自家企业内部跳转(比如从另外一个 App 唤起)用 scheme 更直接,不用非走一遍域名校验。
需要明确的是:Universal Links 只管“已装 App 时唤起”,管不了“未安装用户装完后再自动唤起”。用户先点了链接、再下载安装,等他打开 App,刚才那条链接的“落地页信息”不会自动递进来,这属于安装归因的领域,得靠 AppsFlyer 这类归因 SDK 结合落地页参数去服务端换取。很多做产品的人在这块想当然,容易把需求往下游推,实际是两套体系。
2. 苹果后台、Xcode 工程与 AASA 文件的三方联动配置
2.1 Associated Domains 能力开启与 entitlement 的坑
在 Xcode 里给工程开启 Associated Domains,路径是 Target -> Signing & Capabilities -> 左上角加号 -> 选 Associated Domains,然后在列表里填写applinks:yourdomain.com。
注意这里有个很容易踩的坑:域名前面不能加https://。你写applinks:https://yourdomain.com是错的,正确写法就是applinks:yourdomain.com。
开启之后 Xcode 会生成一个.entitlements文件,里面真正生效的核心是这段:
<key>com.apple.developer.associated-domains</key> <array> <string>applinks:yourdomain.com</string> </array>而且Associated Domains 是一个 Capability,会在 Provisioning Profile 里体现。如果你用的是手动签名,开发者后台的 App ID 必须勾选 Associated Domains,然后重新生成描述文件下载。忘掉这一步,Xcode 里配得再漂亮也没用,真机点链接照样没反应,还不会报错。
做 Unity 项目更要小心一个问题:Unity 每次重新导出 Xcode 工程,工程文件是全新生成的。如果你不是每次构建完都在 Xcode 里手动加一次 Capability,那么 CI 自动构建出来的包大概率没有 Associated Domains。项目里我最后是加了PostProcessBuild脚本,在导出完成后自动往project.pbxproj注入 entitlements 文件并关联 target,这样无论本地构建还是 CI 构建都能保持一致。
#if UNITY_IOS using UnityEditor.Build; using UnityEditor.Build.Reporting; using UnityEditor.iOS.Xcode; public class IOSPostProcess : IPostprocessBuildWithReport { public int callbackOrder => 0; public void OnPostprocessBuild(BuildReport report) { if (report.summary.platform != BuildTarget.iOS) return; string projectPath = report.summary.outputPath + "/Unity-iPhone.xcodeproj/project.pbxproj"; PBXProject project = new PBXProject(); project.ReadFromFile(projectPath); string targetGuid = project.GetUnityMainTargetGuid(); string entitlementPath = "Unity-iPhone/Entitlements.entitlements"; var entitlement = new PlistDocument(); entitlement.root.CreateArray("com.apple.developer.associated-domains").AddString("applinks:yourdomain.com"); entitlement.WriteToFile(report.summary.outputPath + "/" + entitlementPath); project.AddFile(entitlementPath, entitlementPath); project.AddCapability(targetGuid, PBXCapabilityType.AssociatedDomains, entitlementPath); project.WriteToFile(projectPath); } } #endifAddCapability这种写法依赖 UnityEditor.iOS.Xcode 包里提供的 API,在给 entitlements 文件指定 path 时,要跟实际写出的相对路径保持一致,否则 build 的时候 Xcode 可能找不到文件直接报签名错误。
2.2 apple-app-site-association 文件格式与上传细节
AASA 文件可以放在域名根目录,也可以放在.well-known子目录里,两个位置的优先级有差异,我习惯放在.well-known/apple-app-site-association,这也是苹果文档明确推荐的位置。
文件内容不能带任何业务字段,只服务和 Deep Link 关联:
{ "applinks": { "apps": [], "details": [ { "appID": "TEAM12345.com.yourcompany.yourapp", "paths": [ "NOT /admin/*", "/activity/*", "*" ] } ] } }上传之后,用curl验证文件能不能直接拿到,并检查返回的 JSON 语法是否合法:
curl -s https://yourdomain.com/.well-known/apple-app-site-association | jq .实际项目里我遇到过 CDN 给文件包了一层 gzip 或者加了重定向,iOS 端有的版本能容忍,有的版本直接判定失效。稳妥做法是让 CDN 对.well-known路径直接返回 200,不做302跳转,也不按请求头动态压缩。如果用的是对象存储加 CDN,记得关闭“仅通过 CDN 回源鉴权”这类会导致重定向的配置。
另外,AASA 文件本身对大小没有硬性公开上限,但苹果要求文件尽量精简,只保留关联信息。别下意识把业务配置一起塞进去——文件越复杂,拉取解析耗时越长,冷启动时系统校验的延迟也会变长。
2.3 Info.plist 的 URL Scheme 兜底配置
Universal Links 是主链路,但 scheme 兜底配置还是要加。在 Unity 里可以直接通过 Player Settings -> iOS -> Supported URL schemes 里写mygame,也可以等导出 Xcode 工程后手动改 Info.plist:
<key>CFBundleURLTypes</key> <array> <dict> <key>CFBundleURLName</key> <string>com.yourcompany.yourapp.ios</string> <key>CFBundleURLSchemes</key> <array> <string>mygame</string> </array> </dict> </array>这个配置对应的是mygame://开头的那一类链接。scheme 名称最好选一个不常见的,避免和其他 App 的 scheme 冲突。如果你随便写个game,iOS 上很可能有其他 App 已经注册了同样的 scheme,唤起链路会被系统干扰,弹出来的都不是你的 App。
2.4 配置完成后第一步验证:别急着跑 Xcode
很多人配完第一反应是直接跑 Xcode 真机预览,其实正确的顺序是先做“文件侧验证”,再做“工程侧验证”。
上传完 AASA 之后,我习惯先拿curl确认文件能被公网正常访问,并且appID字段确实包含自己的 TeamID 和 BundleID。然后打开一个浏览器,直接输入落地页地址做一次“模拟点击”,确认网页能正常打开。最后再去跑 Xcode 真机,点一次链接,看日志里有没有-application:continueUserActivity:回调。
这看起来很基础,但真能省不少时间。我有一次在真机上排查了半天,最后才发现是 AASA 文件里的appID写错了——一个字母的差距,系统静默失败,又因为缓存机制不会立刻暴露,折腾掉整个下午。先验证文件层,能直接把这类低级错误隔离开。
3. 原生层接收链路:从 AppDelegate 到 UnitySendMessage
3.1 冷启动与热启动的分流:参数必须先落地再上抛
Deep Link 在原生层最核心的问题不是“怎么拿”,而是“拿了之后怎么安全地传给 Unity”。真正麻烦的是启动时序。
我把场景分成两种:
- 热启动:App 已经在后台,Unity 状态完好,收到链接后可以直接原生调 C# 方法。
- 冷启动:App 被链接拉起,系统回调触发的时候,Unity 引擎可能还在初始化阶段,Unity 侧的 GameObject 甚至还没创建完,这时候调 UnitySendMessage 十有八九丢消息。
所以我的原生层处理原则是:任何收到 Deep Link 的时刻,都先存入原生变量,再尝试向 Unity 转发。转发成功了 C# 立即处理;转发失败也没关系,C# 启动后会主动从原生侧把缓存的一段链接读走。
传输协议我定义得很简单:
static NSString *gPendingDeepLink = nil; static void DispatchDeepLink(NSString *link) { if (link.length == 0) return; // 无条件缓存,C# 启动阶段靠主动拉取兜底 gPendingDeepLink = [link copy]; // 如果 Unity 已就绪,走即时推送 if (UnityIsReady) { UnitySendMessage("DeeplinkBridge", "OnReceiveLink", [link UTF8String]); } }UnityIsReady在原生侧是我自定义的一个标志位,做法是继承 UnityAppController 后在startUnity的[super startUnity]之后置为 YES。这样不需要依赖 Unity 内部是否有公开的 ready 接口,逻辑完全可控。
3.2 在 UnityAppController 子类里处理 Universal Links
Unity 导出的 Xcode 工程里,入口类是 UnityAppController,它本身就是 AppDelegate 的实现。拿到一个新导出工程时,先打开Classes/main.mm看UIApplicationMain最后一个参数字符串是谁,那决定了实际生效的 AppDelegate 类。我项目里这一参数默认就是 UnityAppController,所以我直接用继承的方式扩展它:
#import "UnityAppController.h" @interface CustomAppController : UnityAppController @end @implementation CustomAppController - (BOOL)application:(UIApplication *)application continueUserActivity:(NSUserActivity *)userActivity restorationHandler:(void (^)(NSArray<id<UIUserActivityRestoring>> * _Nullable))restorationHandler { if ([userActivity.activityType isEqualToString:NSUserActivityTypeBrowsingWeb]) { NSURL *url = userActivity.webpageURL; NSLog(@"[DeepLink] Universal Link: %@", url.absoluteString); DispatchDeepLink(url.absoluteString); return YES; } return [super application:application continueUserActivity:userActivity restorationHandler:restorationHandler]; } @end然后再去main.mm里把第四参数字符串改成CustomAppController。
这里有一个 Unity 版本差异要提醒:有些版本导出的工程默认不是 UnityAppController,而是 AppDelegate 类。不要只照着网上教程改,先看清你工程里的实际入口。改完主入口后重新构建,确认改动还在,因为 Unity 重新导出工程时 main.mm 会被覆盖回默认内容。
3.3 URL Scheme 的 openURL 回调实现
Universal Links 的回调方法是continueUserActivity,URL Scheme 对应的则是:
- (BOOL)application:(UIApplication *)application openURL:(NSURL *)url options:(NSDictionary<UIApplicationOpenURLOptionsKey, id> *)options { NSLog(@"[DeepLink] URL Scheme: %@", url.absoluteString); DispatchDeepLink(url.absoluteString); return YES; }一个额外的冷启动入口是didFinishLaunchingWithOptions里的UIApplicationLaunchOptionsURLKey:
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { NSURL *url = launchOptions[UIApplicationLaunchOptionsURLKey]; if (url) { NSLog(@"[DeepLink] Launch with URL: %@", url.absoluteString); DispatchDeepLink(url.absoluteString); } return [super application:application didFinishLaunchingWithOptions:launchOptions]; }实际开发中,如果冷启动是 URL Scheme 触发的,系统通常会在didFinishLaunchingWithOptions里给你这个 URL,同时也会在后续走一次openURL回调。如果两边都收到,C# 侧必须做去重,不然一个链接会被处理两次。
3.4 UnitySendMessage 的调用时机与“C# 对象不存在”问题
UnitySendMessage 的原型是:
void UnitySendMessage(const char *obj, const char *method, const char *msg);它依赖的场景是:C# 场景里存在名为obj的 GameObject,且挂在它身上的 MonoBehaviour 里定义了method这个方法。这个找对象的过程发生在 Unity 主线程,触发时指针传过去,消息本身不校验目标是否存在,目标丢失会静默丢掉,不打日志也不报错。
所以如果你的游戏启动场景刚好切走,或者DeeplinkBridge对象被DontDestroyOnLoad挂错了场景,热启动链路也可能出现“链接收到了但 C# 没反应”的情况。我建议把接收器 GameObject 放在启动场景,并且用单例方式常驻:
public sealed class DeeplinkBridge : MonoBehaviour { public static DeeplinkBridge Instance { get; private set; } void Awake() { if (Instance != null && Instance != this) { Destroy(gameObject); return; } Instance = this; DontDestroyOnLoad(gameObject); } }原生侧为了避免竞态,还有一个更稳的配合方案:C# 启动时通过 P/Invoke 主动从原生取 pending 内容,而不是干等 UnitySendMessage。
[DllImport("__Internal")] private static extern string DeepLink_GetPendingLink();原生侧对应的 C 接口也要用extern "C"包住,防止 C++ 符号表改名:
extern "C" const char *DeepLink_GetPendingLink() { if (gPendingDeepLink == nil) return strdup(""); const char *result = strdup([gPendingDeepLink UTF8String]); gPendingDeepLink = nil; return result; }这里用strdup生成一个新的 C 字符串再返回,是避免返回指向临时 Objective-C 对象内部的指针。Unity 的 P/Invoke marshaling 拿到返回值后会自己处理拷贝,但源头数据得是稳定内存,这是个容易忽略的坑。
4. C# 层参数投递:从字符串到游戏内行为
4.1 统一链接协议与 JSON 解析
原生层转过来的参数,本质上就是一条字符串,可能长这样:
mygame://open?activityId=188&source=safari https://yourdomain.com/activity?activityId=189&source=facebook第一步先把 URL 规范化为一个内部结构。我建议团队提前定一个统一的 Deep Link 协议,不管来源是 URL Scheme 还是 Universal Links,解析之后都转成同一个对象。
[Serializable] public class DeepLinkPayload { public string action; // open_activity / open_page / join_room public string activityId; public string source; // safari / facebook / google public string campaign; // 投放计划标识 public string scene; // 目标场景名 }解析方法不要依赖 Unity 的 JsonUtility 直接解析原始 URL,先用一个工具函数把 query 转成字典,再逐字段填充:
Dictionary<string, string> ParseQuery(string query) { var result = new Dictionary<string, string>(); if (string.IsNullOrEmpty(query)) return result; query = query.TrimStart('?'); string[] pairs = query.Split('&'); foreach (string pair in pairs) { int idx = pair.IndexOf('='); if (idx <= 0) continue; string key = Uri.UnescapeDataString(pair.Substring(0, idx)); string value = Uri.UnescapeDataString(pair.Substring(idx + 1)); result[key] = value; } return result; }这个解析函数简单直接,没有把方案搞重。真没必要为了一条链接把第三方网络库和 JSON 库全拉进来。
4.2 启动阶段的事件暂存与场景就绪后的重放
游戏启动时,Deep Link 到达的时机是不可控的。有可能到了主菜单,也有可能还在加载资源、甚至登录流程都还没走完。一定不能在收到回调的瞬间立刻执行“打开活动页”这类业务逻辑,得做事件暂存。
我项目里的流程是这样的:
EnqueueLink收到原始链接,解析成DeepLinkPayload,进入待处理队列。- 主场景加载完成、登录状态就绪这两个条件同时满足时,才真正开始执行动作。
- 如果条件不满足,继续排着,等条件满足后再触发。
具体实现上,我用SceneManager.sceneLoaded事件和登录状态轮询配合:
IEnumerator TryExecutePending() { while (_pending.Count > 0) { if (!IsMainSceneReady()) { yield return null; continue; } if (AccountManager.Instance != null && !AccountManager.Instance.IsLoggedIn) { yield return null; continue; } DeepLinkPayload payload = _pending.Dequeue(); ExecutePayload(payload); } }有个容易漏掉的问题:如果玩家在战斗场景中被 Deep Link 拉起,直接弹活动页会打断操作。合理做法是记录“待展示弹窗”状态,等玩家回到大厅后再弹出提示。所以ExecutePayload里要判断当前场景类型,不是立刻弹 UI,而是把“待处理消息”塞进游戏内消息系统。这块逻辑不复杂,但很多团队不做,上线后就会被玩家骂“打着打着跳个活动页”。
4.3 重复链接拦截、参数清洗与埋点
去重是很容易被忽略的一环。一个链接在冷启动 + 热启动的组合下,有可能被原生层触发多次。C# 侧收到同一个链接处理两次,就可能出现“活动页连开两次”或者“重复请求领奖接口”。
我的做法是用一个固定大小的去重集合:
private readonly HashSet<string> _processedLinks = new HashSet<string>(); private readonly Queue<string> _processedOrder = new Queue<string>(); bool IsDuplicate(string link) { if (_processedLinks.Contains(link)) return true; _processedLinks.Add(link); _processedOrder.Enqueue(link); // 只保留最近 50 条,防止内存无限增长 while (_processedOrder.Count > 50) { _processedLinks.Remove(_processedOrder.Dequeue()); } return false; }参数清洗和埋点一起提。C# 侧在做ExecutePayload之前,记录一个deep_link_received事件,里面带上source、campaign、activityId,方便和投放平台的数据对账。执行成功后再埋一个deep_link_processed。
如果你们接了三方归因 SDK,这些 SDK 自己会上报一部分 Deep Link 参数,但不要完全依赖它做内部流程追溯。投放到归因 SDK 看到的数据和客户端真正收到的链接,经常存在几个小时的延迟。自家埋点的价值在于:点击已经到达了客户端、参数完整、处理结果是什么,这些信息越早拿到越容易定位投放链路问题。
5. 真机实测矩阵与我在项目里踩过的坑
5.1 必测的 5 种场景
Deep Link 的测试特别依赖真机。iOS 模拟器上 Universal Links 的支持一直不太稳定,我碰到过模拟器里点了链接完全不回调、换真机就好的情况,所以以真机结果为准。上线前我至少强制跑这 5 个场景:
| 测试场景 | 操作方式 | 预期结果 | 常见问题 |
|---|---|---|---|
| 冷启动 Universal Links | 杀干净 App 后台,Safari 点落地页链接 | 直接拉起 App,落到目标活动页 | C# 启动阶段没去原生取 pending,参数丢失 |
| 热启动 Universal Links | App 切后台,Safari 点链接 | 立即回到前台并打开目标页 | UnitySendMessage 竞态,目标对象还没 Ready |
| 冷启动 URL Scheme | 杀干净 App 后台,Safari 输入mygame://open?... | 系统弹确认框后拉起 App | 首次弹窗打断体验 |
| 未安装点链接 | 卸载 App,Safari 点 Universal Link | Safari 打开网页落地页 | 落地页缺少下载引导 |
| 微信内点链接 | 微信公众号菜单或聊天记录中打开链接 | 不同 iOS 版本表现不一致,大概率不唤起 | 必须引导用户右上角“在浏览器打开” |
第 5 个场景在手游买量里非常常见。微信对 Universal Links 的唤起有平台层面的拦截策略,这属于外部环境限制,不是你的代码问题。与其在技术上硬刚,不如在产品层面加引导语,让用户在浏览器里打开。
5.2 点链接没反应、参数丢失的常规排查路径
我把出过问题的环节整理成固定排查顺序,每次先按顺序对一遍,不要东查一榔头西查一棒子:
- 确认 AASA 文件可访问:
curl -s https://yourdomain.com/.well-known/apple-app-site-association | jq .,看返回里appID是否和当前包的 TeamID.BundleID 一致。 - 确认工程项目里 Associated Domains 还在:重新导出 Xcode 工程后,很容易被覆盖。检查 Signing & Capabilities 页面,或者打开
.entitlements文件直接看。 - 确认描述文件更新过:手动签名的项目尤其要注意,Capability 变更后旧描述文件不会自动带新 entitlement,需要去开发者后台重新生成。
- 原生回调进没进:在
continueUserActivity和openURL里加NSLog,真机连 Xcode 跑一遍,看日志打印的是哪条链路。 - C# 侧收没收到:在
OnReceiveLink和EnqueueLink里都打日志。如果原生日志里有、C# 日志里没有,重点查 GameObject 名称和 method 签名。 - 缓存问题:改完 AASA 或者重新安装了包,系统缓存可能导致旧的关联关系还在,测试前把 App 卸载重装一次再点。
上述六步走完,90% 的“没反应”都能定位到具体层。剩下的多半是 CDN 缓存配置问题,刷新 CDN 缓存后再验证。
5.3 几个你可能没意识到但很要命的细节
第一,AASA 文件的路径匹配对 query 参数不敏感,但请求仍会带完整 query 过去。也就是说,https://yourdomain.com/activity和https://yourdomain.com/activity?from=facebook,只要 paths 里配了/activity,两条都能唤起,而且 C# 收到的都是完整链接。解析时一定要兼容带参和不带参的情况。
第二,P/Invoke 的函数名不要用 C++ 默认签名。在.mm文件里写 extern "C",编译出来才是 C 符号,Unity 的[DllImport("__Internal")]才能对上。写 C++ 方法的话,符号名会被编译器加上重载修饰,直接找不到函数。
第三,别把 AASA 文件和业务配置放一起。文件加不进任何业务逻辑,重定向和动态内容会导致校验失败。用静态 JSON 上传,其他需求都放另一个文件。
第四,Unity 重新导出会覆盖 main.mm 的修改。我一开始手动改工程,每次构建完都要重新操作一遍,后来改成构建脚本统一改,才彻底治本。凡是要改原生入口的操作,都进工程自动化,别指望人工记忆。
最后给同行的经验
整套跑通之后,再回头看这个需求,价值最高的其实是把“冷启动参数不丢”这件事想明白了。只要原生层先缓存、再转发,C# 层启动时主动拉取,同时保留即时推送路径,三大场景(冷启动、热启动、Unity 初始化竞态)就都能兜住。
还有一点很实际:上线后没人会天天盯着 Xcode 日志,Deep Link 的埋点一定要提前做。把“链接到达客户端”和“活动页成功打开”两个事件拆开埋,哪天投放平台说点击多但游戏内活动参与少,你能立刻定位到是链接断了、还是游戏内跳转逻辑出了问题。这套链路平时没人注意,一上线到大流量投放就会变成事故排查的瓶颈,提前把根基打顺,后面省下的都是加班的命。