
Unity 手游 iOS Deep Link 唤醒全流程从 URL Scheme / Universal Links 到 C# 层参数投递做手游运营或者用户增长的同学应该都有同感一条短信、一个分享卡片、一个广告点击用户点下去之后能不能直接落到游戏里的指定界面直接决定了活动转化率能差出好几个点。这里的核心链路就是 iOS 上的 Deep Link深度链接——用 URL Scheme 或者 Universal Links 把用户从外部拉进 App再把携带的参数一路送到游戏逻辑层。这事在 Unity 手游里尤其容易翻车因为多了一层原生和 C# 的桥接参数一丢就是一场线上事故。这篇文章把我做 Unity iOS 手游 Deep Link 全流程的配置、踩坑、参数投递方案一次性讲透适合正在接邀请活动、分享回流、广告归因的 Unity 客户端开发同学参考也适合刚接触 iOS 原生桥接的朋友当一份实操手册。先说结论整个链路拆开看其实就三段。第一段是 iOS 系统层面怎么识别链接并唤醒 App这是 URL Scheme 和 Universal Links 的配置问题。第二段是 Unity 引擎和原生层之间的接力系统回调把 URL 交到哪、 Unity 什么时机能拿到它。第三段才是真正考验工程能力的地方——冷启动、热启动、场景没加载完、参数特殊字符任何一个环节处理不到位都会导致用户明明点进来了却卡在登录页还查不到任何报错。1. 方案选型URL Scheme 和 Universal Links 怎么选能不能只配一个很多刚接 Deep Link 的同学上来就问现在 Universal Links 是不是已经全面替代 URL Scheme 了我可以直接说目前主流项目基本都是两者并存URL Scheme 做兜底Universal Links 做主力。原因不复杂两个机制解决的问题有交叉但不完全一样放在一起用才是最优解。1.1 URL Scheme 的工作机制和适用场景URL Scheme 是最古老也最直接的方式。你在 Info.plist 里注册一个自定义协议比如mygame://然后 iOS 系统在用户打开mygame://open?room123这样的链接时会直接唤起注册了这个协议的游戏 App。整个过程系统会弹一个确认框询问是否用该 App 打开这是它体验上最大的短板但也正因为有明确的系统确认URL Scheme 被第三方 App 劫持的概率是可控的。我在项目里通常把 URL Scheme 用在两个地方一个是 H5 页面发短信或者扫码场景下的兜底跳转因为 Universal Links 偶尔会被外部 App 内嵌浏览器拦截Scheme 是最后一道保险另一个是跳转到系统设置页、或者和其他已安装 App 之间做定向通信这种场景只有 Scheme 能做。URL Scheme 的格式也很灵活参数可以放在 path 上也可以放在 query 上iOS 不会做任何校验到了 App 里你想怎么解析都行。1.2 Universal Links 的机制、AASA 文件和关联域名Universal Links 是 Apple 从 iOS 9 开始推的标准方案它的核心是你在自己的 HTTPS 域名下放一个apple-app-site-association文件一般简称 AASA文件里声明哪些 path 对应哪个 App 的 bundle ID然后在 Xcode 的 Associated Domains 里配置相同的域名。配置好之后用户点击https://example.com/openGame?room123这类链接如果手机上装了对应 App系统会直接拉起且不弹确认框如果没装这个链接就在 Safari 里正常打开你的网页不会报错。这里要注意一个关键点AASA 文件必须通过 HTTPS 访问且不能有重定向返回值必须带application/json的 Content-Type。文件内容核心就是一个 applinks 字典里面 details 数组标识哪个 App 响应哪些 path 规则。很多第一次配置的同学在这里翻车文件确实放在根目录了但没有处理移动端 Safari 的缓存改了配置之后手机上一两天还是不走 Universal Links实际上 iOS 对 AASA 的缓存非常激进最稳妥的验证方法我后面单独说。1.3 实际项目里的选型建议和兜底逻辑我的习惯是 Universal Links 和 URL Scheme 全量配置但业务逻辑必须优先走 Universal Links。原因很实际Universal Links 没有弹窗用户点击链接到进入游戏的过程干净顺滑体验最好而且 Universal Links 在 iOS 的 App 内嵌浏览器、系统短信、邮件里都能正常工作不会被某些大流量 App 的 webview 劫持。URL Scheme 就留在后面当 Plan B比如某些第三方 SDK 内部跳转、扫码落地页或者用户手机系统版本太旧不支持 Universal Links 时拿来做降级处理。从 iOS 13 开始Apple 其实也收紧了一部分 Scheme 的权限比如限制第三方 App 唤醒系统设置页的能力但对普通游戏自建 Scheme 影响不大。个人建议如果你的游戏需要面向外部投放渠道做大量落地页跳转Universal Links 一定要作为线上主链路来维护因为投放系统很多都要求使用标准的 HTTPS 链接来做点击归因URL Scheme 在部分渠道的数据回传上是有兼容问题的。2. 工程配置Xcode 原生侧和 Unity 导出工程的联动方案定下来之后就是配置落地。这一步的难点不在于信息量大而在于很多 Unity 工程是交给自动化打包机出包的手动改 Xcode 工程一次两次可以每周出包的时候就非常痛苦。所以我们要把原生的配置和 Unity 打包后的自动化修改结合起来做。2.1 URL Scheme 在 Info.plist 里的配置方法和坑URL Scheme 的配置位置在Info.plist的CFBundleURLTypes数组。一个常见的配置项长这样CFBundleURLName写一个标识符CFBundleURLSchemes数组里写你的 scheme 名。一个 App 可以注册多个 scheme但我不建议这么做因为 iOS 的 scheme 匹配是全局的注册得越多被其他 App 误唤起或者你误唤起别人的概率就越大保持一个唯一 scheme 就够用了。如果你是手动出包直接在 Xcode 的 Info 标签页里加就行。但 Unity 项目我强烈建议用PostProcessBuild自动注入脚本里使用PlistDocument读取导出的Info.plist然后添加字典到CFBundleURLTypes。这里有个容易忽略的细节PostProcessBuild的执行时机要选择BuildTarget.iOS的后期处理阶段因为 Unity 导出 Xcode 工程之后已经生成了 plist但还没有进行签名此时修改 plist 是生效的。如果脚本写错执行时机改的是临时文件打包机上根本不会生效。2.2 Universal Links 的关联域名配置与 AASA 文件细节Universal Links 在 Xcode 里的配置在 Signing Capabilities 面板需要添加 Associated Domains 能力然后填applinks:example.com。这里有个很常见的误区很多人只填了域名没注意主域名和子域名的关系。AASA 文件的获取是以你填写的域名为准如果你配置的是applinks:open.example.com那系统会去https://open.example.com/apple-app-site-association拉取文件而不是example.com根域。所以统一管理域名和 AASA 文件的部署路径很重要。再强调一次 AASA 文件本身的关键字段appID必须写成TeamID.BundleID的格式中间是点不是别的符号。paths数组支持精确匹配、通配符和排除符但规则比正则简单得多通配符只有*和?而且不能嵌套使用。文件最大不能超过 50MB但实际建议控制在 64KB 以内因为 iOS 会做大小校验超大文件直接判定无效。正常游戏用不到那么多 path把规则配精确就行。比较稳妥的做法是 AASA 文件同时放到域名根目录和.well-known目录因为 iOS 有两个请求路径双保险能应对不同 iOS 版本的请求差异。放置完成后用 Safari 直接访问这个文件如果浏览器能打开且显示 JSON说明基本部署没问题。2.3 Unity 导出工程里怎么自动注入 Universal Links 配置Unity 导出工程默认没有打开 Associated Domains 能力而且 Xcode 的 Capability 配置本质上是修改工程的 project.pbxproj 文件。在PostProcessBuild里操作 pbxproj 是比较繁琐的好在社区里有成熟的工具库让人可以直接修改工程配置比如通过加载 PBXProject 对象AddCapability 或者手动往 Entitlements 文件加 key。我个人的习惯是用脚本生成 Entitlements 文件并且把com.apple.developer.associated-domains数组写入域名同时要修改工程的 CODE_SIGN_ENTITLEMENTS 设置指向这个文件。这套逻辑如果纯手写很痛苦但好在 Unity 的 iOS 打包本身就会处理签名相关配置只要你脚本在PostProcessBuildAttribute里正确执行就不会和 Unity 原生的签名工程冲突。还有一点记得处理 bitcode 的影响不过现在 iOS 打包已经基本不用 bitcode新项目不需要考虑老项目的话还是建议手动验证一下整个导出工程能不能正常归档。2.4 iOS 13 后的 SceneDelegate 陷阱和缓存问题配置做到这里很多人以为就完事了但我必须专门提一个版本差异问题。iOS 13 之后如果 App 支持 SceneDelegate 生命周期那么某些回调方法不会走到AppDelegate而是会走到SceneDelegate的scene:openURLContexts:。很多从老项目迁移过来的游戏代码还在AppDelegate的openURL里结果线上用户从 iOS 13 开始就发现 Deep Link 部分失效查了几天才发现是生命周期变了。如果你用的是 Unity 官方导出的原生工程它默认没有启用 SceneDelegate所以这个坑对大部分 Unity 游戏来说不会触发。但只要你接入了某些第三方登录、广告 SDK这些 SDK 可能会自动引入 Scene 生命周期支持这时候就不得不处理这个分支。我的建议是原生侧统一封装一个 DeepLinkHandler 单例从AppDelegate和SceneDelegate两边都往它里面投递 URL谁先收到都无所谓最终由它统一转发给 Unity。另一个高频坑就是 AASA 的缓存。iOS 对 AASA 的缓存通常是最少几小时、最多可能一两天。开发测试的时候改了文件想立刻验证最好的方法是App 没启动时用 Safari 打开你的 Universal Link然后等拉起 App 后杀掉 App再修改 AASA 重新测试如果一直不生效可以把手机上的 App 删除重装或者用设置里的关闭 App来强制刷新但这不是 100% 有效。经验之谈测试 AASA 修改最好保留一台专门的测试机不装其他竞品 App减少变量。3. Unity C# 层接参从原生回调到游戏内参数分发原生配置全部到位系统能正常拉起 App 了接下来就是最核心的 C# 层接参。这一块我说实话是最容易出问题、也最考验基本功的部分因为涉及到 iOS 原生和 Unity 的时序问题。用户的 App 可能根本没启动可能已经启动在后台可能正在看战斗结算动画——不同状态下系统把参数给你的时机和形式完全不一样。3.1 冷启动、热启动和后台恢复的区别先明确三个概念。冷启动App 进程不存在用户点击链接后系统拉起进程此时 Deep Link 参数包含在进程启动参数里。热启动App 进程已经在后台存活用户点击链接后系统直接唤起已有进程参数通过系统回调方法传入走不到启动参数。后台恢复App 进程刚被系统杀掉但还残留在后台切换列表中重新拉起时相当于冷启动但速度极快参数来源和冷启动一致。在 iOS 原生层面冷启动时 AppDelegate 的didFinishLaunchingWithOptions里能拿到UIApplicationLaunchOptionsURLKeyUniversal Links 在冷启动时也可能通过continueUserActivity回调抵达但不会出现在didFinishLaunchingWithOptions里。热启动时URL Scheme 走application:openURL:options:Universal Links 走application:continueUserActivity:restorationHandler:。所以原生侧必须同时实现这些方法并且在方法实现里统一把 URL 组装成标准字符串转发到一个共享入口而不是各自处理各自的。3.2 Unity 官方提供的 deepLinkActivated 事件与原理Unity 从很早就提供了Application.deepLinkActivated事件但很多同学不知道的是这个事件在 iOS 上的触发时机比原生回调晚一拍。原因很简单官方实现是在原生侧收到 Deep Link 之后把参数通过 UnitySendMessage 发给一个固定的 GameObject再由引擎层转发给 C# 侧。这个过程跨越了原生和托管两个环境中间必然有时序差。而且要注意deepLinkActivated只在有新的 Deep Link 到达时触发。如果 App 已经起来你注册事件之前收到的链接它是不会补发给你一个历史事件的。所以更稳妥的方式不是只依赖这个事件而是用当前 URL 事件通知的组合。我封装的时候一般会额外保存一份latestDeepLinkURL不管是冷启动还是热启动只要原生层拿到了 URL就立刻同步给 C# 层存到一个静态属性里。等游戏主流程启动完毕再从这个静态属性里取参数去消费。这种做法比纯粹等事件要稳得多因为事件可能会丢但静态值不会丢。3.3 从原生到 C# 的可靠投递缓存优先事件辅助这里讲一下我最终落地的投递方案。原生侧持有一个NSString类型的缓存变量和一组待回调的 block当系统把 URL 传进来时原生先把 URL 保存进缓存同时判断当前 Unity 是否已经 ready。判断方式可以是 Unity 官方接口的UnityFramework是否有效或者你自己在 C# 侧 Awake 之后调用一个原生方法标记 Ready 状态。如果 Native 拿到 URL 时 Unity 还不 ready就先存着等 C# 侧主动调用原生查询方法时原生把缓存的 URL 返回。C# 侧的逻辑是在游戏启动入口注册deepLinkActivated事件同时马上调用原生查询接口拿一次缓存值两边可能拿到同一个 URL要去重处理。去重的标准不能只看 URL 字符串是否相等还要看时间戳或序号因为同一用户连续点击两次同一个链接是真实存在的你不能当成重复请求丢弃。原生侧每转发一次 URL就递增一个自增序号C# 侧用URL 序号二元组去重这样既不会漏事件也不会把同一链接的二次点击误判成重复。3.4 参数解析与协议约定URLDecode 和参数命名参数到达 C# 层后第一件事是解码。iOS 在传递 URL 时不会替你解码 query 里的内容而 H5 侧拼参数时经常会把中文、特殊字符做 URLEncode所以 C# 层必须先用UnityWebRequest.UnEscapeURL或Uri.UnescapeDataString处理一遍整串 query再去逐字段解析。这里有个很典型的坑如果你先按分割再解码那某个参数值里如果包含被编码过的比如%26先分割就会把值切断。正确顺序是先对 query 整体做解码再按分割。但整体解码也会出问题如果参数里有未编码的空格系统可能把空格自动转成解码之后要处理还原成空格的问题。协议命名上我的建议是 Game 内部统一定义一套 Deep Link 协议格式比如scheme://open?scenelobbyuid10086tokenabc解析层只负责把顶层参数转成字典再交给业务层去决定跳转。不要把业务逻辑写在协议解析器里否则以后加一个页面就要改一次解析器。解析完参数之后还要注意参数是否过期的问题链接里如果带了时间戳要在业务层判断有效期防止老链接反复刷奖励。我们线上就吃过这个亏分享链接没加过期时间结果有玩家把链接存下来反复点一个活动奖励领了几十次。4. 实操中的状态机设计与避坑实录接 Deep Link 最怕的不是功能做不出来而是各种时序问题在测试的时候没暴露上了线上才炸。我把自己在实际项目中遇到过的典型问题整理成状态机和排查清单按这个思路去设计可以少走很多弯路。4.1 冷启动时序场景没加载完参数就来了怎么办冷启动最麻烦的情况是用户点击了一个邀请链接拉起 App但此时 Unity 引擎刚刚初始化游戏场景还在加载 Loading 界面你这个时候去执行跳转指令场景里的 UI 都还没实例化跳转必然失败。我一开始的做法是直接在参数到达时执行跳转结果线上反馈一批用户卡在初始场景后来日志一看都是因为跳转时目标场景尚未加载。正确的处理方式是引入一个延迟队列。C# 层拿到 Deep Link 参数后不要立刻消费而是先存到PendingDeepLink结构里等到你的游戏主场景跑完初始化流程、主相机和 UI 根节点都就绪后再去消费这个参数。判断就绪的标准可以是你的全局游戏状态机切到了主界面状态或者某个IsMainReady标志位变为 true。如果参数到达时游戏已经 ready那就直接消费如果没有 ready就排队等待 ready 信号。这个队列要支持先进先出因为用户在外部可能连续点击了多个链接最后一个到达的链接往往是最新鲜的操作业务上应该优先处理最新参数。所以队列可以不设长度每次新参数到达时覆盖掉上一个未消费参数即可或者用堆栈结构取栈顶最新值。4.2 热启动场景App 在后台时链接来了场景切换要小心热启动时游戏进程还在UI 状态完整看起来没什么难度但实际有另一个坑App 从后台被拉起时正在显示的可能是战斗场景、结算界面、商店页面你一上来就按 Deep Link 参数切场景会打断玩家当前操作。比如玩家正打 Boss 打了一半突然点了个广告链接跳回游戏如果直接按参数跳转到活动页玩家的对局就没了这种体验非常伤留存。所以热启动的消费逻辑应该是如果游戏正在战斗状态先把 Deep Link 参数暂存弹一个确认框提示玩家有一个活动入口要进入是否现在前往确认后再执行跳转。在技术上这要求在游戏状态机里暴露一个当前是否可以切场景的接口消费 Deep Link 前先检查这个接口。不能只看场景名因为战斗场景里还有结算流程可能处于不可打断的阶段。这种细节如果不在设计阶段考虑上线后一定会收到玩家投诉。4.3 原生层回调和 C# 事件收不到的双重保险我踩过的最深的一个坑是iOS 原生层明明已经收到了 URL但 C# 侧始终收不到deepLinkActivated。后来发现原因是我们工程里同时有 Unity 官方的 DeepLink 处理逻辑又有自己接的原生桥接两边通过 UnitySendMessage 各自发消息结果其中一个把 Unity 侧接收消息的 GameObject 销毁了另一个就再也发不进去了。类似这种回调被干扰的问题非常隐蔽而且只在特定包体上出现。为了避免这种问题我将所有原生到 C# 的消息统一走一个固定的常驻 GameObject这个对象在游戏启动时创建挂一个 DontDestroyOnLoad并注册接收所有 native 消息的接口。原生侧发送的 UnitySendMessage 全部指向这个对象的方法不要在多个地方各自发。同时C# 侧在注册事件前主动向原生要一次缓存 URL即使事件通道被外力打断也能靠这次主动查询救回来。实际上这套被动收 主动拉的双通道机制上线以后Deep Link 丢失率几乎降到了零我觉得是非常值得借鉴的防御式设计。4.4 常见问题排查速查表我把实战中最常出现的几个问题整理成一个排查表开发的时候直接对照着查。这张表也是我每次带新人做 Deep Link 接入时必发的资料。现象可能原因排查方式点击链接无反应没有任何拉起动作关联域名/AASA 未生效或 URL Scheme 协议头写错使用 Safari 直接打开链接看是否报错检查 AASA 文件是否可访问且 JSON 格式正确单独测试 Scheme 能拉起但从 H5 点击不拉起H5 侧使用了 Universal Link但 AASA 没匹配到 path 规则对照 AASA 里的 paths 与实际链接 path 是否完全匹配注意大小写链接拉起 App 但参数为空冷启动时读取时机太早或 URL 编码/解码处理顺序错误在原生层打断点或输出日志确认系统回调里的 URL 是否完整再确认 C# 侧解析前是否先整体解码同一个链接被处理两次热启动事件与冷启动缓存互相重复使用原生层自增序号 C# 侧去重字典过滤部分 iOS 系统版本收不到回调SceneDelegate 与 AppDelegate 双生命周期问题检查工程是否启用了 Scene 生命周期两端都要实现回调且统一转发到单例参数中的中文/特殊字符变成乱码只做了分段解析没有先整体 URLDecode修改解析顺序整体解码 → 分割字段 → 再次处理号为空格游戏内已安装但 Universal Links 打开后跳转到了网页AASA 缓存或 App 关联域名配置未生效删除重装 App确认 TeamID.BundleID 完全正确确认域名支持 HTTPS 且无重定向5. 测试与上线前检查可复现的验证步骤开发完功能不等于能上线Deep Link 这种链路长、跨端交互多的功能必须有一套完整的测试方案和上线检查清单。我每次提测前都会按下面的顺序过一遍确保万无一失。5.1 本地调试的三种模拟方式和日志输出第一种是 URL Scheme 的本地调试在 Safari 地址栏直接输入mygame://open?scenelobbyuid123回车之后系统会弹窗询问是否打开对应 App点击确认就能拉起这是最快捷的验证方式。注意 Safari 有时会尝试把 scheme 当成搜索词处理需要在地址栏完整输入并敲回车不能省略冒号。第二种是 Universal Links 的调试需要你的域名和 AASA 文件已经部署到正式环境或测试环境然后在备忘录里输入完整链接长按点击或直接点击观察是否拉起。第三种是外部 App 跳转测试用系统自带的 Safari 打开一个包含了 Deep Link 的网页或者用微信/QQ 的聊天窗口发送链接模拟真实用户场景。调试期建议在原生侧加一条基于 NSLog 的输出把每次系统回调的方法名、URL 完整字符串、当前时间都打出来。C# 侧也加一个自定义日志把deepLinkActivated的参数、是否 ready、队列状态都打印到日志文件。这样两端日志合起来一对问题基本能定位到具体环节。5.2 Universal Links 的线上真机验证方法线上验证 Universal Links最标准的方法是直接访问你这个域名的 AASA 文件地址确认 JSON 内容正确后用真机 Safari 打开一条匹配 path 的链接。如果系统拉起 App说明整条链路是通的。如果没拉起而是展示网页接下来逐个排查先看 AASA 的 appID 是不是当前的 TeamID.BundleID再看 paths 是否匹配然后看是不是缓存问题。缓存问题的终极验证手段是把 App 删除重装重新点链接如果恢复说明是缓存导致如果仍然不行那就是配置问题。更深入一点你还可以在 Mac 上用curl模拟 AASA 文件的请求头查看Content-Type是否为application/json同时校验文件是否会被重定向。有些 CDN 服务默认给 json 文件加跳转参数这会导致 AASA 校验失败。如果你的域名走了 CDNAASA 文件的缓存策略最好设置成不缓存或短缓存尽量降低 iOS 拉到陈旧配置的概率。5.3 iOS 14 之后的隐私提示和用户体验细节iOS 14 之后系统对 App 读取剪贴板会弹隐私提示如果你的落地页或 H5 侧在跳转前把参数暂存到剪贴板、等 App 启动后再读取就会触发这个弹窗很影响体验。所以 Deep Link 参数永远不要依赖剪贴板传递要走正规 URL 参数通道。另外在 App 被拉起的过程中如果游戏启动时间超过两三秒用户可能会以为没反应最好在启动阶段展示一个启动页收到 Deep Link 参数后不要立刻藏掉启动页等业务消费完成再切到目标界面这样视觉上更顺滑。如果你的游戏使用了 ATT 隐私弹窗还要注意不要在 Deep Link 拉起后的首帧就弹出 ATT此时用户可能还没反应过来被弹窗打断之后流失率很高。合理的做法是优先展示落地页内容等用户做出有效操作之后再弹授权。这不是 Deep Link 本身的问题但属于接入后最容易伤害转化率的一个交互细节。5.4 打包与线上监控的配合Deep Link 的验证一定要在正式包Release 模式和提审包两种包体上分别测。Release 模式的日志会被裁剪所以线上监控不能依赖日志要在 C# 层把 Deep Link 接收事件和消费结果上报到数据平台。比如用户点击链接、App 拉起到启动完成、参数解析成功、跳转成功这四个关键节点至少各埋一个事件。后面排查用户问题的时候有埋点就能精确定位是系统没拉起还是拉起后解析失败还是跳转被业务逻辑拦截。提审包还有个特殊问题App Store 审核正是以 URL Scheme 和 Universal Links 为重点审核项之一。如果你的 App 注册了 scheme提审时审核员会在 Safari 输入 scheme 尝试唤起如果你没处理好导致唤起后闪退或白屏是有被拒风险的。所以提审前一定要模拟审核员的路径冷启动状态下点 scheme 和 Universal Link确保 App 正常进入游戏主界面不闪退、不卡在加载页。这个流程我建议写进自动化测试的 checklist每次提审都过一遍。6. 最终落地体会与进阶建议整套流程走下来我自己最深的体会是Deep Link 的配置复杂度不在知道几个 API而在于每一层之间都有时序耦合。原生层不知道 Unity 引擎什么时候 readyUnity 引擎不知道游戏业务什么时候可以安全跳转业务层又不知道外部来的参数是不是新的。所以只要有一个环节用简单直接的方式处理就一定会出线上问题。把这些经验总结成一句话原生层只负责不丢参数C# 层只负责时序安全业务层只负责消费参数三个角色各司其职。原生层用缓存变量保证任何时序下参数都不丢C# 层用 ready 标志和待消费队列保证参数不会在错误时机被使用业务层用统一入口保证每个参数只被处理一次。按这个思路去实现不管未来 iOS 版本怎么改、业务需求怎么加Deep Link 这一块都不会成为你反复修 Bug 的地方。最后再分享一个小技巧如果你的游戏以后要接多家广告平台归因 SDK建议 Deep Link 的统一入口设计成可以串接多个消费者的形式也就是一个 Deep Link 参数到达后先让归因 SDK 消费一遍再让业务跳转消费一遍不要互相拦截。我见过一个项目因为广告归因 SDK 把链接参数当成自己的归因数据消耗掉了导致游戏内邀请活动的参数永远接不到排查了很久才发现是两个模块在抢同一个数据源。入口和数据源分离是这个环节减少未来冲突最重要的设计决策。