.NET MAUI 深度链接完全指南 (2026):App Links、Universal Links 与 Shell 路由实战

手把手带你在 .NET MAUI 10 里跑通 iOS Universal Links 与 Android App Links:域名验证、Shell 路由参数、冷启动缓存全流程实战。

.NET MAUI 深度链接完全指南 (2026)
On this page
  1. 什么是深度链接?和 URL scheme 有什么区别?
  2. iOS Universal Links 配置全流程
  3. Android App Links 配置与 assetlinks.json 验证
  4. MAUI Shell 路由与参数接收
  5. 如何处理冷启动时的深度链接?
  6. 如何测试 App Links 与 Universal Links?
  7. 延迟深度链接:Firebase 停服后怎么办?
  8. 常见问题排查清单

更新时间:2026年7月27日

在 .NET MAUI 中实现深度链接(Deep Linking)的正确做法,是同时配置 iOS Universal Links(通过 AASA 文件)与 Android App Links(通过 assetlinks.json + autoVerify intent-filter),然后在 AppDelegate / MainActivity 中把入站 URI 转发给 Shell 的 Shell.Current.GoToAsync 路由。这样用户点击一条 https://yourdomain.com/product/42 链接时,系统会跳过浏览器直接把参数交给你的 MAUI 页面,不用再走自定义 URL scheme。本文用 .NET MAUI 10 的最新 API 手把手带你跑通全流程。

什么是深度链接?和 URL scheme 有什么区别?

深度链接是指用户点击一个链接后,直接进入应用内部特定页面(例如商品详情、订单页),而不是应用首页或浏览器。在 .NET MAUI 里,我们通常关心三种深度链接:

类型示例浏览器降级App Store 政策推荐场景
自定义 URL schememyapp://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 的注册方式。

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 侧的思路和 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 的关键。

用 Release keystore 生成签名指纹:

keytool -list -v -keystore your-release.keystore     -alias your-alias -storepass ****** -keypass ******
# 输出中找到 SHA256: 5F:8E:...

把指纹写入 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

延迟深度链接: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 年营销分享场景应该只用后者。

按优先级排查: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 清缓存重试。

大概率是 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 已在 2025 年 8 月 25 日正式关闭。MAUI 项目推荐两条迁移路径:小团队用自建短链 + 首次启动查询(后端存原始参数);中大型团队直接迁到 Branch.io 的 BranchSdk.Maui NuGet 包,API 与 Firebase 接近,改造成本低。切记在迁移窗口期用埋点校对参数丢失率。