为什么需要设备ID
“设备ID”即用于标识设备唯一身份的ID,即 Unique Device Identifier。基于以下原因,我们经常需要处理设备ID相关功能:
- 统计需求。DAU,MAU,转化率,用户行为等统计。
- 业务需求。个性化推荐,日志收集,灰度发布,AB Test等业务侧需求。
- 风控需求。防刷单,反作弊等。
设备ID的特征
为了满足以上需求,一个良好的设备ID方案应当具有“唯一性”和“稳定性”两个特征。
- 唯一性:系统中的任意两台设备,它们的设备ID应当不同。
- 稳定性:同一台设备在重启、清空应用数据、卸载应用重装、系统升级、Android 版本升级、刷机等情况下,设备ID应当保持不变。
遗憾的是,Android 平台并没有稳定的API可以提供具有上面两点特征的ID。
可选方案及限制
关于Android设备ID,常见的方案有IMEI、MAC地址、Serial、AndroidID等,下面逐一介绍它们是什么,以及为何无法承担唯一ID的职责。
IMEI
是 国际设备识别码(Imternational Mobile Equipment Identity) 的缩写,即通常所说的手机串号,用于在移动电话网络中识别每一部独立的手机等移动通信设备,共15~17位数字。在拨号键盘输入*#06#
即可查看。
1 | // 读取IMEI的样例代码,需要 READ_PHONE_STATE 权限 |
在早些时候,IMEI是很多应用采取的设备ID方案,因为它读取方便,且同时具备唯一性和稳定性的特征。然而自从Android 6开始,READ_PHONE_STATE
被列入dangerous
的保护级别,意味着我们不仅要在AndroidManifest.xml
文件里申请,还应当在应用到这个权限之前动态申请。尤其在中文的安卓系统上,弹窗里的文字提示是“申请电话设备信息”,很容易让人误以为这是要获取电话号码、短信内容等敏感信息。
如果说Android 6只是提高了使用IMEI作为设备ID的门槛,Android 10则是完全堵死了这条路。在Android 10的系统里,即使申请了READ_PHONE_STATE
权限,也无法获取IMEI,会抛出SecurityException
异常或者返回null。
MAC地址
MAC地址(Media Access Control Address),用于标识可上网设备的唯一地址,设备有几张网卡,就会有几个MAC地址。在OSI七层模型中,MAC地址位于第二层数据链路层。看到这里你也许以为MAC地址是作为设备ID解决方案的最佳选择,实则不然,首先,MAC地址的获取方法历经多次修改,足见Google正欲收紧MAC地址权限,以后完全堵死也并非不可能。
在 (?, 6.0),[6.0, 7.0),[7.0, ?) 三种不同的Android版本下,有着不同的获取MAC地址方式,可以参考简书这篇文章 《Android 版本兼容 — Android 6.0 和 7.0后获取Mac地址》。
1 | // 获取MAC地址,适用于目前Android全版本,不保证以后继续适用(很大可能不再适用) |
Serial
何为Serial?Serial即“设备序列号”,是设备厂商提供的设备唯一串号,唯一性由各厂商保证。拿vivo举例,它会保证自己生产的每一台设备序列号都是不同的,但是不是与OPPO的也不一样呢?这就无法保证了。一个方案是用厂商ID_设备型号_序列号
拼接起来,作为设备ID,这样可以避免不同厂商设备具有相同Serial的问题。但是,并非所有厂商都会严格按照这个规定来做。我曾经在RK3399的开发板上做过开发,试过很多张板子,它们的Serial都是0123456789,让人哭笑不得。
更糟糕的是,Android 10又堵死了获取Serial的路,会直接抛出SecurityException
,除非应用是系统签名且具备android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE
权限。
1 | // 读取Serial,需要 READ_PHONE_STATE 权限 |
AndroidID
AndroidID是SDK提供的获取ID方法,它不需要申明任何权限,具有64bit的取值范围,且唯一性也还不错。但是,它最大的硬伤在于无法满足稳定性:
- 刷机、root、恢复出厂设置后都会变化
- 对安装在8.0系统的应用来说,AndroidID取决于应用签名+设备两者的组合
- 在8.0之前安装的应用,如果在系统升级到8.0后,卸载重装该应用,读取到的AndroidID会变化
由于以上原因,在一些要求不严格的场景中,可以采用AndroidID作为设备ID,比如记录激活数、曝光数据等。但是在严格的场景中就不能用AndroidID了,如以设备ID标识用户身份,提供相应服务的场景。
读取AndroidID的样例代码如下:
1 | // 读取AndroidID,不需要额外申请任何权限 |
可选方案总结
用一张表格总结上述方案:
方案 | 概述 | 限制 |
---|---|---|
IMEI | 具备唯一性和稳定性,过去的最佳选择 | Android10堵死,无法获取 |
MAC | 网卡地址,唯一且稳定 | 权限逐渐收紧,未来极有可能关闭 |
Serial | 手机厂商提供的设备序列号 | 不保证唯一性,Android10堵死 |
AndroidID | Android SDK 提供,不需要申请权限,唯一性较好 | 不具备稳定性 |
设计一个新方案
上述四个方案里,没有哪一个是解决设备ID问题的终极武器(银弹),只有各种方法综合运用,才是解决之道。下面提出一种设备ID方案,综合使用多个硬件ID,借助服务器生成唯一虚拟设备ID(VDID),可以最大限度地保证唯一性和稳定性。
稳定性:拜占庭容错
稳定性要求当获取不到某种ID,或者某种ID发生变化时,系统能够辨识出这个设备。借助“拜占庭容错”可以解决稳定性的问题。
拜占庭容错机制源于古老 拜占庭将军(Byzantine failures) 问题。用简单的语言解释: 如果系统中有n个故障节点,系统要想正确运行,必须至少要有2n+1个正常节点。 。但对于Android设备ID,我们采用弱化的拜占庭容错机制,即客户端每次上传4个ID(IMEI、MAC、Serial、AndroidID),服务器根据这4个ID生成一个随机的唯一ID即VDID。后续客户端再请求时,可以使用VDID,或者再次使用4个ID,由服务器拿这4个ID在数据库中进行查找VDID,若找到则返回,若未找到,再使用4个ID中的3个进行查找,3个不行则用2个,以此类推。
获取VDID的时序图
唯一性:VDID的生成方式与取值范围
要实现VDID的唯一性,有两种方案可以考虑:
方案一:自增主键ID
自增、分步自增、分段构造、Redis分布式ID等方法,可以保证唯一性。但是在传输此类ID时,应当进行hash操作,且保证hash后的ID不可碰撞。
自增ID的优点是可作为索引,检索速度快;缺点是生成规则存在被破解的风险。
方案二:随机生成ID
这是另一种生成唯一ID的方法,当位数足够多时,可以认为碰撞概率趋近于0。首先看一下这张表,它描述了随机数位数与发生碰撞的概率。
假设我们应用的活跃用户数为20,000,000,即两千万。可以看到至少要有128Bits,才能在2*10^7(两千万)的数量级有极小的碰撞概率,符合我们的业务需求。
随机ID的优点是具有隐蔽性,缺点是检索效率一般。
小结
本文是笔者阅读郭霖公众号推文《漫谈设备唯一ID的那些秘密》后的总结归纳,此处向原作者@呼啸长风致谢。然而作者原文中有一处纰漏,设备序列号(SERIAL)在Android 10上基本是无法获取的,这也会影响原作者提出的解决方案。笔者已经在Github上对此提了issue。
出于对用户隐私的保护,Google一直试图收紧的设备ID的获取权限。而由于“账户”的概念在国内市场并不普遍,再加上各大OEM碎片化严重,国内的各种业务不得不依赖于设备ID进行展开。这也是开发者近年来不得不面对的问题,希望未来国内的Android生态可以更加统一,也希望Google也对此类需求提供更好的权限方案,能让开发者可以不必为此头痛。