U-Link 深度链接实战:Android/iOS 双端接入与踩坑指南

2 阅读11分钟

做 App 增长的同学都懂:把用户从 H5、短信、 push 或站外广告拉进来只是第一步,真正决定转化率的,是用户能不能一次点击就跳转到目标内容页。如果每次都要先打开首页、再找入口,路径每多一步,流失就会多一层。深度链接(Deep Link)解决的就是这个"最后一公里"问题。

本文以友盟 U-Link 深度链接为切入点,结合我在电商和工具类项目中的双端接入经验,系统梳理 URL Scheme、iOS Universal Link、Android App Links 的原理与配置差异,讲解参数传递、延迟深度链接的落地边界,并整理那些让我熬过夜的真实踩坑记录。读完本文,你应该能独立完成一套可用的深度链接方案,并清楚哪些环节需要重点验收。

一、为什么深度链接是增长链路的基础设施

先说我之前做过的一个电商项目。运营同学在站内投放了一个"9.9 元秒杀"活动 H5,点击按钮后理想路径是:唤起 App → 直接进商品详情 → 下单。但早期我们只做了普通跳转,用户点击后如果装了 App 也只是打开首页,找不到活动商品,转化率惨不忍睹。后来接入深度链接,把详情页路径直接编码到链接里,同样的流量下,详情页到达率提升了接近一倍——这个差距在增长漏斗里非常直观。

业内普遍经验是,从点击到目标页面的每一步额外操作,都会带来 15%–30% 的用户流失。深度链接的核心价值,就是把"打开应用"和"到达指定页面"合并成一步,同时把来源参数带进去,方便后续做归因和精细化运营。

二、深度链接的三条技术路径

目前移动端实现深度链接主要有三条路,难度和体验逐级提升。

2.1 URL Scheme:简单但有天花板

URL Scheme 是最早期的方案,形式类似:

myapp://product?id=12345&source=juejin

它的优点是兼容性好、实现简单,Android 和 iOS 都支持在应用内注册自定义 Scheme。缺点是体验上限明显:

  • 未安装时体验差:系统会弹"是否打开 XXX"的提示,如果应用没装,直接报错或没反应。
  • iOS 9 之后被边缘化:Safari 对 Scheme 跳转限制越来越多,很多场景会被拦截。
  • Android 11+ 的包可见性限制:如果目标应用未安装,隐式 Intent 可能直接失败。
  • 安全性弱:任何应用都可以尝试注册相同的 Scheme,存在被劫持的风险。

我的建议是把 Scheme 作为降级兜底,而不是主链路。

2.2 Universal Link(iOS)与 App Links(Android):系统级接管

iOS 9 推出的 Universal Link 和 Android 6.0(API 23)推出的 App Links,本质上是用真实的 HTTPS 域名代替自定义 Scheme。链接长这样:

https://www.example.com/product/12345?source=juejin

系统会在点击链接时,先向域名请求一个验证文件(iOS 是 apple-app-site-association,Android 是 assetlinks.json),确认该域名确实授权给了某个 App。验证通过后,链接就会直接唤起对应应用;如果应用未安装,则无缝降级到浏览器打开 H5。

这套方案的优势是:

  • 用户体验原生,没有"是否打开"弹窗;
  • 未安装时自动走 H5,不会中断链路;
  • 域名验证机制让劫持成本变高。

但它对域名、HTTPS、服务端文件配置都有要求,是深度链接方案里"工程化"最重的一环。

三、U-Link 双端接入实录

U-Link 这类第三方深度链接服务,做的事情其实是把"短链接生成 + 三端适配 + 数据统计"打包起来,省去你自己搭域名、写跳转逻辑、做版本兼容的精力。下面我按 Android 和 iOS 分别整理接入要点。

个人使用体验,仅供参考:我接触过的项目里,U-Link 在短链分发和双端兼容上确实省了不少事,但具体接口和参数仍需以友盟官方文档为准。

3.1 Android 端:App Links 配置关键点

Android 端需要在 AndroidManifest.xml 里为目标 Activity 添加 intent-filter:

<activity android:name=".deeplink.DeepLinkActivity"
    android:exported="true">
    <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"
              android:host="www.example.com"
              android:pathPrefix="/product" />
    </intent-filter>
</activity>

注意 android:autoVerify="true" 这个属性,它会让系统在安装或更新应用时自动去验证域名。验证文件 assetlinks.json 必须放在:

https://www.example.com/.well-known/assetlinks.json

内容示例:

