1. 一个“用户全变新”的诡异Bug:先认识SSAID
有段时间我们运营后台的数据特别怪,某次版本灰度之后,老用户数量骤降,“新用户”却一夜之间翻了好几倍。产品经理以为是渠道投放爆了,技术侧一查,发现很多老设备拿到的设备标识变了,后端按设备标识做用户去重,直接把一批活跃用户判定成了新设备。
问题出在一个字段上:SSAID。这东西全称叫 Settings.Secure.ANDROID_ID,是 Android 系统里一个存在很久的匿名设备标识。不要把它和 IMEI 混为一谈,它既不和设备硬件绑定,也不是拿不到授权的敏感权限项。它本质上是一串在设备首次开机时随机生成的字符串,存在系统设置里,后续系统、应用都能读到。
你可以在自己的应用里用两行代码把它取出来:
val ssAid = Settings.Secure.getString( contentResolver, Settings.Secure.ANDROID_ID )很多做数据统计、广告归因、反作弊的老开发,对这个值又爱又恨。爱的是它不需要任何运行时权限,不像 IMEI 那样在 Android 10 之后几乎拿不到;恨的是它的生命周期和取值规则在不同 Android 版本里变了好几次,稍微没注意,线上就出各种“灵异事件”。
这篇文章不想复述官方文档,我想从实际踩坑的角度,把 SSAID 是什么、怎么取、有哪些边界情况、以及今天做设备标识到底该怎么选型,一次性讲清楚。
1.1 名字拆解:为什么叫 SSAID 而不是直接叫 Android ID
Android 系统内部有很多“设置表”,其中 Settings.Secure 是存放一些不直接暴露给用户、但也不至于敏感到需要系统级权限才能读的键值对。ANDROID_ID 就是存在这张表里的一个 key,值为 16 位十六进制字符串(在某些实现里也可能是不同长度)。
所以大家口语里的 SSAID,其实就是 Settings.Secure.ANDROID_ID 的缩写。很多 SDK 文档里写“获取 SSAID”,和你去读 Settings.Secure.ANDROID_ID 是同一个东西。早期的项目里,还有人把它写成 ANDROID_ID、AndroidID、AID,都是一回事。
1.2 它到底是怎么生成的:不是硬件 ID,是“第一次开机”的随机数
我第一次看源码时以为 ANDROID_ID 会从设备序列号、MAC 地址之类的东西里算出来,结果完全不是。系统在设备第一次开机时,如果发现 Settings.Secure 里没有 android_id 这个键,就会用一个安全的随机数生成器生成一个 64 位随机数,再转成十六进制字符串写进去。之后这台设备的所有用户空间,正常情况下都会一直用这个值。
换句话说,它更像一个“设备实例标识”,而不是厂商定义的硬件序列号。它不保证全球唯一,也不保证不可变。恢复了出厂设置、刷机、部分品牌的数据备份恢复,都可能让这个值重新生成。这也解释了为什么它不需要任何权限——系统从一开始就没打算让它承担硬件级标识的职责。
2. Android 版本演进,SSAID 的“身份规则”改了三轮
如果你只写过 Android 8 之前的老代码,然后直接把它搬到现在的项目里,一定会踩坑。因为 SSAID 在不同版本的 Android 里,取值规则发生过非常明显的调整。
2.1 Android 8.0 之前:一机一号,跨应用随便读
在 Android 8.0(API 26)之前,SSAID 的取值逻辑非常简单:一台设备上,所有应用读到的值都一样。也就是说,你用 App A 读到的 SSAID,和用 App B 读到的 SSAID,完全一致。这在那个年代特别适合做设备级归因、渠道去重、风控关联。
但问题也随之而来:
- Android 2.2(Froyo)时期出现过广为人知的“万能 Android ID”,大量设备返回同一个固定值
9774d56d682e549c,导致很多 App 把所有用户都识别成了同一个人。 - 不少厂商的定制 ROM 在恢复出厂设置时没有正确清除 Settings.Secure,导致用户恢复出厂后 SSAID 不变。
- 因为所有应用都能拿到同一个值,第三方 SDK 之间悄悄互传设备标识做用户画像,几乎没有任何技术门槛。
也是从那个时期开始,SSAID 在广告行业和风控体系里被用到了极致。但也正是因为这样,它的“可跨应用追踪”能力逐渐变成了隐私问题。
2.2 Android 8.0 之后:签名、用户、设备三个维度绑定
Android 8.0 开始,Google 把 SSAID 的取值规则改了,核心变化是:同一台设备上,不同签名密钥签名的应用,读到的 SSAID 不一样。
也就是说,现在的 ANDROID_ID 是由“应用签名密钥 + 用户 + 设备”三个维度共同决定的。具体表现是:
- 同一台设备上,你的 App 和别的公司签名的 App,拿到的 SSAID 是不同的。
- 同一个签名密钥签名的多个 App,在同一个用户空间下,拿到的 SSAID 是相同的。
- 如果某个应用更换了签名密钥,升级后读到的 SSAID 会发生变化。
- 同一台设备上的不同用户空间(比如手机分身、应用双开、访客模式),读到的 SSAID 也不同。
这直接导致了一个常见线上事故:App 升级到 Android 8.0 及以上系统后,老用户突然被识别成新设备。我在第四节会专门复盘这个问题。
2.3 Android 10 之后:系统从“硬件 ID”全面转向“安装实例 ID”
在 Android 10 和之后的版本里,系统对设备标识的整体态度越来越明确:普通应用不应该再拿到跨应用通用的硬件级标识,IMEI、MAC 地址这些限制越来越严,权限门槛越来越高。
SSAID 虽然还能被普通应用直接读取,但它的“跨应用唯一性”已经被削弱了。它现在更多适用于:作为你自己应用内的设备实例标识,用来做数据缓存键、安装去重、以及账号弱绑定。如果你想拿它做跨 App 的用户关联,那基本走不通了,Android 8 之后不同签名 App 拿到的 SSAID 本身就不一样。
所以我的判断是:SSAID 并不是一个已经过气的字段,而是它的定位变了。从“设备唯一标识”变成了“应用维度下的设备实例标识”。理解这一点,后面所有方案选型都会清晰很多。
3. 代码获取 SSAID:最短示例与最容易翻车的边界情况
3.1 最短可用示例
获取 SSAID 不需要在 AndroidManifest.xml 里申请任何权限,直接读即可。Kotlin 写法:
val ssAid: String? = Settings.Secure.getString( context.contentResolver, Settings.Secure.ANDROID_ID )Java 写法同样简单:
String ssAid = Settings.Secure.getString( getContentResolver(), Settings.Secure.ANDROID_ID );如果你只是为了在本地做一个标识,这个值基本可以拿过来直接用。但请一定注意:它可能为 null,也可能是特殊值。很多线上事故就是从这里开始的。
3.2 为什么不需要权限,却总有人误以为需要
原因很简单:很多团队的旧代码是从 IMEI 迁移过来的。原来拿 IMEI 要申请 READ_PHONE_STATE 权限,后来发现拿不到,就改成读 SSAID,却还沿用老一套流程去申请权限。实际上,SSAID 被设计为“系统设置中的匿名标识”,读取它不需要任何权限。
这里有个容易误解的地方:不需要权限,不代表它是敏感字段。它只是一个随机生成的标识符。它不包含手机号、型号、序列号等信息。除非你在后端做了关联,否则它本身并不泄露用户隐私。
3.3 返回 null、全 0、固定值:厂商定制系统的“惊喜”
我见过最离谱的几种情况,建议你在使用前至少做一个兜底过滤:
| 情况 | 现象 | 原因 |
|---|---|---|
| 返回 null | 某些系统应用或受限环境下读不到 | 厂商定制 ROM 对 SettingsProvider 做了改动,或系统未完成初始化 |
| 全 0 | 0000000000000000 | 部分模拟器、虚拟化环境、测试固件 |
| 固定值 | 大量设备返回同一个 ID | 老版本 Android 2.2 的 bug,或某些定制 ROM 的兼容问题 |
| 恢复出厂后不变 | 设备重置后 ID 仍然一致 | 厂商备份恢复机制把 Settings.Secure 一起恢复了 |
针对 null 和异常值,最稳妥的做法是:不要直接把异常值写入业务库。可以先生成一个本地 UUID 作为备用标识,同时记录 SSAID 是否异常。等设备进入正常状态后,再尝试读取一次并做关联。这也是很多 SDK 的通用做法。
3.4 拿去做哈希之前,务必想清楚这三件事
很多人有个习惯:把 SSAID 做一次 SHA-256 再存到后台,觉得这样就“脱敏”了。这个思路对了一半,但有几个前提必须想清楚。
第一,哈希不加盐,等于白做。同一个 SSAID 哈希出来是确定的值,别人同样可以反查。建议在服务端用带盐的 HMAC 或加盐哈希保存,盐单独管理。
第二,哈希只能解决“存储侧”的明文问题,不能解决 SSAID 本身语义变化的问题。该在 Android 8 之后变化的还是会变。
第三,不要在客户端只存哈希值。如果你在后端需要根据原始 SSAID 做人工排查,只存哈希会让你什么都查不了。我一般建议后台存两份:一份是哈希后的关联 ID,一份是设备指纹相关的辅助信息,原始值则尽量减少留存周期。
4. 我踩过的 SSAID 相关坑:完整复盘四个典型场景
下面这四个坑,每一个都是我真实遇到过的。我把排查链路写出来,比直接给你结论更有用。
4.1 用户重置手机后,SSAID 居然没变
现象:线上反馈,某品牌手机恢复了出厂设置,重新安装 App 后,后台仍然识别成同一个设备。第一反应是“恢复出厂肯定会变”,但数据不会骗人。
排查过程:先确认版本行为。大部分设备恢复出厂设置后,Settings.Secure 被清空,SSAID 会重新生成。但有个别品牌的“换机助手”或“云备份”功能,会把系统设置也一起恢复。用户重置后,系统又从备份里把 android_id 写回去了。
根因:不是系统 bug,是备份恢复机制把 SSAID 也当成了可恢复设置。
解决建议:不要依赖“恢复出厂必变”这个假设。如果业务上需要区分“设备恢复后的新实例”,可以结合其他信号判断,比如应用首次安装时间、设备启动时间戳等。单纯拿 SSAID 做设备生命周期判断,在部分机型上会失真。
4.2 升级 Android 8 后,老用户全部掉线
现象:某 App 的用户量没有变化,但 DAU 统计里的“新设备数”突然暴涨,老设备大量流失。查后端日志发现,同一台设备升级到 Android 8.0 系统后,上报的 SSAID 变了。
排查过程:先查代码里 SSAID 的读取方式,没有变化。再查是否是升级过程中应用数据被清,也没发现。最后对比设备型号和系统版本才发现,变化的设备全部集中在 Android 8.0 及以上系统。结合文档一看,Android 8.0 之后 SSAID 已经把“应用签名”纳入了取值因子,App 签名没变,但系统从旧版本升级到新版本之后,取值算法变了,老值自然对不上。
根因:Android 版本升级导致 SSAID 取值规则变化,不是应用数据丢失。
解决建议:如果你维护老项目,建议在后台同时维护两个字段:历史 SSAID 和当前 SSAID,并在首次发现变化时做一次自动关联。这比直接按新值建用户要稳妥得多。尤其是做账号体系绑定的场景,别让 SSAID 成为唯一主键。
4.3 应用双开、手机分身里的“一人多号”
现象:用户在同一台手机上开启应用双开,两个实例被后台识别成了两个独立设备。这在某些业务里不算 bug,但在做“一设备一账号”限制的业务里就成了误杀。
排查过程:应用双开在大部分 ROM 里是基于多用户机制实现的,每个用户空间有独立的 Settings.Secure,所以 SSAID 不同。如果你以为它是“设备唯一”的,不做兼容,就会出现同一个物理设备产生多个逻辑设备的情况。
根因:SSAID 按用户空间隔离,它就是会不同的。
解决建议:如果业务必须识别“同一个物理设备”,单靠 SSAID 不够,需要叠加其他设备特征(型号、分辨率、传感器列表、系统版本、时区等)做综合设备指纹。如果业务只是需要“安装实例级标识”,那 SSAID 在双开场景下反而更合理:每个用户空间一个标识,互不干扰。
4.4 把 SSAID 当密钥因子,结果用户数据全毁
现象:某团队做本地数据加密,用 SSAID 作为 AES 密钥的一部分。结果用户恢复出厂设置后,SSAID 变化,之前加密的本地数据再也解不开。用户反馈是“App 白屏”“登录后数据全没了”。
排查过程:看日志,本地数据库文件还在,但解密失败。再查密钥派生逻辑,发现用了 SSAID 作为因子。恢复出厂后 SSAID 变化,密钥自然就变了。
根因:把“标识”当“密钥因子”,混淆了两种完全不同的概念。标识要求稳定可读,密钥要求随机保密,两者属性天然冲突。
解决建议:本地加密密钥应该使用 Android Keystore 生成的密钥,或者由服务端下发的密钥保护。SSAID 最多只能作为加密后的数据索引,不能参与密钥派生。这点务必记住,否则丢数据的责任谁都扛不住。
5. 设备标识选型:SSAID、OAID、GAID、IMEI、安装 ID 到底怎么选
5.1 一张表格看差异
| 标识 | 获取成本 | 跨应用一致性 | 可重置性 | 适用场景 | 主要限制 |
|---|---|---|---|---|---|
| SSAID(ANDROID_ID) | 无需权限 | Android 8 前一致,之后同签名一致 | 恢复出厂、刷机、备份恢复可变 | 本地缓存、安装去重、内部关联 | Android 8 后受签名影响 |
| OAID | 需要厂商 SDK/接口 | 各厂商策略不同,一般跨应用一致 | 可重置 | 广告归因、个性化推荐 | 依赖厂商支持,海外设备支持差 |
| GAID(Google Advertising ID) | 需要 Google Play 服务 | 跨应用一致 | 用户可重置 | 海外广告场景 | 国内设备基本不可用 |
| IMEI | 需要高级权限 | 跨应用一致 | 不可重置 | 运营商、特殊企业场景 | Android 10 后普通应用无法获取 |
| Installation ID(自建 UUID) | 完全自主 | 仅本应用内一致 | 卸载重装即变 | 用户级会话跟踪、本地缓存 | 无法跨设备跨应用 |
这张表的核心结论是:没有万能标识。你只能在“设备级”“安装级”“用户级”三个层级里选一个最贴近业务诉求的组合。
5.2 常见场景的选型建议
如果你是做数据统计 SDK,我推荐以自建的 Installation ID 为主键,SSAID 作为辅助字段。因为卸载重装后 Installation ID 虽然变了,但通过 SSAID 还能判断是否同一台设备,数据可以做串联。
如果你是做广告归因,国内建议接 OAID,海外建议接 GAID,SSAID 只能作为兜底。因为广告归因特别看重“跨应用一致性”,而 Android 8 之后的 SSAID 已经做不到这一点。
如果你是做风控和反作弊,不要只依赖任何单一标识。SSAID 可以做稳定因子,IMEI 拿不到就放弃,OAID 做补充,再叠加设备基础信息做综合判断。设备指纹模型虽然复杂,但比单一 ID 可靠得多。
如果你只是需要给某台机器上的某个安装实例生成一个唯一索引,比如本地数据库表主键、日志上报的 device_id,那 SSAID 完全够用。它比 UUID 多了一个优势:App 升级不会变。
5.3 后端生成的 Installation ID 到底香不香
自建 UUID 最大的好处是简单:客户端首次启动时生成一个 UUID,保存到 SharedPreferences 或 DataStore,同时上报服务端。后续所有关联都基于这个 UUID。
但它的致命弱点是:用户卸载重装、清除应用数据、或换一台新手机,UUID 就没了。所以它只能代表“安装实例”,不能代表“设备”或“用户”。
我见过很多团队一开始偷懒,只用 Installation ID,等业务需要跨安装识别设备时,才发现重建关联非常痛苦。因此我的建议是:从项目第一天就把 SSAID 作为辅助字段和数据一起上报,哪怕当时用不到,以后做数据清洗时你会感谢这个决定。
6. 关于 SSAID 的使用原则:隐私合规与工程安全边界
6.1 先回答三个问题:需要设备级、安装级还是用户级标识
每接一个新项目,我习惯先问三个问题:
业务需要识别“同一台设备”吗?如果是,SSAID 不够,需要设备指纹或 OAID 这类跨应用标识。业务需要识别“同一个安装”吗?如果是,SSAID 或自建 UUID 都行。业务需要识别“同一个用户”吗?如果是,老老实实做账号体系,别指望任何设备标识能替代登录态。
这三个问题想清楚,很多选型纠结会自动消失。SSAID 最大的误用场景,就是被拿来当月疯狂薅羊毛时的“唯一凭证”。
6.2 最小化收集和合理匿名化
不管从合规角度还是从工程维护角度,设备标识都属于“能不存就不存、能短存就短存”的数据。
我现在的落地做法是:
- 客户端只把 SSAID 原始值用于本地逻辑,比如缓存 key、加密索引。
- 上报服务端时,不传原始 SSAID,而是传一个用服务端盐做的 HMAC 哈希值。
- 原始 SSAID 如果需要排查问题,只在有明确需要时临时拉取,用完即删。
- 隐私政策里明确说明会收集设备匿名标识,并说明用途是数据统计和优化服务。
这套流程并不复杂,但能省掉不少后续风险。行业里因为设备标识不规范收集翻车的案例太多了,没必要为了省事把基础数据管控丢了。
6.3 别把标识当密钥,也别忘了它只是“权宜标识”
前面提到过用 SSAID 派生密钥导致数据全毁的案例。更稳妥的做法是:设备上的密钥只放 Android Keystore,服务端密钥只从服务端下发。SSAID 只作为数据索引,不承担任何安全职责。
同时,我认为它只是一个“过渡标识”。Android 系统在逐渐压缩普通应用的设备识别空间,未来的应用大概率要更依赖账号体系和第一方数据,而不是某个字符串。如果你的新项目,架构上还有余力,可以优先降低对 SSAID 的耦合度,而不是设计一套完全围绕它转的存储结构。我手上的处理方式,是把 SSAID 当作“增强型辅助字段”,主标识永远是一个后端生成且可迁移的内部 ID。这样就算某天某台设备取不到这个值、或者厂商又改了规则,核心链路也不会瘫痪。
这个思路,算是我在多次线上故障之后最想分享的一条经验。