跳到正文
LinkProfit

iOS 与 Android 上的 deep link:一份实用指南

LinkProfit Team阅读约 9 分钟
  • deep-links
  • developers
  • marketing
本页内容

deep link 是这样一个 URL:它打开的是已安装 App 内部的某个具体页面,而不是一个网页。整件事的想法就这么简单,而在过去十五年里,它先后有过三种不同的实现方式,每一种都有各自独特的失效模式。营销团队之所以不断为它提交 bug,是因为这三种方式至今同时存在,同一个 URL 的行为会取决于它是在一条消息里、在浏览器里、在邮件客户端里,还是在社交 App 内嵌的 WebView 里被点开的。

本文如实讲清楚这些机制,也包括它们做不到什么。如果只带走一条结论,那就带走这一条:把一次点击路由到正确的 App 页面,是已经被解决好的基础设施;而把一位用户在安装 App 之后送到正确的页面,则不是——至少单靠链接平台不是。

App 链接的三代实现

自定义 URL scheme

最早的那套机制。一个 App 注册一个像 myapp 这样的 scheme,于是 myapp://product/42 这样的 URL 就会把请求交给它。scheme 实现起来毫不费力,作为 App 内部的路由格式今天依然好用,但它带着两个结构性问题。

一是没有归属校验:任何 App 都能注册任何 scheme,当两个 App 声称拥有同一个 scheme 时,iOS 上的解析结果是未定义的,Android 上则弹出一个选择对话框。二是没有兜底:在没装这个 App 的设备上,一个 scheme URL 得到的是一个错误页面,或者干脆什么都没有,因此每一条 scheme 链接都需要一层外壳,用来检测失败并把用户送去某个有用的地方。而正是这层外壳,在浏览器收紧对导航计时器的处理之后先垮掉了。

Apple 的替代方案用的是普通的 HTTPS URL。你的 App 声明一项形如 applinks:yourbrand.com 的关联域名 entitlement,而这个域名在 /.well-known/apple-app-site-association 上发布一份 JSON 文件,说明哪些路径属于这个 App。

{
  "applinks": {
    "details": [
      {
        "appIDs": ["ABCDE12345.com.yourbrand.app"],
        "components": [{ "/": "/p/*", "comment": "Product pages" }]
      }
    ]
  }
}

这份文件必须以 JSON 形式、在那个确切的路径上、经由 HTTPS 提供,并且不能有任何跳转。iOS 会在安装和更新前后去取它,而且大体上经过 Apple 的 CDN,这意味着改动不会立刻生效。它的好处是同一个 URL 到哪儿都能用:App 已安装且路径匹配,就打开 App;否则 Safari 加载网页。不存在错误状态。

Android 这边的对应物是一份放在 /.well-known/assetlinks.json 的 Digital Asset Links 文件,里面写明 App 的包名和签名证书的 SHA-256 指纹。

[
  {
    "relation": ["delegate_permission/common.handle_all_urls"],
    "target": {
      "namespace": "android_app",
      "package_name": "com.yourbrand.app",
      "sha256_cert_fingerprints": ["A1:B2:C3:D4:E5:F6:..."]
    }
  }
]

App 一侧为这个域名声明一个开启了自动校验的 intent filter(意图过滤器)。校验通过时,链接直接打开 App,不弹选择对话框。校验失败时,链接打开浏览器,而最常见的原因遥遥领先——指纹对不上:团队发布的是自己本地发布密钥的指纹,而 Play App Signing 用另一把密钥重新给 App 签了名。该写进文件的那个指纹,是商店为实际分发出去的那份构建物所显示的指纹。

短链接是怎么挑选目标地址的

一条短链接是一个决策点,而不是一个固定的指针,这正是它对 App 投放有用的原因:同一个印出来的码,可以分别服务 iPhone 用户、Android 用户和桌面端访客,而且这个决策在码印好之后还能再改。