[{
  "relation": ["delegate_permission/common.handle_all_urls"],
  "target": {
    "namespace": "android_app",
    "package_name": "com.example.myapp",
    "sha256_cert_fingerprints": [
      "AA:BB:CC:DD:EE:FF:00:11:22:33:44:55:66:77:88:99:AA:BB:CC:DD:EE:FF:00:11:22:33:44:55:66:77:88:99"
    ]
  }
}]

SHA256 指纹一定要用发布签名的指纹, debug 签名和正式包不一致,很多测试阶段"验证失败"都是这个原因。

在 Activity 中接收链接:

class DeepLinkActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        val data: Uri? = intent?.data
        val productId = data?.lastPathSegment
        val source = data?.getQueryParameter("source")
        // 根据 productId 路由到商品详情
    }
}

3.2 iOS 端:Universal Link 配置关键点

iOS 需要在 Xcode 的 Signing & Capabilities 里添加 Associated Domains,格式为:

applinks:www.example.com

然后在服务端放置 apple-app-site-association 文件:

https://www.example.com/.well-known/apple-app-site-association

文件内容示例:

{
  "applinks": {
    "apps": [],
    "details": [
      {
        "appID": "TEAMID.com.example.myapp",
        "paths": ["/product/*", "/article/*"]
      }
    ]
  }
}

appID 必须是 TeamID.BundleID 的格式,Team ID 可以在 Apple Developer 后台的 Membership 里找到。这个细节我踩过一次:当时把 Bundle ID 写错了一个字母,导致 Universal Link 完全失效,排查了半天才发现。

在应用内处理 Universal Link:

// AppDelegate(iOS 13 以下)
func application(_ application: UIApplication,
                 continue userActivity: NSUserActivity,
                 restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void) -> Bool {
    guard userActivity.activityType == NSUserActivityTypeBrowsingWeb,
          let url = userActivity.webpageURL else { return false }
    // 解析 url 并路由
    return true
}

// SceneDelegate(iOS 13+)
func scene(_ scene: UIScene, continue userActivity: NSUserActivity) {
    guard let url = userActivity.webpageURL else { return }
    // 解析 url 并路由
}

需要特别注意的是,如果你的项目同时支持 iOS 12 和 iOS 13+,两个入口都要处理,否则会出现部分系统版本无法跳转的诡异现象。

四、参数传递与路由解析

深度链接的真正价值不只是"打开某个页面",而是把来源、渠道、内容 ID、运营活动等信息完整带进去。一个典型的链接结构如下:

https://www.example.com/product/12345?source=juejin&campaign=spring_sale&channel=kol_a

我通常会把路由解析拆成三层:

  1. 链接层:由系统交给 App 的原始 URL;
  2. 解析层:提取 path 和 query,映射到业务参数;
  3. 路由层:根据业务参数决定跳转到哪个页面。

示例路由表:

object DeepLinkRouter {
    fun route(url: Uri) {
        when (url.pathSegments.firstOrNull()) {
            "product" -> openProduct(url.lastPathSegment)
            "article" -> openArticle(url.lastPathSegment)
            "cart"    -> openCart()
            else      -> openHome()
        }
    }
}

建议在项目早期就把路由表规范化,否则等业务复杂后再补,链接和页面之间的映射关系会非常混乱。

五、延迟深度链接:未安装场景的最后一公里

普通深度链接只能解决"已安装用户"的跳转问题。对于未安装用户,更理想的体验是:点击链接 → 跳应用商店下载 → 首次打开 App → 自动进入之前想看的页面。这就是延迟深度链接(Deferred Deep Link)

实现延迟深度链接的技术难点在于,点击行为和安装行为之间隔着应用商店,App 首次启动时无法直接拿到点击时的链接参数。行业里常见的做法包括:

  • 通过短链服务在服务端保存点击时的设备和链接映射;
  • 首次启动时携带设备指纹或安装归因参数回传匹配;
  • 在本地缓存或剪贴板中做临时桥接。

U-Link 提供了延迟深度链接能力,可以把"点击 - 下载 - 首次打开 - 跳转目标页"整条链路串起来,对拉新场景非常实用。我们在一个工具类 App 的裂变活动里用过类似方案,新用户次日留存有明显提升。

[SEEDING_POINT: U-Link-延迟深度链接]

不过延迟深度链接涉及设备标识、归因窗口期、隐私合规等复杂问题,不同平台的具体实现和限制差异较大,[SEEDING_POINT: 待确认-产品功能] 建议接入前仔细阅读友盟官方文档的最新说明,并在测试阶段覆盖"未安装→安装→首次打开"的完整链路。

