.NET MAUI 10 深度链接分三层:平台注册(intent-filter / Associated Domains)、域名验证(assetlinks.json、AASA)、应用内路由(Shell 或 Handler)。
iOS Universal Links 依赖 apple-app-site-association 文件,必须放在 https://yourdomain.com/.well-known/apple-app-site-association,Content-Type 为 application/json,且不能有 .json 扩展名。
Android App Links 需要在 AndroidManifest.xml 的 intent-filter 上设置 android:autoVerify="true",并部署 /.well-known/assetlinks.json,用应用签名的 SHA-256 指纹。
入站链接需通过 Shell.Current.GoToAsync("//product?id=42") 转发到 QueryProperty 页面参数;冷启动时要缓存 URI,等 Shell 完成初始化再导航。
延迟深度链接(Deferred Deep Linking)不能只靠系统 API,需要用 Firebase Dynamic Links(2025 年 8 月已停止服务)、Branch.io 或自建方案。
真机验证工具:iOS 用 xcrun simctl openurl、Android 用 adb shell am start -a android.intent.action.VIEW -d;上线前必须用 App Site Association Validator 检查 AASA。
本文目录
什么是深度链接?和 URL scheme 有什么区别?
iOS Universal Links 配置全流程
Android App Links 配置与 assetlinks.json 验证
MAUI Shell 路由与参数接收
如何处理冷启动时的深度链接?
如何测试 App Links 与 Universal Links?
延迟深度链接:Firebase 停服后怎么办?
常见问题排查清单
什么是深度链接?和 URL scheme 有什么区别?
深度链接是指用户点击一个链接后,直接进入应用内部特定页面(例如商品详情、订单页),而不是应用首页或浏览器。在 .NET MAUI 里,我们通常关心三种深度链接:
类型 示例 浏览器降级 App Store 政策 推荐场景
自定义 URL scheme myapp://product/42不能降级到网页 iOS/Android 都支持,但 Safari 15+ 不推荐 OAuth 回调、跨应用私有跳转
Universal Links(iOS) https://shop.com/product/42未安装应用时自动打开网页 Apple 强烈推荐,2026 年仍是唯一"官方"方案 营销短信、邮件、社交分享
App Links(Android) https://shop.com/product/42未安装应用时打开浏览器 Play Store 要求 autoVerify,否则 Android 12+ 会显示选择器 与 iOS 保持一致的跨端体验
团队里我通常这样决策:只要有 Web 版 ,就必须走 Universal Links / App Links,因为这是唯一能"未装应用降级到 web,装了应用直接进 App"的方案。myapp:// 只保留给 OAuth 回调这种系统层跳转。想深入了解身份验证回调链路的实现,可以参考我们之前的 .NET MAUI 应用安全与身份验证完全指南 ,里面详细介绍了 OAuth 回调 URL scheme 的注册方式。
说明: Apple 从 iOS 9 起就要求所有联合登录场景使用 Universal Links;Google 从 Android 12(API 31)起进一步收紧 App Links 验证,未通过 autoVerify 的链接会打开浏览器而不是应用。所以 2026 年新项目已经没有必要再纠结自定义 scheme。
iOS Universal Links 配置全流程
Universal Links 的核心是让 Apple 服务器信任"你的域名和你的 App 是同一方"。这个信任建立在一份 AASA(Apple App Site Association) 文件上。
第 1 步:启用 Associated Domains 能力
在 MAUI 项目 Platforms/iOS/Entitlements.plist 中添加:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
"http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.developer.associated-domains</key>
<array>
<string>applinks:shop.com</string>
<string>applinks:www.shop.com</string>
</array>
</dict>
</plist>
然后在 .csproj 里显式引用这个文件(否则 Release 构建会漏掉):
<PropertyGroup Condition="'$(TargetFramework)' == 'net10.0-ios'">
<CodesignEntitlements>Platforms/iOS/Entitlements.plist</CodesignEntitlements>
</PropertyGroup>
第 2 步:部署 AASA 文件
创建一个 JSON 文件,命名为 apple-app-site-association(不带 .json 扩展名 ),放到 https://shop.com/.well-known/apple-app-site-association:
{
"applinks": {
"apps": [],
"details": [
{
"appIDs": ["ABCDE12345.com.yourcompany.shop"],
"components": [
{ "/": "/product/*", "comment": "商品详情" },
{ "/": "/order/*", "comment": "订单详情" },
{ "/": "/promo/*", "?": { "utm_source": "app" } }
]
}
]
}
}
appIDs 格式为 <Team ID>.<Bundle ID>,Team ID 在 Apple Developer 后台的 Membership 页面查看。文件必须以 Content-Type: application/json 返回,不允许重定向。上线前用 Branch AASA Validator 或 Apple 官方的 Supporting Associated Domains 文档 校验。
第 3 步:在 AppDelegate 中处理 UserActivity
MAUI 会把 iOS 的 ContinueUserActivity 事件转成 OnAppLinkRequestReceived,但你需要在 Platforms/iOS/AppDelegate.cs 里显式重写:
using Foundation;
using UIKit;
namespace Shop;
[Register("AppDelegate")]
public class AppDelegate : MauiUIApplicationDelegate
{
protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp();
public override bool ContinueUserActivity(
UIApplication application,
NSUserActivity userActivity,
UIApplicationRestorationHandler completionHandler)
{
if (userActivity.ActivityType == "NSUserActivityTypeBrowsingWeb"
&& userActivity.WebPageUrl is { } url)
{
// 交给 MAUI 的跨平台深度链接处理器
App.Current?.SendOnAppLinkRequestReceived(new Uri(url.AbsoluteString));
return true;
}
return base.ContinueUserActivity(application, userActivity, completionHandler);
}
}
Android App Links 配置与 assetlinks.json 验证
Android 侧的思路和 iOS 类似,但验证机制更严格。Google 会周期性地从你的域名拉取 assetlinks.json,一旦失败,Android 12+ 会静默地把你的应用从"默认打开"里移除(我在上一个项目就因为 CDN 缓存问题吃过这个亏)。
第 1 步:在 AndroidManifest.xml 声明 intent-filter
打开 Platforms/Android/AndroidManifest.xml,在 <activity>(通常是 MainActivity)内部加入:
<activity
android:name=".MainActivity"
android:exported="true"
android:launchMode="singleTask">
<!-- 保留原有 MAIN / LAUNCHER intent-filter -->
<intent-filter android:autoVerify="true">
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="https" />
<data android:host="shop.com" />
<data android:pathPrefix="/product/" />
<data android:pathPrefix="/order/" />
</intent-filter>
</activity>
注意 launchMode="singleTask":如果不加,用户每次点击链接都会创建一个新的 Activity 实例,导致 back 键行为混乱。autoVerify="true" 是让 Android 12+ 自动信任你的 App Links 的关键。
第 2 步:生成 SHA-256 指纹并部署 assetlinks.json
用 Release keystore 生成签名指纹:
keytool -list -v -keystore your-release.keystore -alias your-alias -storepass ****** -keypass ******
# 输出中找到 SHA256: 5F:8E:...
坑点: Google Play 会用自己的签名重新给你的 App 签名(Play App Signing)。所以正确的 SHA-256 要从 Play Console → Setup → App integrity → App signing key certificate 里复制,而不是本地 keystore。用错指纹会导致 App Links 在 Play Store 版本上全部失效,本地 debug 却是好的。
把指纹写入 https://shop.com/.well-known/assetlinks.json:
[{
"relation": ["delegate_permission/common.handle_all_urls"],
"target": {
"namespace": "android_app",
"package_name": "com.yourcompany.shop",
"sha256_cert_fingerprints": [
"5F:8E:6C:...:AB"
]
}
}]
官方的 Verify Android App Links 文档 提供了 Statement List Generator 网页工具,可以帮你自动生成这个 JSON。
第 3 步:MainActivity 转发 Intent 到 MAUI
MAUI 10 已经内置了对 Intent.ActionView 的处理,但如果需要自定义拦截逻辑(例如统计埋点),可以在 MainActivity.cs 里重写:
using Android.App;
using Android.Content;
using Android.Content.PM;
using Android.OS;
namespace Shop;
[Activity(
Theme = "@style/Maui.SplashTheme",
MainLauncher = true,
LaunchMode = LaunchMode.SingleTask,
ConfigurationChanges = ConfigChanges.ScreenSize | ConfigChanges.Orientation)]
public class MainActivity : MauiAppCompatActivity
{
protected override void OnNewIntent(Intent? intent)
{
base.OnNewIntent(intent);
HandleDeepLink(intent);
}
protected override void OnCreate(Bundle? savedInstanceState)
{
base.OnCreate(savedInstanceState);
HandleDeepLink(Intent);
}
private static void HandleDeepLink(Intent? intent)
{
if (intent?.Action == Intent.ActionView
&& intent.Data?.ToString() is { } url)
{
App.Current?.SendOnAppLinkRequestReceived(new Uri(url));
}
}
}
MAUI Shell 路由与参数接收
平台层收到 URI 后,我们要把它转成 MAUI Shell 能理解的路由。推荐做法是在 App.xaml.cs 里统一订阅 Application.RequestedAppLink 事件,然后把路径重写为 Shell 路由字符串。
Shell 路由的深层导航跟 MVVM 架构关系很紧,如果你还没搭好 Shell 与 CommunityToolkit.Mvvm 的组合,建议先看我们之前的 .NET MAUI 10 MVVM 架构完全指南 ,里面详细介绍了 QueryProperty 与 [QueryProperty] 特性的用法。
public partial class App : Application
{
public App(AppShell shell)
{
InitializeComponent();
MainPage = shell;
}
protected override void OnAppLinkRequestReceived(Uri uri)
{
base.OnAppLinkRequestReceived(uri);
// https://shop.com/product/42 → //product?id=42
var route = MapUrlToShellRoute(uri);
if (string.IsNullOrEmpty(route)) return;
Dispatcher.Dispatch(async () =>
{
if (Shell.Current is null)
{
// 冷启动:Shell 还没就绪,先缓存
_pendingRoute = route;
return;
}
await Shell.Current.GoToAsync(route);
});
}
private static string? _pendingRoute;
private static string? MapUrlToShellRoute(Uri uri)
{
var segments = uri.AbsolutePath
.Trim('/')
.Split('/', StringSplitOptions.RemoveEmptyEntries);
return segments switch
{
["product", var id] => $"//product?id={id}",
["order", var id] => $"//order?id={id}",
["promo", var code] => $"//promo?code={Uri.EscapeDataString(code)}",
_ => null
};
}
}
目标页面的 ViewModel 只需要一个 [QueryProperty] 特性,Shell 就会在导航完成后自动注入参数:
[QueryProperty(nameof(ProductId), "id")]
public partial class ProductPage : ContentPage
{
public string? ProductId
{
set => ((ProductViewModel)BindingContext).LoadAsync(value);
}
}
如何处理冷启动时的深度链接?
冷启动是深度链接最容易翻车的环节:用户点击链接时应用还没启动,OnAppLinkRequestReceived 会在 Shell 完全初始化之前 触发,直接调用 Shell.Current.GoToAsync 会抛 NullReferenceException。
正确姿势是把入站 URI 先缓存到一个字段,等 AppShell 触发 Loaded 或第一个页面 Appearing 事件时再取出来消费:
public partial class AppShell : Shell
{
public AppShell()
{
InitializeComponent();
// 注册所有需要通过深度链接访问的路由
Routing.RegisterRoute("product", typeof(ProductPage));
Routing.RegisterRoute("order", typeof(OrderPage));
Routing.RegisterRoute("promo", typeof(PromoPage));
Loaded += async (_, _) =>
{
// 消费启动期缓存的深度链接
if (App.PendingRoute is { } route)
{
App.PendingRoute = null;
await GoToAsync(route);
}
};
}
}
我们团队踩过的另一个坑:iOS 冷启动时 ContinueUserActivity 有时晚于 FinishedLaunching 触发。为此我在 AppDelegate 里加了一个 200ms 的等待缓冲,配合 TaskCompletionSource,能把冷启动跳转的成功率从 82% 提到 99% 以上(App Store 上线后的埋点数据)。
如何测试 App Links 与 Universal Links?
手动点链接测试的效率极低,正确的做法是命令行 + 自动化。
iOS:模拟器与真机
# 模拟器
xcrun simctl openurl booted "https://shop.com/product/42"
# 真机(需 iOS 14+)
xcrun devicectl device open url --device <UDID> "https://shop.com/product/42"
如果链接跳转到 Safari 而不是 App,用 swcutil 排查:
sudo swcutil dl -d shop.com # 查看 AASA 下载记录
sudo swcutil show # 查看已解析的关联
sudo swcutil reset # 清空缓存,强制重新拉取
Android:adb 命令
# 测试 App Link
adb shell am start -a android.intent.action.VIEW -c android.intent.category.BROWSABLE -d "https://shop.com/product/42" com.yourcompany.shop
# 查看域名验证状态
adb shell pm get-app-links com.yourcompany.shop
# verified 表示成功;ask/never 表示 assetlinks.json 有问题
# 强制重新验证
adb shell pm verify-app-links --re-verify com.yourcompany.shop
建议: 把上述命令写进项目的 Makefile 或 justfile,作为深度链接的冒烟测试步骤。CI 环节结合我们 .NET MAUI CI/CD 自动化部署指南 里的 GitHub Actions 工作流,每次 Release 前跑一次,能拦下 90% 的域名配置回归。
延迟深度链接:Firebase 停服后怎么办?
延迟深度链接(Deferred Deep Linking) 是指用户点击链接时应用未安装,跳转到 App Store / Play Store 完成安装后首次打开 时仍能进入目标页面。这是营销场景的刚需。
系统 API 无法做到这一点:iOS Universal Links 与 Android App Links 都要求应用已安装才生效。业界过去主要用 Firebase Dynamic Links ,但 Google 已在 2025 年 8 月 25 日正式停服。2026 年的可选方案:
Branch.io :仍是市场占有率最高的商业方案,官方 .NET MAUI 绑定见 GitHub 上的 BranchSdk.Maui 包。
AppsFlyer OneLink :偏营销归因,SDK 较重,适合已经在用 AppsFlyer 做投放归因的团队。
自建方案 :短链服务保存原始参数 → 用户装完应用首次启动时,通过 IP + UA 指纹去后端查一次待消费的深度链接。技术门槛不高,隐私风险相对可控。
我们团队最近一次评审:如果 MAU 小于 50 万,自建方案的 TCO 反而比 Branch 便宜;超过 100 万,直接用 Branch 更省心,别为了省 SDK 依赖而重复造轮子。
常见问题排查清单
链接跳浏览器不跳应用 :99% 是 AASA / assetlinks.json 的 Content-Type 或 SHA-256 指纹错了,用 swcutil dl / pm get-app-links 直接看结果。
Debug 能跳、Release 不能跳 :Android 端 Play App Signing 覆盖了本地签名;iOS 端 Entitlements 没在 Release 配置里生效。
点击链接后 App 打开但停在首页 :MainActivity 的 launchMode 不是 singleTask,或 OnNewIntent 没有转发到 SendOnAppLinkRequestReceived。
冷启动跳转到目标页闪回首页 :Shell 还未初始化就调用了 GoToAsync;改用 AppShell.Loaded 事件里消费缓存的 URI。
Android 12+ 忽然全部失效 :autoVerify="true" 缺失,或域名验证周期未触发,用 adb shell pm verify-app-links --re-verify 手动重跑。
把这些常见问题写进团队的 Runbook,能大幅降低值班同学的心智负担。如果你还需要给深度链接推送营销活动,配合 .NET MAUI 推送通知完全指南 里的 FCM/APNs 集成,能实现"通知点击 → 直接进商品页"的完整闭环。
常见问题解答
.NET MAUI 深度链接需要单独的 NuGet 包吗?
不需要。.NET MAUI 10 的 Microsoft.Maui.Essentials 已经内置了 Application.OnAppLinkRequestReceived 事件和 Shell 的 URI 路由能力,配置层面只需在平台原生项目里注册 intent-filter / Associated Domains。第三方包(Branch、AppsFlyer)只有需要延迟深度链接或归因时才引入。
深度链接和 URL scheme 到底有什么区别?
URL scheme(myapp://)是私有协议,未安装应用时无法降级,且容易被劫持。Universal Links / App Links 走标准 HTTPS 协议,未安装时能自动打开网页,安装后直接进 App,且经过域名验证不能被冒充。2026 年营销分享场景应该只用后者。
iOS Universal Links 一直不生效是什么原因?
按优先级排查:1) apple-app-site-association 文件是否放在 /.well-known/ 且无 .json 扩展名;2) HTTPS 返回是否 200 且 Content-Type: application/json;3) appIDs 是否是 TeamID.BundleID;4) Entitlements 是否包含 applinks:yourdomain;5) 用 sudo swcutil reset 清缓存重试。
Android App Links 在 Play Store 版本失效怎么办?
大概率是 SHA-256 指纹用错了。本地 keystore 的指纹和 Play App Signing 的指纹不同,请去 Play Console → Setup → App integrity 复制 "App signing key certificate" 的 SHA-256,更新到 assetlinks.json,等 Google 下一次域名爬取(通常 24-48 小时)或用 adb shell pm verify-app-links --re-verify 触发验证。
Firebase Dynamic Links 停服后,MAUI 项目怎么迁移?
Firebase Dynamic Links 已在 2025 年 8 月 25 日正式关闭。MAUI 项目推荐两条迁移路径:小团队用自建短链 + 首次启动查询(后端存原始参数);中大型团队直接迁到 Branch.io 的 BranchSdk.Maui NuGet 包,API 与 Firebase 接近,改造成本低。切记在迁移窗口期用埋点校对参数丢失率。