跳转引擎能看到请求的 user agent、它携带的 client hints,以及由网络推导出的国家等信号。一条为 App 流量配置好的链接通常握着三个目标地址:一个 iOS URL、一个 Android URL,以及一个用于其余所有情况的网页 URL。在这之上再叠一层,定向规则可以按国家、设备、操作系统或语言来路由,于是一场营销活动可以把加拿大的 Android 流量送往一个目标地址、把其他所有人送往另一个,而不必创建多条链接。

有两个实现细节值得知道,因为绝大多数令人困惑的行为都由它们解释。

第一个是社交爬虫。当 Facebook 或 Telegram 的抓取器这类机器人为了生成预览卡片而请求一条链接时,一个正确的跳转引擎会返回预览标记,而不是把这次请求算成一次点击,也不会把它当成一部手机来路由。如果你的点击计数在链接刚发出去、还没有任何人点它的那一刻就跳涨,说明这个引擎并没有这么做。

第二个是关联关系的边界,这也是本文中最重要的那一个技术要点。Universal Links 是拿用户真正点下去的那个 URL 的域名来判定的。如果有人点了 go.yourbrand.com/p/42,而这个域名以一次跳转把他送往 yourbrand.com/p/42,iOS 并不会把跳转的目标当作一条被关联的链接,于是 App 不会打开。要让一条短链接原生地打开 App,关联文件必须由短域名本身提供。在做不到这一点的地方,务实的兜底办法是交接给一个自定义 scheme,或者直接交接给应用商店——多数短链接服务的 deep link 功能实际做的正是后者。无论面对哪一家厂商,都问清楚它实现的是两者中的哪一种,因为两种做法的宣传文案写出来是一模一样的。

兜底链路

一条能用的 App 链接,实质上是一棵小小的决策树,而每一个分支都需要一个有意为之的答案。

| 场景 | 应当发生什么 | 常见错误 | | --- | --- | --- | | App 已安装,路径可识别 | App 打开到目标页面 | 跳转链路破坏了关联关系 | | App 已安装,路径不可识别 | 网页加载,并提示可以用 App 打开 | 每次点击都弹出选择对话框 | | App 未安装,移动端 | 对应平台的应用商店页面 | 商店链接被写死成某一个平台 | | 桌面端 | 与该页面完整对应的网页版 | 落到一个笼统的首页 | | 社交爬虫 | 返回预览标记,不计点击 | 预览抓取抬高了数据分析 |

把商店那个分支指向与平台相符的商店页面,而不是一条通用链接,并且让网页分支真正对得上 App 里的那个页面。无论其余部分配置得多好,App 投放的流量里都有相当大一部分最终落在网页分支上,把它当成死路,浪费掉的是这笔预算的大半。

安装分界线,实话实说

厂商的宣传和现实在这里分道扬镳。设想一位新用户点了一条指向某个具体商品的链接,他没有装这个 App,于是落到商店,装好,打开。此时 App 显示的是它默认的首页,因为没有任何东西把商品标识符带过安装这道坎。让这件事跑通,叫做延迟 deep link(deferred deep linking),而它不是一个链接平台单凭自己就能做到的。

它需要 App 内部有一个 SDK,在首次启动时去问服务端:这位用户在安装之前点的是什么。服务端必须把这两个事件匹配起来,而可用于这次匹配的信号已经急剧收窄。设备指纹并不可靠,而且正被平台政策越收越紧。广告标识符需要用户授权,而多数用户会拒绝。Apple 的归因框架向广告主报告的是安装层面的归因,并不会把一条路径传给你的 App。曾经用来携带令牌的剪贴板手法,如今会触发一条可见的粘贴提示。

剩下还能用的那部分确实有效,但那是另一个产品,接入成本也不同:一套移动衡量合作伙伴 SDK、App 内的初始化,以及一个概率性而非精确的匹配时间窗。LinkProfit 按设备路由点击并交接给应用商店,它不声称提供安装后的路由,因为把这件事做对,意味着要在你的 App 里交付代码。如果延迟路由是硬性需求,就把那套 SDK 规划在链接平台的旁边,而不是拿它取代链接平台。

WebView 以及其他实际存在的陷阱