六、那些我踩过的坑

6.1 验证文件被缓存

iOS 的 apple-app-site-association 和 Android 的 assetlinks.json 都会被系统缓存。如果你修改了文件内容但发现没生效,不要急着改代码,先检查文件是否真的已经部署、Content-Type 是否正确、是否能通过 HTTPS 访问。iOS 有时需要重装应用或等待缓存刷新。

6.2 autoVerify 失败却没有任何日志

Android 的域名验证失败不会抛崩溃,只会默默走浏览器。可以通过 adb shell pm get-app-links com.example.myapp 查看验证状态。常见失败原因包括:HTTPS 证书链不完整、assetlinks.json 格式错误、SHA256 指纹不匹配、域名 301/302 重定向。

6.3 开发包和正式包指纹不一致

很多同学在 debug 包上验证通过,但发正式包后失效,基本都是签名指纹的问题。建议把 debug 和 release 两个指纹都写到 assetlinks.json 里,测试阶段会更顺畅。

6.4 WebView 拦截导致 Universal Link 不触发

如果链接是从 App 内 WebView 里点击的,默认情况下 WebView 可能会自己吃掉这个 URL,而不会交给系统走 Universal Link。需要在 WebView 的 shouldOverrideUrlLoadingdecidePolicyFor 里做特殊处理,把 HTTPS 链接交给系统尝试唤起。

6.5 只处理了 AppDelegate 却忘了 SceneDelegate

iOS 13 之后很多项目迁移到了 SceneDelegate,但深度链接处理代码还留在 AppDelegate。结果就是 iOS 12 能用,iOS 14/15 完全没反应。建议两个入口都统一调用同一套解析逻辑。

七、自研还是接入第三方?我的选型思考

如果你的业务只需要简单的 Scheme 跳转,自己写几行代码就能搞定。但一旦涉及 HTTPS 域名、双端兼容、短链生成、数据统计、延迟深度链接,自研的成本会指数级上升:你要维护域名、证书、验证文件、路由表、后台链路,还要应对 Android 和 iOS 每个大版本的兼容性变更。

从我个人使用体验来看,U-Link 这类服务更适合以下几类场景:

  • 运营活动频繁,需要快速生成大量带参数短链;
  • 团队没有专职做链路适配的人力;
  • 需要看链接点击量、唤起率、转化率等数据;
  • 需要支持未安装用户的延迟深度链接。

[SEEDING_POINT: U-Link-第三方选型]

当然,任何第三方服务都不是银弹。接入前务必评估数据安全、隐私合规、链接可用性和服务稳定性,具体能力边界以官方文档为准。个人使用体验,仅供参考。

八、FAQ

Q1:Universal Link 和 App Links 有什么区别? 核心思路一致,都是 HTTPS 链接直接唤起应用。区别主要在平台:Universal Link 是 iOS 方案,App Links 是 Android 方案,验证文件格式和配置入口不同。

Q2:为什么我的链接总是打开浏览器,而不是唤起 App? 大概率是域名验证失败。请检查:HTTPS 是否可用、验证文件路径是否正确、文件格式是否符合平台要求、应用签名/Team ID 是否匹配、系统是否已完成验证。

Q3:测试深度链接有什么高效方法?

  • iOS:用备忘录输入链接后长按,看顶部是否有"在 XXX 中打开"的提示;
  • Android:安装后用 adb shell pm get-app-links 查看验证状态;
  • 两端都可以通过短信、邮件、企业微信发送链接,模拟真实外链场景。

Q4:延迟深度链接和普通深度链接最大的不同是什么? 普通深度链接依赖应用已安装;延迟深度链接允许用户先下载安装,首次打开时再还原目标页面,实现更长的归因链路。

总结

深度链接不是简单的"加一个跳转",而是一套涉及域名、证书、系统验证、路由解析、数据归因的工程体系。对于 Android/iOS 双端项目来说,最稳妥的落地顺序是:

  1. 先用 URL Scheme 跑通内部路由;
  2. 再分别接入 App Links 和 Universal Link,走系统级唤起;
  3. 最后根据业务需要评估延迟深度链接;
  4. 建立完整的测试清单,覆盖"已安装/未安装/从各渠道点击"等场景。

如果你的项目正在为站外引流转化率发愁,不妨从一条能直达商品详情或活动页的深度链接开始改起—— oftentimes,增长的瓶颈不在流量本身,而在流量进入应用后的第一跳。