ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Android App Links 跳转不一致:从 assetlinks.json 到系统差异的完整排查

Android App Links 跳转不一致:从 assetlinks.json 到系统差异的完整排查 同样一个链接用户今天一点直接进了App明天再点却弹出了系统选择器甚至干脆落在浏览器里。这种链接跳转表现不一致的问题我在App Links的排查里碰到过不止一次标题里的Inconsistent presentation of links说的就是它。核心绕不开三个环节app links 配置、assetlinks.json 验证、以及未安装应用时的下载兜底。搞了几年深链开发后我越来越清楚链接能不能稳定跳转根本不是 manifest 里写个 autoVerifytrue 就能解决的事而是从静态文件托管、证书指纹、签名一致性到系统版本差异一环扣一环。这篇文章我就把实际踩过的坑、排查过程和处理方案完整拆开讲适合正在做 Android 深链接入、或者被线上链接跳转搞得头大的移动端研发参考。1. 链接跳转表现不一致问题到底出在哪1.1 三种看起来一样、底层完全不一样的链接先说个容易混淆的点。市面上叫深链的东西其实分好几种表现层都是点一个链接打开App但系统处理链路完全不同这也是表现不一致的第一个根源。第一种是自定义 Scheme 的 Deep Link比如myapp://detail?id123。这种链接只要 App 注册了对应的 intent-filter系统就会尝试匹配。但问题在于自定义 scheme 的命名空间是全球共享的任何 App 都可以声明同一个 scheme所以 Android 默认会弹一个打开方式的选择器让用户选而且一旦用户选了某个应用并勾选了始终以后就再也不会问你了。这种表现就是典型的时灵时不灵。第二种是 Intent Scheme形如intent://detail?id123#Intent;schememyapp;packagecom.example.app;end。它可以在网页里通过 JS 或 a 标签触发能指定包名甚至 fallback 地址。但 Chrome 从 25 版本之后就限制非浏览器环境直接拉起 intent移动端 WebView 里经常被拦截兼容性极差。我接手过一个老项目就用这种方式线上反馈一半用户能打开一半打开失败最后全部迁移到了 App Links。第三种是 Android App Links使用的是标准的https://链接配合autoVerifytrue和服务器上的assetlinks.json文件做数字资产校验。只有校验通过系统才认为是已验证的链接点击后直接拉起 App 且不弹确认框。这是目前 Google 推荐的方案也是绝大多数链接跳转表现不一致问题集中爆发的区域——因为它引入了服务端配置、签名校验、系统版本差异等多个变量任何一个环节出错表现就会退化。1.2 不一致的典型表现清单我在实际项目里把用户反馈和测试结果归了一下类基本逃不出下面这几种用户看到的现象底层实际原因问题归属第一次点直接进App第二次弹确认框assetlinks.json 验证失败系统回退到普通深链验证环节同一链接部分机型进App部分机型开浏览器系统版本差异低版本不自动重试验证版本差异链接能打开网页但 App 已安装就是不响应intent-filter 的 path 或 host 配置不匹配配置环节未安装 App 时直接打开网页首页没有下载引导缺少网页端 fallback 或 smart banner未安装兜底用户之前选过始终用浏览器之后再也进不了App用户偏好被系统持久化用户偏好开发环境正常线上包跳不了debug 签名和 release 签名指纹不一致签名环节看到这里应该有个直觉这个问题的本质不是单一原因崩了而是多个环节里有一个悄悄失效。下面我从最常见的 assetlinks.json 开始逐个拆。2. assetlinks.json 验证是App Links的地基2.1 assetlinks.json 到底怎么放才正确App Links 的验证机制说起来很简单系统安装或升级 App 时会根据 manifest 里声明的 host 去请求https://你的域名/.well-known/assetlinks.json拿到文件后用里面的包名和证书指纹和本地安装包比对一致就标记为 verified。但正确这两个字坑很多。文件必须放在域名根目录下的.well-known目录里路径不能自定义。这个目录很多人会拼错——注意是.well-known点号开头中间一个连字符不是.wellknown也不是well-known。服务器必须返回 200不能重定向到别的地方不能要求登录不能带鉴权头。我之前遇到过一个项目前端做了 SPA 路由所有未匹配的路径都返回index.htmlassetlinks.json 看似能访问但返回的内容是 HTML 而不是 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:AD:A3:3A:6E:9C:79:8A:31:3D:1A:90:FD:9C:E7:81:9D ] } } ]这里有个容易忽视的细节relation字段默认是delegate_permission/common.handle_all_urls如果你的网页只想让 App 处理特定路径可以写多个 statement但不能把handle_all_urls和handle_as_http混在一个 statement 里。我在一个项目里为了同时支持整站跳 App和某些页面留在浏览器在这里反复改过几次最后是把两条 statement 分别写清楚才稳定。另外一个实战提醒assetlinks.json 里可以包含多个 App 的条目也可以包含同一 App 的多个指纹。文件是 JSON 数组每一项算一条 statement。一定要保证输出内容能被标准 JSON 解析器解析不带 BOM末尾没有多余逗号。很多编辑器保存时自动加 BOM或者 JSON 里混了注释解析器直接报错——这种问题在浏览器里访问看着正常但系统验证时用的是严格解析一票否决。2.2 SHA-256 指纹怎么取才不出错证书指纹这个环节是我见过翻车频率最高的地方。很多人直接用 Android Studio 面板里显示的 SHA-1 或者把 debug 签名的指纹传到生产环境的 assetlinks.json 里结果就是线上用户全部被打回浏览器。正确的取法是拿你线上正式签名的 keystore 来算。命令行方式keytool -list -v -keystore your-release.jks -alias your-alias -storepass your-pass输出里找到SHA256:那一行然后把冒号和小写字母处理成大写。assetlinks.json 里要求的是带冒号且大写的形式比如14:6D:E9:...。这里有个坑值得单独说某些 JDK 版本下 keytool 输出的 SHA256 只有一行而 Android Studio 的 签名面板或者 Gradle 的 signingReport 任务输出的指纹格式可能不带冒号或者全是小写两种格式混着用就会校验失败。我建议统一用 keytool 的输出手动转大写复制过去之后再用 Google 官方的 Digital Asset Links 测试工具验证一遍。测试工具地址是https://developers.google.com/digital-asset-links/v1/tester输入你的域名和 AppID它会直接调用官方 API 查 assetlinks.json 的解析结果和指纹匹配情况。这个工具在我的排查流程里排第一位比在真机上反复安装测试快得多。最容易被忽视的是 debug 和 release 的签名不一致。开发阶段你用的是 debug.keystore 签名生成的指纹和线上发布包的 release 指纹完全不一样。如果你在 assetlinks.json 里只写了 release 指纹本地 debug 包永远验证不通过反过来如果只写了 debug 指纹线上包又验证不通过。规范做法是把两条指纹都写进去或者至少搞清楚当前测试的是哪种签名。实际项目里我更推荐用 Gradle 的 signingConfig 区分 flavors让 debug 和 release 用不同的 keystoreassetlinks.json 里两条都维护这样开发和线上互不干扰。2.3 为什么验证失败却看起来正常这是Inconsistent presentation of links最迷惑人的地方很多链接验证失败了但表现上并没有完全失效。原因在于 App Links 校验失败之后系统会静默降级为普通深链处理——链接依然能打开浏览器里的对应网页如果该网页上又有拉起 App 的脚本或者 smart banner用户点一下还是能进 App整体体验看起来能用。但降级后的行为和你预期完全不同。普通深链会弹系统选择器或者被浏览器接管用户每次点击都可能得到不同结果今天直接进、明天弹选框、后天开浏览器全看当时系统状态和用户之前做过什么选择。所以看起来正常恰恰是最大的信号如果你发现链接没有一个确定性的跳转结果那就是验证没通过先查 assetlinks.json别急着改业务代码。还有一个容易误导的场景你在浏览器地址栏直接访问https://example.com/.well-known/assetlinks.json看到内容显示正常就以为服务器没问题。但系统验证时用的是独立的网络请求可能被 CDN 缓存、被 WAF 拦截、被运营商劫持。我遇到过 CDN 把 assetlinks.json 缓存了旧版本App 升级换了新包之后指纹对不上整整一周线上验证全挂CDN 刷新之后瞬间恢复。所以排查时一定要看 CDN 和反代层的缓存策略assetlinks.json 这类低频变更文件建议设置较短的缓存时间甚至通过版本号参数绕过缓存。3. 未安装场景为什么有的链接能跳下载页有的直接4043.1 App Links 对未安装应用的处理机制用户没装 App 时点击链接应该怎么办是另一个和 未安装、下载 强相关的重灾区。先说系统层面的机制App Links 的验证目标是本机已安装的应用如果系统发现没有任何已安装且验证通过的 App 能处理这个链接那么链接就交给浏览器处理——注意这里没有自动打开应用商店的机制系统不会管你 App 是否在商店上架更不会自动跳下载页。所以你看到的未安装时能跳下载页一定是网页端做了兜底逻辑而不是系统行为。常见的兜底有三类第一类是微信/微博这类社交平台内实现的智能跳转它们通过白名单来控制第二类是普通浏览器里的网页应用了 Google Smart Lock 的 App Indexing 或者 Manifest 里的related_applications第三类就是最通用的网页脚本在页面加载时检测客户端是否能处理自定义 scheme不能就跳商店。第三类实际最可控但需要和后端网页团队配合。如果使用 Intent Scheme 方式的网页会有标准的 fallback 写法a hrefintent://example.com/path#Intent;schemehttps;packagecom.example.app;S.browser_fallback_urlhttps%3A%2F%2Fplay.google.com%2Fstore%2Fapps%2Fdetails%3Fid%3Dcom.example.app;end 打开 App /a这种写法在 App 未安装时会把用户带到browser_fallback_url指向的下载地址在 App 已安装且验证通过时会直接拉起。它的问题在于对 Chrome 之外的浏览器兼容性参差不齐而且在 iOS 上没有对应实现。所以如果做跨端深链我建议网页端用https://链接 JS 检测的通用方案Android 端走 App LinksiOS 端走 Universal Links服务端统一维护一份下载页 智能识别逻辑而不是依赖某一种网页写法。3.2 方案对比网页回退 vs Play商店直达 vs 下载页落地兜底方案实现成本体验一致性适用场景仅打开普通网页最低差用户找不到下载入口临时处理网页内置 smart banner中需接 Google Play Services较好但仅官方市场单一商店分发网页脚本检测后跳转下载页中高最好可自定义埋点多市场分发、运营需要HTTP 302 服务端判断 UA 后重定向中依赖 UA 识别有误判风险大流量落地页我自己的经验是工业级方案应该是服务端重定向 网页脚本 下载页参数透传三层配合。比如分享出去的链接里带?channelxxxfromyyyApp 已经安装时打开 App 时读取这些参数完成归因App 未安装时下载页拿到同样的参数用户安装后第一次启动时通过install referrer接回去。这种方案每一层都有明确分工出问题也容易定位。这里有个常被忽略的技术细节即使 App 未安装assetlinks.json 仍然必须保持可访问且内容正确。因为 Android 的校验动作在安装阶段就会发生而且网页浏览时 Google Play Services 也会做一次基于网页的验证用于决定浏览器的提示形态。如果 assetlinks.json 挂了哪怕你下载页做得再完善已安装的用户也会被降级未安装的用户也会失去打开 App的智能提示等于两头都吃亏。4. Android系统版本差异与验证状态的真实反馈4.1 不同Android版本的验证行为差异Inconsistent presentation还有一个隐蔽推手Android 不同版本对 App Links 的验证和重试策略不一样导致同一个链接在不同机型上表现不同。Android 6 到 Android 10 时代App Links 的验证基本只在应用安装或更新时触发一次。如果此时 assetlinks.json 因为网络抖动、服务端没配置好等原因导致验证失败之后系统不会主动重试除非你卸载重装或者清除应用数据。很多开发者在本地测试时因为刚装完、文件也正确验证通过但线上用户可能是在我们配置错误的窗口期装的包之后永远等不到重试于是长期表现为别人能跳我不能跳。从 Android 11 开始系统增加了手动验证链接入口用户可以在系统设置 - 应用 - 打开支持的链接里主动触发验证且系统在尝试匹配时会更宽容地重试。Android 12 和 13 则进一步收紧了未验证状态的行为如果链接未通过验证系统甚至不会把它列为可处理的 App用户点击根本不会看到你的 App 选项。这个收紧方向本身是好事但如果你还在用旧时代的深链 选择器思维升级系统后用户表现会断层式变差。另外一个版本相关的小坑Android 11 及以上adb shell pm get-app-links的输出格式更丰富可以直观看到每个域名的验证状态是verified、failed还是none。低版本则要用dumpsys package domain-preferred-apps去翻信息量少且难读。我已经把这两条命令放进所有项目的 README 里了每次排查链接问题第一件事就是跑一遍。4.2 用命令确认系统当前的验证状态这是排查 App Links 问题最高效的一步两条命令吃遍所有场景。# Android 11 adb shell pm get-app-links com.example.app输出示例com.example.app: ID: 6f1b2c3d-xxxx-xxxx Signatures: [14:6D:E9:...] Domain verification state: example.com: verified www.example.com: failed看到verified说明这一项没问题看到failed就说明配置确实有问题直接去查 assetlinks.json看到none常见原因是响应头不对或者网络层不可达但也可能是应用根本没声明这个域名的 intent-filter。# Android 6~10 或者想看用户偏好时 adb shell dumpsys package domain-preferred-apps这段输出里重点看Package: com.example.app下面的Domain verification status和User precedence。User precedence代表了用户有没有手动设置过始终用某 App 打开如果这里显示了你没意料到的条目说明用户的记住选择在起作用代码层面再改也改不回用户的自主选择。4.3 用户记住选择带来的假性不一致这一点其实是很多不稳定反馈的真正来源。当多个应用声明了同一个链接或者 App Links 验证失败导致系统降级时系统会弹出一个选择器。用户如果在这时选择了始终用浏览器打开那么这个选择会被持久化保存系统以后会绕过你的 App直接按用户选择处理。用户自己可能完全不记得自己点过始终于是反馈就是链接时好时坏但实际是系统偏好设置的问题。测试团队也常踩这个坑一台设备上之前测试别的应用时选了始终用某应用打开换了一个应用测试时发现链接根本不弹选项误以为是新应用的配置问题。所以我建议测试流程里固定一条清理命令adb shell pm clear-package-preferred-activities这条命令清掉所有应用的偏好设置恢复系统默认行为。每个测试用例执行前跑一次可以大幅减少环境污染导致的假性 bug。同时要在测试用例文档里明确写不能用同一台设备连续测两个不同 App 的链接行为必须清偏好或换设备。5. 常见问题与排查技巧实录5.1 高频问题速查表结合这些年的接待和线上问题我把高频问题整理成了一张速查表方便排查时对照现象优先检查项常见根因链接从未直接打开 App总在浏览器adb shell pm get-app-links状态assetlinks.json 不可访问或内容错误打开时弹选择器域名的验证状态验证失败降级为深链部分域名能进 App部分不能manifest 各域名的验证状态某个 host 没配置或指纹不匹配debug 包能进release 包不能指纹列表assetlinks.json 只配了 debug 指纹线上链接今天正常明天不正常CDN 缓存 / 文件被误删服务端变更未同步未安装应用时打开空白页网页兜底逻辑页面脚本未处理未安装分支用户反馈刚开始能用后来不行设备用户偏好用户选了始终用浏览器打开5.2 一套标准排障流程我自己在项目里沉淀了一套固定排查顺序遇到链接问题不慌按这个顺序走一遍基本能定位。第一步先用官方 Digital Asset Links 测试工具验证域名和 AppID确认服务器端配置是否通过——这一步能把服务端问题在 30 秒内排除。第二步本地装一个开发包执行adb shell pm get-app-links看验证状态如果状态是failed重点看工具返回的具体错误是不是 JSON 解析失败。第三步检查 intent-filter 里的路径匹配规则。很多项目在链接上加pathPrefix或pathPattern限制跳转路径但实际线上分享出去的 URL 路径格式和规则不一致结果某些链接能打开、某些不能。第四步确认用户实际点击的 URL 是不是标准形式。我碰到过一个有意思的案例运营在后台手工拼接链接URL 里带了中文字符和多余空格跳转时部分编码部分没编码导致同一个链接在不同客户端表现不同。第五步才是查代码里的跳转逻辑和统计埋点。很多链接打开后 App 内又做了一层二次跳转如果这层逻辑有判断分支就会让用户看到点链接后进了个奇奇怪怪的中间页。这里的关键心得是一定要先确认系统层面的验证状态再去查业务代码。系统状态是原点业务逻辑是分支原点没对后面所有排查都是浪费时间。而且每排查一步都要记录当时的验证状态和复现路径避免下次再从头查一遍。5.3 几个让链接稳定运行的经验最后分享几个能长期减少Inconsistent presentation of links问题的工程经验。第一assetlinks.json 的变更流程要和发版流程绑定。换个签名证书、加个新域名都应该在发版清单里有一项更新并验证 assetlinks.json并且要有 CDN 刷新和官方工具验证这两个动作凭记忆真的别信。第二增加监控。在你的核心链路上埋点记录用户点击来源、落地 App 后的原始 URI、以及打开过程中的错误用实时告警覆盖验证失败率异常升高的情况。我在项目里是把 Verification 状态上报到了日志平台pm get-app-links的结果在测试阶段自动跑一旦发现verified变成failed就会触发告警这个习惯帮我们提前发现过两次证书到期导致的隐患。第三内测阶段就要引入真机云测矩阵覆盖 Android 11 以下的旧版本和主流厂商 ROM。很多国产 ROM 对 App Links 的处理并不完全遵守 AOSP 行为有些会默认禁止后台拉起、有些会额外弹权限询问这些表现差异在线下用户手里会放大成链接坏了的投诉。提前在测试矩阵里把厂商差异摸清楚至少能做到心里有数而不是被用户牵着走。第四给运营和客服部门准备一份 FAQ说明链接跳转受系统和用户选择影响以及最简单的处理办法比如在系统设置里清除默认打开方式。很多客服反馈的链接失效其实根本不是技术故障而是用户设备端偏好或系统版本的问题一份 FAQ 能省掉大量无效排查时间。我在实际项目里被这个问题折腾最久的一次是线上版本发布后一周内用户反馈点击分享链接进入 App 的成功率从 90% 掉到 30%。当时代码没有任何改动assetlinks.json 也没动过最后查出来是 CDN 某条线路把旧文件缓存到了边缘节点部分地区的用户请求到了旧 JSON指纹和新包的签名对不上。从那之后我做了一个强制规定assetlinks.json 的响应里加Cache-Control: no-cache并且每次发版前用官方工具跑一遍再让 QA 用一台全新设备做首装验证。链接跳转这种东西用户是感受不到你中间做过多少努力的但只要它有一次表现不一致用户对产品的信任就会掉一截。把验证状态、缓存策略、版本差异这些变量管住你才能真正做到同一个链接永远打开同一个 App。
返回列表