App 内置浏览器

在社交 App 里点开的链接,多数运行在内嵌的 WebView 里,而不是系统浏览器,而各家 WebView 对 App 关联关系的处理并不一致:有的无视 Universal Links,有的屏蔽自定义 scheme,行为还会因 App 版本而异。一条在测试里完美工作的链接,到了线上却打开网站,最常见的原因就是这个——因为测试通常是在即时通讯 App 里点一条链接完成的,而那里的行为是正确的。

没有任何一项配置能一劳永逸地解决它。奏效的做法是:把网页目标做好,在上面加一个显眼的「在 App 中打开」控件,并且逐个测试每一个发布渠道,而不是假定它们表现一致。

关联文件上的错误

反复出现的有四种。一是经由跳转提供关联文件,包括从裸域自动跳到 www 的那种跳转,它会让文件失效。二是用错误的 content type 提供,或者放在一个会被框架重写的路径上。三是发布了错误的 Android 签名指纹,如上文所述。四是忘了 iOS 会缓存这份文件,因此一次订正要送达那些已经装了 App 的设备,可能需要一天甚至更久。

App 分支上的数据分析

一次点击一旦进入 App,你的网站分析就看不见它了,而报表正是在这里悄悄散架的。把营销活动参数保留在链接上,让跳转引擎记录下来;同时把一个标识符透传给 App 目标地址,这样 App 内的会话才能和这次点击对上。我们的链接点击跟踪指南讲了一次点击事件能告诉你什么、又不能告诉你什么,其中关于机器人和独立访客的那些注意事项,在这里同样适用。

发布之前先测试

curl -sSI https://yourbrand.com/.well-known/apple-app-site-association
curl -sS https://yourbrand.com/.well-known/assetlinks.json

# Android: open a URL as if it were tapped, then inspect verification state
adb shell am start -a android.intent.action.VIEW -d "https://yourbrand.com/p/42"
adb shell pm get-app-links com.yourbrand.app

# iOS simulator
xcrun simctl openurl booted "https://yourbrand.com/p/42"

前两条命令应当返回 200,且没有跳转。请注意,在 Safari 地址栏里手敲一条 Universal Link 是被刻意设计成不打开 App 的,所以要改用备忘录、一条消息,或者上面那条模拟器命令来测试,否则你会去追一个根本不存在的 bug。

各家链接平台实际包含了什么

deep link 支持在这个品类里的打包方式差别很大,而差别在于你必须买哪一档套餐,而不在于能力本身。

| 厂商 | deep link 的提供情况,截至 2026 年 8 月 | | --- | --- | | BL.INK | 所有套餐,入门档位 48 美元/月,含一个用户和一个域名 | | Short.io | 从 48 美元/月的 Team 套餐起 | | Rebrandly | 从 99 到 119 美元/月的 Growth 套餐起 | | Switchy | 宣称覆盖 130 多个 App | | LinkProfit | 所有套餐都提供按设备区分的目标地址 |

在这件事上 BL.INK 是个实在的参照:在每一个档位上都包含 deep link 并不常见,尽管就只含一个域名而言,它的入门价格偏高。Rebrandly 把它放在 Growth 上的做法,则是普遍值得留意的那种模式——因为你为某一项功能而不得不买的那个档位,往往决定了整张账单。

一套行得通的上线顺序

  1. 为每一个你想链接到的 App 页面确定一个规范的网页 URL;网页既是兜底,也是事实来源。
  2. 在将要出现在链接里的那个域名上发布两份关联文件,并通过 HTTPS 验证它们、确认没有跳转。
  3. 在 iOS 上加上关联域名 entitlement,在 Android 上加上开启校验的 intent filter,用的是实际分发那份构建物的指纹。
  4. 确认你的短链接是在哪个域名上被点开的,以及关联关系是不是就住在那里,还是平台会交接给一个 scheme 或应用商店。
  5. 给每一条链接配置按设备区分的目标地址,外加一个网页兜底,并为每个平台配上正确的商店页面。
  6. 在两个平台上分别从即时通讯 App、从系统浏览器,以及从你发布内容的每一个社交 App 里测试一遍。
  7. 验证营销活动参数在跳转中存活下来,并被记录进数据分析
  8. 如果链接是按营销活动、按收件人或按商品逐条生成的,就通过 API 把创建过程自动化。

