1. Android设备标识的演进背景
十年前我做Android开发时,获取设备ID就像喝水一样简单。一个TelephonyManager.getDeviceId()调用,IMEI码手到擒来。但如今在Android 10+的设备上运行这段代码,只会收到一串刺眼的"unknown"。这不是代码写错了,而是移动生态正在经历一场隐私革命。
传统IMEI码就像设备的身份证号,具有终身唯一性。但这也意味着用户无法重置它,所有应用都能用它追踪用户行为。2018年欧盟GDPR实施后,Google开始收紧设备信息获取权限。Android 10砍掉了IMEI/MEID的普通应用访问权限,Android 11进一步限制了MAC地址获取。这给开发者带来的直接困扰是:用户换设备后无法识别老用户,广告归因失去依据,风控系统难以识别设备伪造。
国内厂商的应对方案是OAID(开放匿名设备标识符)。这个由中国信通院牵头、移动安全联盟(MSA)制定的标准,本质上是个可重置的临时身份证。用户恢复出厂设置时会生成新OAID,且不同应用获取的OAID不同。我在小米和华为设备上实测发现,OAID的格式类似"a1b2c3d4-5678-90ef-ghij-klmnopqrstuv",不同厂商的实现略有差异。
2. 传统标识方案的局限性
2.1 IMEI/MEID的没落
IMEI的获取曾经简单到令人发指:
TelephonyManager tm = (TelephonyManager)getSystemService(TELEPHONY_SERVICE); String imei = tm.getDeviceId();但在Android 6.0之后需要动态申请READ_PHONE_STATE权限,到Android 10+直接返回空值。更麻烦的是双卡设备的处理逻辑:早期代码往往只取卡槽1的IMEI,但用户可能使用卡槽2上网。我在项目中就遇到过因IMEI获取不全导致30%的设备识别失败。
2.2 Android ID的陷阱
很多人转向使用Settings.Secure.ANDROID_ID,这个64位编码看起来很美:
val androidId = Settings.Secure.getString( contentResolver, Settings.Secure.ANDROID_ID )但我在三星S8上发现一个致命问题:不同应用获取的ANDROID_ID不同。这是因为Android 8.0引入了SSAID(作用域存储Android ID),根据应用签名生成不同ID。更糟的是,某些厂商设备会返回全零的ANDROID_ID,比如著名的"9774d56d682e549c"。
2.3 MAC地址的随机化
过去我们常用WiFi MAC地址作为备用方案:
WifiManager wifi = (WifiManager)getSystemService(WIFI_SERVICE); WifiInfo info = wifi.getConnectionInfo(); String mac = info.getMacAddress();但从Android 6.0开始,非系统应用获取的MAC地址固定为"02:00:00:00:00:00"。Android 10更是默认开启MAC地址随机化,用户每次连接新WiFi都会变化。我在测试华为Mate40时发现,开启"随机MAC"功能后,同一WiFi下每次获取的MAC都不同。
3. OAID技术深度解析
3.1 MSA联盟与技术规范
移动安全联盟(MSA)的OAID规范实际上包含四层标识体系:
- 设备唯一标识符(DUID):硬件级标识,厂商掌握
- 匿名设备标识符(OAID):系统级临时ID
- 开发者匿名设备标识符(VAID):开发者维度ID
- 应用匿名设备标识符(AAID):应用级ID
国内主流厂商的适配情况:
| 厂商 | 最低支持版本 | 获取方式 |
|---|---|---|
| 华为 | EMUI 10+ | 华为移动服务(HMS) |
| 小米 | MIUI 12+ | MSA SDK |
| OPPO | ColorOS 7+ | 系统内置API |
| vivo | Funtouch 10+ | 系统APK通信 |
3.2 OAID获取实战
对于无法使用官方MSA SDK的个人开发者,推荐使用开源实现Android_CN_OAID。以下是集成示例:
// build.gradle dependencies { implementation 'com.github.gzu-liyujiang:Android_CN_OAID:4.2.8' runtimeOnly "com.huawei.hms:ads-identifier:3.4.62.300" }代码调用分为同步和异步两种方式:
// 同步获取(可能阻塞主线程) String oaid = DeviceIdentifier.getOAID(context); // 异步获取(推荐) DeviceIdentifier.getOAID(context, new IGetter() { @Override public void onOAIDGetComplete(String result) { // 处理OAID } @Override public void onOAIDGetError(Exception error) { // 错误处理 } });注意:OAID可能为空!我在Redmi Note 9上实测发现,关闭"广告跟踪"功能后OAID返回空字符串。此时需要降级到其他标识方案。
4. 分层降级策略实战
4.1 设备指纹算法优化
基于原始文章的算法思路,我优化后的设备指纹生成流程:
第一级标识(高优先级):
- OAID(Android 10+)
- IMEI/MEID(Android 6-9)
- 双卡设备处理:优先取MEID,其次卡槽1 IMEI
第二级标识:
String serial = Build.getSerial(); // 需要READ_PHONE_STATE权限 String manufacturer = Build.MANUFACTURER; String mac = getMacAddress(); // 通过NetworkInterface获取第三级硬件指纹:
# 伪代码示例 hardware_fingerprint = md5( Build.BOARD + "/" + Build.BRAND + "/" + Build.DEVICE + "/" + Build.HARDWARE + "/" + Build.MODEL + "/" + Build.PRODUCT )
4.2 缓存策略设计
为避免重复计算,应采用三级缓存:
- 内存缓存:使用
WeakReference存储,随进程消亡 - 内部存储:
SharedPreferences加密存储 - 外部存储:隐藏文件存储(注意Android 11分区存储限制)
示例代码:
fun getDeviceId(context: Context): String { return memoryCache.getOrElse("device_id") { spCache.getOrElse("device_id") { fileCache.getOrElse("device_id") { generateNewDeviceId(context).also { fileCache.put("device_id", it) } } } } }4.3 厂商兼容性处理
各厂商的魔改系统需要特殊处理:
华为设备:
// 需要添加HMS依赖 if (Build.MANUFACTURER.equalsIgnoreCase("huawei")) { AdsIdentifierService service = new AdsIdentifierService(context); String hwOaid = service.getOAID(); }小米设备:
// 需要初始化MSA SDK Supplier supplier = new MsaSupplier(context); supplier.getOAID(object : IIdentifierListener { @Override public void OnSupport(boolean isSupport, IdSupplier supplier) { if (isSupport) { String msaOaid = supplier.getOAID(); } } });5. 隐私合规要点
5.1 权限声明优化
在AndroidManifest.xml中按需声明权限:
<!-- 仅当需要IMEI时添加 --> <uses-permission android:name="android.permission.READ_PHONE_STATE" android:maxSdkVersion="28" /> <!-- Android 9及以下 --> <!-- OAID不需要权限 -->5.2 用户授权流程
正确的权限请求时序:
- 展示隐私政策弹窗
- 用户同意后调用
DeviceIdentifier.register(context) - 在用户主动触发时获取设备ID
- 提供重置ID的入口(符合GDPR被遗忘权)
5.3 海外版本适配
对于Google Play版本,应使用Advertising ID:
AdvertisingIdClient.Info adInfo = AdvertisingIdClient.getAdvertisingIdInfo(context); String adId = adInfo.getId();注意处理用户启用"限制广告跟踪"的情况:
if (adInfo.isLimitAdTrackingEnabled()) { // 使用其他替代方案 }6. 终极解决方案
结合多年踩坑经验,我总结的完整解决方案如下:
优先级获取:
graph TD A[尝试获取OAID] -->|成功| B[返回OAID] A -->|失败| C[尝试获取IMEI] C -->|成功| D[返回IMEI] C -->|失败| E[生成复合指纹]复合指纹生成算法:
def generate_fallback_id(): hardware_hash = md5( f"{Build.BOARD}/{Build.BRAND}/" f"{Build.DEVICE}/{Build.HARDWARE}/" f"{screen_width}x{screen_height}" ) android_id = Settings.Secure.ANDROID_ID return md5(f"{hardware_hash}:{android_id}")持久化策略:
- 使用
AndroidKeyStore加密存储 - 备份到云盘时关联用户账号
- 定期校验ID有效性(如发现冲突则重新生成)
- 使用
在最近为某金融APP实施的方案中,这套策略使设备识别准确率从Android 10的62%提升到98%,同时通过了第三方隐私审计。关键点在于:没有银弹,必须根据业务场景组合多种技术手段。