尧图精选

Unity手游 iOS Deep Link 接入全指南:从方案选型到C#参数投递

🕒 发布时间:2026/10/1 5:13:52 📁 来源:尧图网络
Unity 手游做 iOS Deep Link 唤醒这活儿看着不大真接起来坑不少。需求通常长这样运营在浏览器里放一个活动链接用户点了之后装过 App 的要直接拉起游戏并跳转到指定页面没装过的要引导去 App Store。听起来简单但从 URL Scheme 和 Universal Links 两套方案的选择到原生层拦截回调再到把参数安全送到 C# 侧每一步都有讲究。这篇文章就把我实际项目中跑通的完整链路写出来从方案选型到原生配置再到 C# 层参数投递顺带把我踩过的坑和排查方法一并整理给要接 Deep Link 的 Unity 团队一个可以直接抄作业的参考。适合谁看如果你负责 Unity 手游的 iOS 客户端或者小团队没有专职原生开发需要自己搞定拉新、回流、活动页跳转这类需求这篇就是给你准备的。下面是我在三个不同项目里反复调整后沉淀下来的方案。1. 先别急着写代码URL Scheme 和 Universal Links 怎么选1.1 两套方案的底层逻辑URL Scheme 是最古老的唤起方式原理就是给 App 注册一个自定义协议比如mygame://当系统碰到mygame://open/activity这种链接时会去查有没有 App 注册了这个协议有就弹一个系统确认框问用户“要用‘我的游戏’打开吗”用户点允许才拉起 App。这套机制从 iOS 早期就有兼容性最好但有两个明显毛病一是每次唤起都有弹窗多一步确认真实场景里转化率会掉二是如果用户没装 AppSafari 会直接报错“打不开网页”没有体面的兜底入口。Universal Links 是 iOS 9 开始推的方案思路完全反过来它用的是一个普通的 HTTPS 网页链接比如https://dl.mygame.com/open/activity。App 通过 Associated Domains 声明自己对这个域名有处理权系统会去域名服务器拉一份名叫 apple-app-site-association 的 JSON 文件校验通过后点击这个链接就不再打开网页而是无缝直接唤起 App既不弹窗也不跳转体验非常顺。最妙的是如果用户没装 App系统会直接把这个链接当普通网页打开运营同学可以把活动页和下载页放在同一个 URL 上。1.2 为什么不建议只接一种很多文章会告诉你 Universal Links 是未来、URL Scheme 是历史包袱但作为接过多家渠道的过来人我的建议是两套都接理由很实际。首先是兼容性。Universal Links 要求 iOS 9 以上虽然现在 iOS 9 以下的设备几乎没有了但系统的行为并不总是稳定。苹果对 Universal Links 的首次生效是有延迟的App 刚安装完、或者系统还没下载到最新的 AASA 文件时第一次点击链接很可能直接打开了网页而不是 App这是苹果机制本身决定的不是你配置错了。URL Scheme 因为是本地注册不存在“服务器校验”这个环节冷启动唤起的确定性反而更高。其次是渠道方的要求。国内很多广告平台、SDK 做归因统计时给的链接还是 URL Scheme 形式或者只在 URL Scheme 路径下回传参数。如果只接 Universal Links遇到这类渠道就得干瞪眼。反过来微信、QQ 这类 App 的内置浏览器从 iOS 9.2 开始就不再支持 Universal Links 唤起了但 URL Scheme 在部分场景下还能走通。两套都接才能覆盖比较完整的流量场景。1.3 以 Universal Links 为主、URL Scheme 兜底的策略我最终落地的方案是对外投放、运营活动页、短信营销这些正规入口全部用 Universal Links让用户拿到最顺畅的唤起体验URL Scheme 保留作为兜底主要服务两类场景——第一类是渠道 SDK 内部跳转时硬编码的 Scheme 链接第二类是某些无法部署 AASA 文件的临时域名。两套入口在原生层收到后统一走同一个处理函数C# 层完全感知不到来源差异这样维护成本最低。这里要先定一个原则任何 Deep Link 都要能追溯出“从哪个链接进来、带了哪些参数”所以配置方案之前先把 URL 协议设计好别等接完了再回头改。协议设计的事放到后面第 4 节细说你只需要记住选型阶段的决策顺序是链接格式决定 AASA 配置和 Scheme 注册最终都收敛到同一个原生回调。2. iOS 原生层配置从开发者后台到 Xcode 工程2.1 URL Scheme 注入的三种姿势URL Scheme 的配置本质就是往 Info.plist 里写一段 CFBundleURLTypes。Unity 构建 iOS 工程时Info.plist 是从 Unity 的 Player Settings 里生成的如果你只是临时在 Xcode 里加下一次 build 就会被覆盖。所以有三种做法第一种是手动改 Xcode适合调试时临时验证上线打包千万别依赖这个忘一次就漏一次。第二种是在 Unity 的 Player Settings 里通过 Other Settings 的 Bundle Identifier 之外没有直接配 URL Scheme 的入口。所以很多项目会用一个自定义的 Info.plist 模板从Assets/Plugins/iOS/Info.plist覆盖生成但这会导致 Unity 默认的很多键丢失接手维护的人容易一头雾水。第三种是写一个 PostProcess 构建后处理脚本用 UnityEditor.iOS.Xcode 的 PlistDocument 类读取出生成的 Info.plist把 CFBundleURLTypes 追加进去再写回。这是我在生产环境用的方式干净、可控、只增不改不影响 Unity 默认配置。脚本代码在第 2.3 节统一给出。2.2 Universal Links 的服务器与开发者后台准备Universal Links 涉及三个角色开发者后台、服务器、Xcode 工程。很多人只配了 Xcode 里的 Associated Domains忘了服务器上的 AASA 文件或者反过来结果就是点击链接永远只开网页。开发者后台要做的事是确认你的 App ID 开启了 Associated Domains capability。如果你用的是 Xcode 管理签名直接在 Xcode 的 Signing Capabilities 里加 Associated Domains 即可证书会自动更新。这一步的核心产出是一个 entitlementcom.apple.developer.associated-domains值是一个数组里面写applinks:你的域名。服务器那边要放一个文件路径和文件名有严格约定。苹果官方支持两个位置域名根目录的/apple-app-site-association以及/.well-known/apple-app-site-association。我一般两个位置都放内容完全一样防止某些 CDN 或中间层对 .well-known 目录的处理不一致。文件内容是一个 JSON标准格式如下{ applinks: { apps: [], details: [ { appID: TEAMID.com.mygame.ios, paths: [/open/*] } ] } }注意appID是 Team ID 加 Bundle Identifier中间没有点以外的符号Team ID 在开发者后台的 Membership 页面可以看到是一串 10 位大写字母数字混合串。paths是路径匹配规则支持通配符我用/open/*收敛到统一入口避免把主页和其他页面也纳入唤起范围。服务器返回这个文件时还要注意两点响应头Content-Type必须是application/json文件本身不能带注释苹果的解析器对注释的容忍度很低。部署完之后先用浏览器或者 curl 拉一遍确认能访问再继续下一步。2.3 用后处理脚本把配置固化到 Xcode 工程Xcode 里的 Associated Domains、entitlements 文件和刚才说的 URL Scheme 一样也会被 Unity 构建覆盖。所以我把所有原生配置都塞进同一个 PostProcess 脚本里构建完 iOS 工程后自动完成三件事写 URL Scheme 到 Info.plist、生成 entitlements 文件、在 project.pbxproj 里给 target 挂上 CODE_SIGN_ENTITLEMENTS 并添加 capabilities。贴一段精简版代码放在Assets/Editor/DeepLinkPostProcess.csusing UnityEditor; using UnityEditor.Callbacks; using UnityEditor.iOS.Xcode; using System.IO; public static class DeepLinkPostProcess { [PostProcessBuild(100)] public static void OnPostProcessBuild(BuildTarget target, string buildPath) { if (target ! BuildTarget.iOS) return; // 1. 往 Info.plist 注入 URL Scheme string plistPath Path.Combine(buildPath, Info.plist); PlistDocument plist new PlistDocument(); plist.ReadFromFile(plistPath); PlistElementArray urlTypes plist.root.CreateArray(CFBundleURLTypes); PlistElementDict urlType urlTypes.AddDict(); urlType.SetString(CFBundleURLName, com.mygame.ios); PlistElementArray schemes urlType.CreateArray(CFBundleURLSchemes); schemes.AddString(mygame); plist.WriteToFile(plistPath); // 2. 生成/修改 entitlements string entPath Path.Combine(buildPath, Unity-iPhone.entitlements); PlistDocument entDoc new PlistDocument(); if (File.Exists(entPath)) entDoc.ReadFromFile(entPath); PlistElementArray domains entDoc.root.CreateArray(com.apple.developer.associated-domains); domains.AddString(applinks:dl.mygame.com); entDoc.WriteToFile(entPath); // 3. 挂上 entitlements string projPath Path.Combine(buildPath, Unity-iPhone.xcodeproj/project.pbxproj); PBXProject proj new PBXProject(); proj.ReadFromFile(projPath); string targetGuid proj.GetUnityMainTargetGuid(); proj.SetBuildProperty(targetGuid, CODE_SIGN_ENTITLEMENTS, Unity-iPhone.entitlements); proj.WriteToFile(projPath); } }这段代码只做追加不删任何已有配置构建多次也不会重复堆积。注意GetUnityMainTargetGuid()是 Unity 2019.4 之后的 API老版本项目要用TargetGuidByName(PBXProject.GetUnityTargetName())根据自己的 Unity 版本调整。配置到这一步原生层能做的事基本就绪了。接下来关键的桥接逻辑才是重头戏。3. 原生回调到 C# 层桥接方案与代码实现3.1 原生拦截入口自定义 UnityAppControllerUnity iOS 生成的工程里App 生命周期由Classes/UnityAppController.mm里的 UnityAppController 管理。很多教程让你直接改这个文件但构建一次就被覆盖一次纯属给自己埋雷。正确姿势是在Assets/Plugins/iOS/下放一个自定义的 AppController 子类用 Unity 提供的宏IMPL_APP_CONTROLLER_SUBCLASS把它注册为主控制器。这样 Unity 构建工程时这个 Objective-C 文件会被自动编译进去而且不会被覆盖。核心代码如下文件名DeepLinkAppController.mm#import UIKit/UIKit.h #import UnityAppController.h interface DeepLinkAppController : UnityAppController property (nonatomic, strong) NSMutableArray *pendingDeepLinks; end implementation DeepLinkAppController - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionaryUIApplicationLaunchOptionsKey, id *)launchOptions { // 冷启动场景URL Scheme 可能直接放在 launchOptions 里 NSURL *launchURL launchOptions[UIApplicationLaunchOptionsURLKey]; if (launchURL ! nil) { [self forwardDeepLink:launchURL.absoluteString]; } return [super application:application didFinishLaunchingWithOptions:launchOptions]; } - (BOOL)application:(UIApplication *)app openURL:(NSURL *)url options:(NSDictionaryUIApplicationOpenURLOptionsKey, id *)options { // 热启动App 已在后台通过 URL Scheme 唤起 if (url ! nil) { [self forwardDeepLink:url.absoluteString]; } return YES; } - (BOOL)application:(UIApplication *)application continueUserActivity:(NSUserActivity *)userActivity restorationHandler:(void (^)(NSArrayidUIUserActivityRestoring * _Nullable))restorationHandler { // Universal Links 入口 if ([userActivity.activityType isEqualToString:NSUserActivityTypeBrowsingWeb]) { NSURL *url userActivity.webpageURL; if (url ! nil) { [self forwardDeepLink:url.absoluteString]; } } return YES; } - (void)forwardDeepLink:(NSString *)urlString { if (urlString.length 0) return; if (self.pendingDeepLinks nil) { self.pendingDeepLinks [NSMutableArray array]; } [self.pendingDeepLinks addObject:urlString]; if (UnityIsReady()) { // 如果 Unity 已经初始化完毕立即投递给 C# [self flushPendingDeepLinks]; } } - (void)flushPendingDeepLinks { if (self.pendingDeepLinks.count 0) return; NSArray *links [self.pendingDeepLinks copy]; [self.pendingDeepLinks removeAllObjects]; for (NSString *urlString in links) { UnitySendMessage(DeepLinkManager, OnNativeDeepLinkReceived, urlString.UTF8String); } } end IMPL_APP_CONTROLLER_SUBCLASS(DeepLinkAppController)这段代码把所有入口收口到forwardDeepLink:先用 pendingDeepLinks 数组缓存再判断 Unity 是否就绪。UnityIsReady()是 Unity 提供的函数在 UnityAppController.h 里有声明用来判断引擎是否完成初始化。为什么一定要判断因为冷启动时continueUserActivity和openURL可能发生在 Unity 场景还没加载完的时刻这时候调UnitySendMessage目标 GameObject 还不存在消息直接丢掉C# 侧永远收不到。如果你愿意也可以在这个类里用 NSNotificationCenter 同时发一份原生通知方便自己后续接原生侧的数据统计 SDK注意别重复回调就行。3.2 冷启动、热启动与 Unity 时序问题这里专门把时序问题讲透。我把真实场景分成三种第一种App 已经退到后台但进程还活着这时候点链接唤起Unity 肯定是就绪的openURL或continueUserActivity直接转发给 C#就没问题。第二种App 进程被系统杀了用户点击 Universal Links 冷启动。系统拉起进程先走didFinishLaunchingWithOptions然后很快调用continueUserActivity。但 Unity 引擎初始化是异步的场景加载、Mono 运行时启动都需要时间。如果continueUserActivity里直接UnitySendMessage大概率报错。缓存加延迟投递就是为这个场景准备的。第三种URL Scheme 冷启动。系统把链接放在didFinishLaunchingWithOptions的launchOptions[UIApplicationLaunchOptionsURLKey]里也可能再走一次openURL不同 iOS 版本行为不完全一致。我在两个入口都做了处理统一进同一个转发方法反正缓存机制会保证不丢。还有一个隐藏问题Unity 场景切换时承载接收消息的 GameObject 可能被销毁。所以 C# 侧的接收对象必须用DontDestroyOnLoad常驻具体在第 3.3 节实现。注意iOS 13 之后系统支持 SceneDelegate 生命周期部分 Unity 版本生成的工程里Universal Links 不是走 AppDelegate 的continueUserActivity:而是通过 SceneDelegate 的scene:willConnectToSession:options:传入。如果你的工程模板带了SceneDelegate需要确认回调落点。万变不离其宗核心思想仍然是“拦截、缓存、等 Unity 就绪后再投递”。3.3 C# 侧 DeepLinkManager 的实现原生层把链接通过UnitySendMessage(DeepLinkManager, OnNativeDeepLinkReceived, url)投递过来C# 侧就必须有一个名为 DeepLinkManager 的 GameObject挂一个带OnNativeDeepLinkReceived方法的脚本。这个名字和方法名不能拼错拼错了原生层会静默失败没有任何报错提示。我写的管理器是一个带缓存和事件分发的单例核心代码如下using System; using System.Collections.Generic; using UnityEngine; public class DeepLinkManager : MonoBehaviour { public static DeepLinkManager Instance { get; private set; } private readonly Queuestring pendingLinks new Queuestring(); private bool isReady; private string lastLink; public event Actionstring OnDeepLinkReceived; private void Awake() { if (Instance ! null) { Destroy(gameObject); return; } Instance this; DontDestroyOnLoad(gameObject); Application.deepLinkActivated OnUnityDeepLink; } private void Start() { isReady true; FlushPending(); // 如果原生侧还有缓存主动拉取一次 string cached PullPendingLinksFromNative(); if (!string.IsNullOrEmpty(cached)) { foreach (string link in cached.Split(\n)) { if (!string.IsNullOrEmpty(link)) { EnqueueAndProcess(link); } } } } private void OnUnityDeepLink(string url) { EnqueueAndProcess(url); } // 原生层 UnitySendMessage 的落点 public void OnNativeDeepLinkReceived(string url) { EnqueueAndProcess(url); } private void EnqueueAndProcess(string url) { if (string.IsNullOrEmpty(url)) return; lastLink url; pendingLinks.Enqueue(url); FlushPending(); } private void FlushPending() { if (!isReady) return; while (pendingLinks.Count 0) { string url pendingLinks.Dequeue(); OnDeepLinkReceived?.Invoke(url); } } // 供业务方订阅支持迟到的关注者重放最近一条链接 public void Register(Actionstring handler) { OnDeepLinkReceived handler; if (!string.IsNullOrEmpty(lastLink)) { handler(lastLink); } } public void Unregister(Actionstring handler) { OnDeepLinkReceived - handler; } #if UNITY_IOS !UNITY_EDITOR [System.Runtime.InteropServices.DllImport(__Internal)] private static extern string DeepLinkManager_PullPendingLinks(); #endif private string PullPendingLinksFromNative() { #if UNITY_IOS !UNITY_EDITOR try { return DeepLinkManager_PullPendingLinks(); } catch (Exception e) { Debug.LogWarning([DeepLink] pull pending links failed: e.Message); return null; } #else return null; #endif } }原生侧对应的拉取函数负责把缓存队列吐出来用换行符拼接返回后清空队列extern C const char *DeepLinkManager_PullPendingLinks() { DeepLinkAppController *controller (DeepLinkAppController *)GetAppController(); if (controller.pendingDeepLinks.count 0) return NULL; NSMutableString *result [NSMutableString string]; for (NSString *link in controller.pendingDeepLinks) { [result appendFormat:%\n, link]; } [controller.pendingDeepLinks removeAllObjects]; return strdup(result.UTF8String); }这里用换行符拼接是安全的因为合法的 URL 里不允许出现未编码的换行符而参数编码后的内容也不会产生换行。Register方法里重放lastLink是为了解决一个真实问题场景 A 订阅了 Deep Link 并跳转场景 B 加载完成后也想读这条参数但事件已经错过了。重放最近一条链接业务方拿一次就能用省得自己在各个场景里传参。提示我还额外监听了Application.deepLinkActivated这是 Unity 2021 版本开始提供的统一入口部分场景下可以直接拿到启动链接。实际项目里它有时候和原生回调重复我的处理是在EnqueueAndProcess里做去重很难判断所以干脆只在原生回调为主、Unity 内置事件为辅两处都进队列参数本身的幂等处理交给业务方按需去重。4. URL 参数协议设计别让参数在链路中“丢包”4.1 统一 URL 协议让参数不再“流离失所”Deep Link 接得多了就会发现链接格式不统一是最大的维护噩梦。有的渠道给你mygame://activity?idxxx有的给你https://dl.mygame.com/open?actidxxxC# 侧解析逻辑写了一堆 if else最后还是容易漏。我在项目里定了一套规范所有 Deep Link 统一长这样URL Schememygame://open/activity?actId1001uid123456fromsafariUniversal Linkshttps://dl.mygame.com/open/activity?actId1001uid123456fromsafari规则就三条第一段 path 固定是open表示这是一个 App 唤醒动作第二段是页面标识比如activity、shop、user参数全部放 query string。这要求 AASA 文件的 paths 写成/open/*其他路径不参与唤起避免线上网页被误拉起。URL Scheme 那边因为本来就是专用协议只要不跟其他 App 冲突就行。参数命名统一用小驼峰渠道归因统一用source、活动 ID 用actId涉及用户维度用uid。运营在后台拼链接时大概率会拼错所以我还做了一个链接生成小工具选好页面和参数自动生成两个版本一个给 iOS 用一个给安卓的 scheme 用杜绝手拼。4.2 参数编解码与特殊字符处理参数丢包最典型的场景是参数里嵌了一个 URL比如落地页地址是https://dl.mygame.com/open/webview?targethttps://xxx.com/page?id1如果不做编码系统在解析 query 时遇到?或就会断开参数直接截断。解决方式是任何参数在拼进链接之前统一做一次 URL Encode。C# 侧解析时我建议用Uri.UnescapeDataString来解码它和WWW.UnEscapeURL的区别是前者更接近标准 RFC 行为后者是 Unity 早期 WebPlayer 时代的遗留接口。写一个稳妥的解析函数public static Dictionarystring, string ParseQueryString(string url) { var result new Dictionarystring, string(); if (string.IsNullOrEmpty(url)) return result; int queryIndex url.IndexOf(?); if (queryIndex 0 || queryIndex url.Length - 1) return result; string query url.Substring(queryIndex 1); string[] pairs query.Split(); foreach (string pair in pairs) { if (string.IsNullOrEmpty(pair)) continue; int eqIndex pair.IndexOf(); if (eqIndex 0) continue; string key Uri.UnescapeDataString(pair.Substring(0, eqIndex)); string value Uri.UnescapeDataString(pair.Substring(eqIndex 1)); result[key] value; } return result; }这里有个细节如果用Substring(queryIndex 1)截取 queryURL 中如果还有#锚点锚点后面的内容不属于 query 的一部分。实际运营链接一般不搞锚点但如果遇到解析前先把#之后的内容砍掉。注意号在 query 标准里表示空格如果某些渠道参数里用而不是%2BUri.UnescapeDataString不会自动转空格需要先Replace(, %2B)再解码这点视渠道而定。4.3 渠道归因与安全小建议Deep Link 最常见的业务是渠道归因和自动登录。渠道归因需要把source、campaignId精确透传到服务端最好在原生层就把链接完整上报别在 C# 侧只传解析后的字典因为后续渠道要的原始参数你可能没全解析出来。自动登录这类场景我强烈建议不要在 Deep Link 链接里放长期有效的登录凭证。Deep Link 的 URL 会经过浏览器历史记录、服务器访问日志、系统剪贴板任何一个环节泄露账号就没了。正确的做法是链接里放一个短期有效的 tokenApp 拉起后拿 token 去服务端换 sessiontoken 过期时间控制在几分钟内。还要考虑参数幂等同一个链接用户可能点两次唤起两次业务侧要根据业务逻辑判断是否重复处理。比如签到活动第二次唤起时已经签过到服务端返回“已签到”别在客户端做本地去重避免活动状态不一致。5. 接入后最容易踩的 5 个坑附排查清单5.1 Universal Links 首次失效和安装后点击无反应这算是 Universal Links 最著名的坑。用户第一次安装 App 后立即点击链接很可能唤起失败因为系统还没下载并校验最新的 AASA 文件。苹果这个校验有缓存短则几秒长则几分钟完全看系统心情。排查手段也有限没有官方的在线校验工具。我的经验是分两步走第一步用 curl 确认 AASA 文件本身没问题包括Content-Type和文件内容第二步在 Xcode 里用真机跑一次断点放在continueUserActivity:看系统到底有没有把链接交到你手上。另外域名如果做了 HTTP 跳转AASA 文件必须最终落在 HTTPS 的最终响应里中间跳转过一次也可能导致失败。5.2 冷启动丢参数和二次唤起静默冷启动丢参数是最多人踩的坑90% 是因为UnitySendMessage发早了。解决方法就是第 3 节讲的缓存队列原生先缓冲Unity 就绪后再投递。注意UnityIsReady()这里有个运气成分它不是稳定可靠的标志位——如果你在 C# 场景加载到一半时收到消息GameObject 存在但脚本业务逻辑还没初始化照样可能丢。所以我 C# 侧才做了 pendingLinks 加isReady的双重保险isReady在Start()里才置真确保所有订阅方至少都执行过OnEnable。二次唤起静默的现象通常是App 在后台用户点了链接屏幕只有状态栏闪了一下C# 没收到任何回调。先去看原生openURL:有没有被调用如果没调用大概率是 SceneDelegate 生命周期接管了。再去看scene:openURLContexts:是否被调用同步处理一下。5.3 微信内打不开关、中文乱码、渠道参数冲突等实战细节微信内置浏览器不唤起 Universal Links这是 WKWebView 的系统限制不是你配置错了。运营侧的通用方案是检测到当前浏览器 UA 匹配微信时页面展示“右上角打开 Safari”的引导。别试图用 JS 去调 URL Scheme微信对 Scheme 也有拦截策略体验很差。中文参数乱码根源是编码不一致。服务端拼链接时用 UTF-8 Encode客户端解码用Uri.UnescapeDataString两端统一之后基本不会乱。如果还乱检查 AASA 对应的服务器中间层有没有改过 query string比如 CDN 对非 ASCII 字符做了转义。渠道参数冲突最经典的是两个渠道都往链接里塞uid但语义完全不一样。这个只能靠协议规范兜底我在链接生成工具里做了参数白名单校验不在白名单的自定义参数一律拒绝生成运营发现问题后找渠道方改参数名比客户端做兼容省事得多。排查时按这个清单过一遍能覆盖八成问题现象可能原因处理方式点击链接直接打开网页没有唤起 AppAASA 文件不对或未被系统拉取curl 检查 AASA确认 appID 和 pathsSafari 弹窗问“是否打开”走的是 URL Scheme不是 Universal Link确认链接是 HTTPS 域名检查 Associated DomainsApp 被拉起但 C# 收不到参数冷启动时 Unity 未就绪消息已丢原生缓存 C# 主动拉取后台二次唤起无反应SceneDelegate 回调没处理检查 scene 生命周期下的 openURL 入口中文参数乱码编码不统一统一 UTF-8 URL Encode微信里打开不唤起WKWebView 限制引导用户用 Safari 打开第一次安装后点了没反应系统 AASA 缓存未刷新等待后重试检查 AASA 部署5.4 真机验证清单三个入口缺一不可最后分享一个我每次发版前必跑的真机验证流程。模拟器上 Universal Links 的表现和真机差异很大很多问题在模拟器上根本复现不了所以真机验证这步不能省。三个入口分别是Safari 地址栏输入链接回车备忘录里点链接扫码工具扫同一个二维码。每个入口都跑一遍冷启动和热启动确认 C# 侧能收到完整 URL 并且页面跳转正确。验证时要特别关注首次安装后的场景删掉 App重新装一遍立刻点链接观察是否唤起。如果唤起失败等 30 秒再试一次区别很明显。这个流程看着啰嗦但线上用户用的就是这些入口提前发现问题比发了版被玩家反馈再修复强得多。提示Xcode 连接真机调试时如果断点停在continueUserActivity:附近系统对 Universal Links 的响应会变慢这是调试器的正常影响不是代码卡死。测试的时候先 detach 调试器跑一遍拿到真实体验数据再说。这套链路我先后在三个项目里接过最大的感受是Deep Link 接入本身不难难的是把冷启动、热启动、参数时序和渠道差异都处理干净。如果团队能抽出半天把后处理脚本和原生桥接层一次性搭好后面接再多渠道都只需要改配置文件。我搭基础设施时会顺手把 Deep Link 模块做成框架层的东西给运营留一个链接生成后台线上出问题也能顺着完整 URL 快速定位到渠道和页面。最后提醒一句每次发版前让测试拿着真机从 Safari、备忘录、扫码三个入口各跑一遍完整链路这步省不得。别问我怎么知道的。
上一篇/下一篇内容由系统自动关联 返回资讯列表 →