Android App Links 深度解析:从 Digital Asset Links 域名验证到 Intent 路由分发的 Web-to-App 全链路工程实践
一个诡异的跳转失败
去年给一个电商 App 接入 App Links,测试环境一切正常,发到线上后用户反馈”点链接还是打开网页”。排查发现 Digital Asset Links 文件在 https://example.com/.well-known/assetlinks.json 能正常访问,SHA256 指纹也匹配。
反复对比后找到问题:测试包和正式包用了不同签名。App Links 的验证 绑定的是发布签名的证书指纹,而我在 assetlinks.json 里填的是 debug 签名。更坑的是——这个问题没有报错提示,系统静默降级成了普通 Deep Link。
这就是 App Links 区别于普通 Deep Link 的地方:它要求域名持有者与 App 开发者双向确认身份。如果只把 App Links 当成”自动生效的 IntentFilter”,一上线就会踩坑。
Digital Asset Links:双向信任的锚点
App Links 的信任模型基于一个前提:能让 https://yourdomain.com/.well-known/assetlinks.json 返回特定内容的人,一定是域名的合法控制者。而 App 签名证书的 SHA256 指纹,保证了 App 来源的真实性。两者匹配,就构成了信任闭环。
assetlinks.json 的标准结构:
[
{
"relation": ["delegate_permission/common.handle_all_urls"],
"target": {
"namespace": "android_app",
"package_name": "com.example.app",
"sha256_cert_fingerprints": [
"14:6D:E9:83:C5:73:06:50:D8:EE:B9:95:2F:34:FC:64:16:A0:83:42:E6:1D:BE:A8:8A:04:96:C6:20:FE:BA:CD"
]
}
}
]
三个关键字段:
- relation:固定值
delegate_permission/common.handle_all_urls,含义是”域名的 URL 处理权委托给 App”。这是唯一的合法值。 - package_name:App 的包名,用于区分一个域名关联多个 App 的场景。
- sha256_cert_fingerprints:最重要的一环。用 JDK 自带的 keytool 就能获取:
keytool -list -v -keystore release.keystore | grep SHA256
这个指纹绑定的是证书而非签名文件本身。用同一个证书签名的不同 App(比如主 App 和 Lite 版),只要包名匹配对应的条目,都能通过验证。
一个常见的误解是以为 assetlinks.json 可以放多个证书指纹做”签名兼容”。实测发现,如果你填了 debug 和 release 两个指纹,系统取 第一条匹配的记录,不会做”任意一个通过即可”的合并。不同 Android 版本的行为还不太一致——Android 12 之前宽松一些,13 后收紧了。
系统自动审批的幕后流程
App Links 的核心价值是免用户确认——点击链接直接跳转 App,不弹选择器。这个体验由一套 系统级 Verifier 机制 支撑。
验证时机与条件
系统不会在每次点击链接时都去请求 assetlinks.json。验证发生在以下时机:
- App 安装完成后(
ACTION_PACKAGE_ADDED触发) - App 更新时(仅 Android 12+)
- 设备重启后(重新校验已缓存的验证结果)
验证有网络条件要求:设备必须处于非计费网络,Android 13+ 还要求 DNS 可解析。我踩过的坑是测试机在内网环境,DNS 无法解析公网域名,验证始终不通过,但系统不报任何错误。
验证请求的真实形态
系统发出的验证请求是标准 HTTPS GET,不带任何 User-Agent 特征,看起来就像普通流量。用 Nginx 反代 assetlinks.json 时,不能只对特定 User-Agent 放行,原因就在这。
几个关键约束:
- 请求 不跟随重定向(HTTP 301/302 直接判失败)
- 超时约 5 秒
- 响应必须
Content-Type: application/json - 文件大小上限 64KB
# 服务器端正确配置
location /.well-known/assetlinks.json {
default_type application/json;
add_header Cache-Control "public, max-age=3600";
try_files /static/assetlinks.json =404;
}
try_files 是关键——必须直接返回文件,不能走应用层路由。我曾经因为用了 rewrite 规则导致请求经过 PHP 处理,响应头被框架覆盖成 text/html,验证直接失败。
验证状态的生命周期
系统维护一个验证状态表,存储在 Settings.Global 中,可以通过 adb 查看:
adb shell settings get global device_policy_manager_pending_intents
# 查看 App Links 验证状态(Android 11+)
adb shell pm get-app-links com.example.app
输出类似:
com.example.app:
ID: 3f4b5c6d...
Signatures: [14:6D:E9:...]
Domain verification state:
example.com: verified
www.example.com: legacy_failure
状态分四种:
- verified:完全信任,免确认跳转
- unverified:待验证,行为等同于普通 Deep Link
- legacy_failure:曾经验证失败,Android 12 后不再自动重试(需用户手动在设置中授权或清除缓存)
- none:未声明 autoVerify
legacy_failure 是最容易忽略的状态。如果你改过 assetlinks.json 但用户设备上 App 已安装,这个状态不会自动刷新。必须触发重新安装或手动清除:
adb shell pm clear-domain-preferences com.example.app
多场景路由实战
验证通过不代表所有场景都能按预期跳转。下面三个问题是实际项目中踩过的。
Chrome 的”地址栏输入”与”页面内点击”
Chrome 对 App Links 的处理分成两类:
- 地址栏输入或书签打开:即使域名已验证,Chrome 仍会显示选择器,用户需要点”在 App 中打开”。
- 页面内链接点击(
<a href>):域名已验证则直接跳转 App,不弹选择器。
这个差异化设计是 Chrome 的 Intent 派发逻辑决定的,不是 Android 系统行为,代码层面无法覆盖。
如果业务大量依赖地址栏直接输入(比如线下扫码后手动输入短链),可以考虑在 Web 端用 intent:// scheme 做兜底跳转,或者降级使用自定义 scheme。
跨域场景的验证范围
App Links 的验证是 严格域名匹配,sub.example.com 和 example.com 被视为不同实体。
在 Manifest 中声明多个 host 时:
<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="example.com" />
<data android:scheme="https" android:host="sub.example.com" />
</intent-filter>
系统会分别请求 example.com 和 sub.example.com 的 assetlinks.json。任何一个 host 验证失败,整个 IntentFilter 都会被标记为未验证。这是官方文档没有明确写的”全有或全无”逻辑。
App Link 与 Custom Tab 的优先级冲突
如果 App 里嵌入了 Chrome Custom Tab,当 Custom Tab 中加载的页面包含 App Links 域名时,行为取决于 Android 版本:
- Android 12 之前:Custom Tab 内的链接会直接唤起 App,导致 Custom Tab 退出,体验割裂。
- Android 12+:系统增加
Intent.FLAG_ACTIVITY_REQUIRE_NON_BROWSER判断,Custom Tab 不会触发 App Links 直接跳转,改为显示”在 App 中打开”横幅。
如果业务需要 App 内 WebView/Custom Tab 跳转到 App 自身页面,直接用 Intent 显式跳转比走 App Links 可靠得多。
调试工具箱
排查 App Links 问题,按以下顺序操作最快定位:
1. 验证文件层面
curl -I https://example.com/.well-known/assetlinks.json
# 检查 Status 200、Content-Type: application/json
Google 提供了在线验证工具:https://digitalassetlinks.googleapis.com/v1/statements:list?source.web.site=https://example.com&relation=delegate_permission/common.handle_all_urls
2. 系统状态检查
adb shell dumpsys package domain-preferred-apps
adb shell pm get-app-links com.example.app
3. 模拟系统验证
adb shell pm verify-app-links --re-verify com.example.app
这个命令会强制触发 re-verification,输出每个域名的验证结果。线上排查时比反复安装 App 快得多。
4. Chrome 的调试入口
在 Chrome 地址栏输入 chrome://interstitials,可以看到 Chrome 内部对 App Links 的处理日志,包括为何选择或不选择直接跳转。
写在最后
接入 App Links,真正花时间的不在 XML 配置和 assetlinks.json 编写——这些 30 分钟就能搞定。真正的成本在:理解验证机制的异步特性,处理不同场景下的行为差异,以及建立可复用的排查流程。
我的建议是:上线前用 release 签名打一个内部测试包,在真实网络环境下跑一遍 pm verify-app-links,确认所有 host 都是 verified 状态。这一步省掉,后面排查问题的时间成本至少翻 10 倍。