单看每一件事,都不难。难在这些零件分散在三个地方——App 工程、DNS 区域、链接平台,而它们通常归三个不同的人负责。把「关联文件归谁负责」写下来,比本文里任何一条单独的配置建议都更有价值。

大家常问的问题

deep link 和 Universal Link 有什么区别?

deep link 是一个总的概念:一个 URL 打开的是 App 内部的某个具体页面,而不是一个网站。iOS 上的 Universal Links 和 Android 上的 App Links 是这个概念的现代实现,用的是普通的 HTTPS URL,由托管在你自己域名上的一份文件完成校验。像 myapp://product/42 这样的自定义 URL scheme 属于更早的实现:它们今天仍然可用,但任何一个 App 都能声称拥有某个 scheme,而在没装这个 App 的设备上,得到的是一个错误页面,而不是一个兜底目标。

短链接会不会破坏 Universal Links?

会,而且这是这个品类里最常见的意外。iOS 是拿用户真正点下去的那个 URL 的域名来判定关联关系的,因此一个跳转到你 App 域名的短域名,可能会把请求交给浏览器,而不是交给 App。有两种绕开的办法:把关联文件直接托管在短域名上,让短链接本身成为被关联的那个 URL;或者接受一次基于跳转的交接,转去自定义 scheme 或应用商店页面。在你把短链接定为 App 投放的标准做法之前,先问清楚厂商实现的是哪一种。

短链接能不能在新用户安装 App 之后,把他送到那个确切的页面?

只有在 App 内部配合的前提下才行。能在一次安装中存活下来的路由,通常叫做延迟 deep link(deferred deep linking),它需要 App 里有一个 SDK,在首次启动时去问服务端:这位用户在安装之前点的是什么。而服务端可用于匹配的信号,在 iOS 上已经收窄了很多。仅靠平台侧的链接路由做不到这件事:跳转止步于应用商店,而商店不会把你的路径带过去。如果安装后的路由是硬性需求,就要在链接平台之外,另行为一套移动衡量合作伙伴 SDK 留出预算。

为什么我的链接在 Instagram 或 TikTok 里打开的是网站,而不是 App?

在社交 App 里点开的链接,通常运行在内嵌的 WebView 里,而不是系统浏览器,而各家 WebView 对关联关系的处理并不一致。有的完全无视 Universal Links,有的屏蔽自定义 scheme,具体行为还会随 App 版本和平台而变。务实的答案是不要跟它硬碰:把网页目标做得真正可用,在那个页面上放一个显眼的「在 App 中打开」控件,并且逐个测试你实际发布的每一个渠道,而不是假定它们表现一致。

哪些链接平台在入门套餐上就包含 deep link?

BL.INK 是个例外:deep link 在每一个套餐上都有,尽管截至 2026 年 8 月,它的入门档位是 48 美元/月,只含一个用户和一个域名。Rebrandly 把 deep link 放在 99 到 119 美元/月的 Growth 档位之后,Short.io 从 48 美元的 Team 套餐起提供,Switchy 则宣称覆盖 130 多个 App。LinkProfit 在所有套餐上都包含按设备区分的目标地址。

怎样在不发布营销活动的前提下测试 deep link?

直接用平台自带的工具。在 iOS 上,xcrun simctl openurl booted 可以在模拟器里打开一个 URL;而在真机上,从备忘录或一条消息里点开,才测得到真实的关联路径——因为在 Safari 地址栏里手敲 URL 是被刻意设计成不触发 Universal Links 的。在 Android 上,adb shell am start 配合 VIEW action 可以打开一个 URL,而 pm verify-app-links 系列命令会报告校验状态。另外,用 curl 把两份关联文件都取一次,确认它们返回 200 且没有跳转。