Chrome 让每部手机都自称 Android 10
从 2023 年起,Android 上的 Chrome 不管在哪部手机上,发出的 User-Agent 都一样:Android 10,型号 K。本文讲它给延迟深度链接的匹配带来了什么问题、我们为什么没用 Critical-CH 来解决,以及 Appy 链接域名现在怎样重新拿到真实的系统版本和型号。另外还有这个问题在 iOS 26 上的翻版。
现在每部 Android 手机都是“K”
拿一部装着最新版 Android 的 Pixel,再拿一部四年前的 Galaxy,用两部手机上的 Chrome 打开同一个页面。除了 Chrome 版本号,它们发出的 User-Agent 请求头一模一样:Android 10,设备型号 K。
这不是 bug,而是 user-agent reduction(UA 精简):Chrome 的一项隐私改动,把字符串里有助于给设备做指纹识别的部分冻结了。在 Android 上,它随 Chrome 110 推出。按照 Chromium 项目公布的时间表,推送从 2023 年 2 月 7 日开始,到 2023 年 5 月 11 日覆盖了所有 Android 客户端。
大多数网站根本没察觉。但凡要把应用安装和点击对上的系统,都察觉到了。
同一个请求头,前后对比
Chrome 110 之前
Mozilla/5.0 (Linux; Android 9; SM-A205U) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/93.0.0.0 Mobile Safari/537.36Chrome 110 及以后
Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/93.0.0.0 Mobile Safari/537.36延迟深度链接因此出了什么问题
延迟深度链接必须挺过一次安装。商店能携带令牌时,就会携带:Google Play 和华为应用市场都会把 install referrer 交给新安装的应用,匹配就此精确敲定。商店带不了的时候,比如安装始于商店搜索、来自侧载,或者来自不支持 referrer 的商店,就只能退而推测:找一条来自同一网络的近期点击,看它像不像同一部手机。
“像不像”过去靠的是 User-Agent。一条来自 Android 14 的点击,对首次启动时报告 Android 15 的手机来说是很差的候选;来自 Galaxy 的点击,对 Pixel 来说也一样。UA 精简之后,Chrome 的每次点击都说自己是 Android 10、K。版本号再也起不到缩小范围的作用;在拥挤的网络里,比如办公室或校园,整栋楼的 Android 手机看起来全都一个样。
Appy 如实看待 Android 10; K:没有版本,也没有型号。点击里没有别的信息时,同一网络里的所有 Android 点击看起来都一样,Appy 没法把你新用户的手机和其他手机区别开来。
教科书式的解法,会把每次点击算两遍
Chrome 并没有删掉这些信息,而是把它们挪进了 User-Agent Client Hints(用户代理客户端提示)。有几个低熵提示会随每个请求发送:浏览器品牌和主版本号、设备是否为移动设备,以及平台名称。这里真正要用的两个,Sec-CH-UA-Platform-Version 和 Sec-CH-UA-Model,属于高熵提示。只有网站用 Accept-CH 响应头请求之后,Chrome 才会发送它们,所以发往一个网站的第一个请求永远不会带上它们。
如果页面在首次加载时就需要这些数据,Google 为 Android UA 精简写的 Privacy Sandbox 指南(2023 年 2 月 27 日)给出的方案是 Critical-CH:服务器列出自己离不开的提示;Chrome 发现请求里缺了这些,就丢弃响应,带上提示把请求重发一次。
问题就出在“重发”。对跳转服务来说,这次重试就是对同一链接的第二次导航:Android 上的每次点击都会被算两遍,留下两条点击记录。我们宁可多做一点工作,也不愿把错误的数字放进每位客户的后台,所以 Appy 不用 Critical-CH。
Appy 链接域名的替代做法
两套机制:一套负责浏览器里的第一次点击,另一套负责之后的每一次点击。
手机上的 Chrome
acme.appy.to
- 01第一次点击链接
GET /spring HTTP/2 Host: acme.appy.to User-Agent: Mozilla/5.0 (Linux; Android 10; K) … Chrome/154.0.0.0 Mobile Safari/537.36 Sec-CH-UA-Mobile: ?1 Sec-CH-UA-Platform: "Android"只有低熵提示。UA 显示 Android 10,型号 K。
- 02中转页
HTTP/2 200 Accept-CH: Sec-CH-UA-Platform-Version, Sec-CH-UA-Model Content-Type: text/html; charset=utf-8负责打开应用或商店的页面。Accept-CH 要求之后的每个请求都带上这两个提示。
- 03跳转之前
const hints = await navigator.userAgentData .getHighEntropyValues(["platformVersion", "model"]) → {platformVersion: "16.0.0", model: "Pixel 9", …} navigator.sendBeacon(…)页面读取真实值,附到这次点击的记录上,然后再跳转。
- 04之后的每一次点击
GET /summer HTTP/2 Host: acme.appy.to Sec-CH-UA-Platform-Version: "16.0.0" Sec-CH-UA-Model: "Pixel 9"在用户清除网站数据之前,Chrome 会为这个源(origin)记住这项请求,所以之后提示会直接放在请求头里发出。不用脚本,也不用重试。
真实版本和型号改变了什么
点击里有了真实的版本和型号,点击和手机就必须在这两项上都一致。SDK 在应用首次打开时通过 Build.MODEL 上报手机型号,两边因此可以对照。一条 Pixel 9 的点击,再也不会被当成 Galaxy 上的安装;一条来自 Android 15 的点击,也不再对得上运行 Android 16 的手机。
差别在拥挤的网络里最明显。在有四十部手机的办公网络里,其他机型的点击会被排除,往往只剩下 Pixel 自己的那条点击。
以上说的都是没带令牌的安装。在 Android 上,大多数安装仍然先由 Google Play 和华为应用市场的 install referrer 敲定,Client Hints 负责剩下的那些。没有 referrer 或点击 ID,Appy 就无法核实这次安装;开启严格归因时,不管点击对得多准,它都会计为自然安装。
同一个办公网络,前后对比
一部 Pixel 9 在拥挤的办公网络里第一次打开应用,距离机主点击链接已过去四分钟。机主关掉了链接打开的商店页面,改为在 Play 商店搜索后安装,所以没有 referrer 传过来。
iOS 冻结了自己的版本号
Safari 走的是另一条路,结果殊途同归。Apple 的 Safari 26.0 发布说明提到,在 iOS 26 和 iPadOS 26 上,Safari 现在会在 UA 里报告一个冻结的系统版本,也就是 iOS 26 之前发布的最后一个版本。实际上,运行 iOS 26 的 iPhone 会自称“iPhone OS 18_6”。字符串后面的 Version/26.0 标记仍然跟随 Safari 自身的版本。
Safari 不支持 User-Agent Client Hints,所以没有什么可请求的。Appy 改在匹配时处理:如果手机运行的是 iOS 26 或更高版本,写着 iOS 18.6 的点击会被理解为“26 或更高”,而不是一个确切的版本,因为 Safari 已经不再说明具体是哪个版本。
如果你自己做安装匹配
下面这些做法并不限于 Appy。
把“Android 10; K”当作未知
解析它,然后放到一边。一个看起来像数据的冻结值,比空字段危害更大。
在承接点击的域名上请求提示
从你的链接域名发送 Accept-CH: Sec-CH-UA-Platform-Version, Sec-CH-UA-Model,并在首次访问时读取 navigator.userAgentData,因为这时请求头里还不可能有这些提示。
加 Critical-CH 之前,先算算重试会带来什么
重试的导航就是第二个请求。如果你按请求计数,就要去重,或者干脆不用重试。
把 iOS 18.6 映射为“26 或更高”
不要因为点击里写的是 18.6 就拒绝一次 iOS 26 安装。把它当作 iOS 26 或更高,而不是一个确切的版本。
优先用令牌,而不是推测
install referrer 和点击 ID 比任何 UA 都可靠。只有在没有任何确定性信号时才去推测,并且要说明这是推测。
延伸阅读
常见问题
为什么 Chrome 在每部手机上都说自己是 Android 10?
因为 UA 精简(user-agent reduction)。从 2023 年 2 月至 5 月间推送的 Chrome 110 开始,Android 上的 Chrome 会在 User-Agent 字符串里把系统版本冻结为 10,把设备型号冻结为 K。真实值可以通过 User-Agent Client Hints 获取。
怎样在 Chrome 中获取真实的 Android 版本?
用 Accept-CH 响应头请求 Sec-CH-UA-Platform-Version 提示,或者在页面里调用 navigator.userAgentData.getHighEntropyValues(["platformVersion"])。只有在 Chrome 看到你的 Accept-CH 之后发出的请求,才会带上这个请求头。
Critical-CH 能解决第一个请求的问题吗?
它靠让 Chrome 重发请求来拿到提示。对普通页面,这没问题。但对任何按请求计数的场景,比如跳转或点击追踪,除非做去重,否则重发看起来就像第二次访问。
Safari 在 iOS 26 上报告什么?
一个冻结的系统版本。在 iOS 26 和 iPadOS 26 上,Safari 报告的是 iOS 26 之前发布的最后一个版本,实际为 18_6,而 Version/26 标记仍跟随 Safari。Safari 不支持 User-Agent Client Hints。
Android WebView 和应用内浏览器的 UA 也会被精简吗?
默认还不会。Google 表示,从 Android 17 开始,WebView 的默认 User-Agent 也会受到同样的处理;自行设置 User-Agent 的应用会保留自己的设置。
继续探索
不用 SDK 的延迟深度链接:底层到底是怎么运作的
不嵌入归因 SDK,也能把链接意图带过应用安装这道坎。本文讲清 Android 和 iOS 上什么能真正穿过商店边界、什么会断掉,以及每种变通方案的隐私代价。
看清每次安装来自哪条链接:Appy 的 iOS 与 Android SDK
Appy 的链接现在能跟到应用商店之后。新的 SDK 为每条链接、每个二维码、每个营销活动统计安装、应用内事件和收入,还让新用户第一次打开就进入对的页面。
延迟深度链接(deferred deep linking):原理与使用时机
把原始链接意图带过安装环节,让新用户首次打开应用就直接进入对应页面,而不是默认首页。
想看更多内容?浏览 博客全部主题.