- 一、设备ID的作用
- 二、获取设备ID的API
- 三、设备ID的特性分析
- 四、具体实现
设备唯一标识对于 app 开发是很重要的一个点,主要应用于统计,有时也应用于业务。 Android 平台提供了很多获取唯一标识的 API,但都不是很稳定。
一、设备ID的作用关于设备ID的作用,大概可以分为下面几点:
-
统计需求 统计需求是设备ID最常见的用途,包括DAU, MAU的统计,行为统计,广告激活的统计等。
-
业务需求 设备ID通常也用于业务中。 比如结合行为统计做用户画像,以为用户提供个性化的服务,大家感受比较明显的就是新闻类和电商类的APP了。 这类操作,有利有弊,仁者见仁智者见智。 又如,定向推送,不一定是广告推送,错误修复,内测推送等也会用到设备ID。 还有是一些和特定业务结合的用途,比如构造分布式ID等。
-
风控需求 设备ID还可用于防刷单,反作弊等。 当然,风控需求仅靠设备ID是无法完成的,通常需要建立一套反作弊系统。 关于这方面的内容,难以一言以蔽之,这里我们不多作展开。
IMEI
IMEI
本该最理想的设备 ID
,具备唯一性,恢复出厂设置不会变化(真正的设备相关)。
然而,获取IMEI
需要 READ_PHONE_STATE
权限,估计大家也知道这个权限有多麻烦了。
尤其是Android 6.0
以后, 这类权限要动态申请,很多用户可能会选择拒绝授权。
我们看到,有的APP
不授权这个权限就无法使用, 这可能会降低用户对APP
的好感度。
而且,Android 10.0 将彻底禁止第三方应用获取设备的IMEI,
即使申请了 READ_PHONE_STATE
权限。 所以,如果是新 APP,不建议用IMEI作为设备标识;
如果已经用 IMEI 作为标识,要赶紧做兼容工作了,尤其是做新设备标识和IMEI的映射。
MAC 地址
一般是指 wifi 模块的 mac 地址。 此处分析 wifi 模块:
- 优点: 1、硬件标识,刷机和恢复出厂设置不擦除; 2、大多android设备都有wifi模块。
- 缺点: 1、基于隐私考虑,官方不建议获取;6.0之后通过
WifiManager
获取不到真正的mac地址,7.0之后访问不了/sys/class/net/wlan0/address
; 2、不同的厂商有不同的限制,比如同样是7.0,一加3可以访问,小米6不可以访问。
如今,还是可以从NetworkInterface中获取到MAC的,但说不好后面也不可用了。
public static String getWifiMac() {
try {
Enumeration enumeration = NetworkInterface.getNetworkInterfaces();
if (enumeration == null) {
return "";
}
while (enumeration.hasMoreElements()) {
NetworkInterface netInterface = enumeration.nextElement();
if (netInterface.getName().equals("wlan0")) {
return formatMac(netInterface.getHardwareAddress());
}
}
} catch (Exception e) {
Log.e("tag", e.getMessage(), e);
}
return "";
}
设备序列号
通过android.os.Build.SERIAL
获得,由厂商提供。
如果厂商比较规范的话,设备序列号+Build.MANUFACTURER
应该能唯一标识设备。
但现实是并非所有厂商都按规范来,尤其是早期的设备。
最致命的是,Android 8.0 以上,android.os.Build.SERIAL
总返回 “unknown”;
若要获取序列号,可调用Build.getSerial()
,但是需要申请 READ_PHONE_STATE
权限。
到了Android 10.0以上,则和IMEI一样,也被禁止获取了。
总体来说,设备序列号有点鸡肋:食之无味,弃之可惜。
ANDROID_ID
Android ID 是获取门槛最低的,不需要任何权限,64bit 的取值范围,唯一性算是很好的了。 但是不足之处也很明显:
- 刷机、root、恢复出厂设置等会使得 Android ID 改变;
- Android 8.0 之后,Android ID 的规则发生了变化:
对于升级到8.0之前安装的应用,ANDROID_ID 会保持不变。如果卸载后重新安装的话,ANDROID_ID将会改变。
对于安装在8.0系统的应用来说,ANDROID_ID根据应用签名和用户的不同而不同。ANDROID_ID的唯一决定于应用签名、用户和设备三者的组合。
两个规则导致的结果就是: 第一,如果用户安装APP设备是8.0以下,后来卸载了,升级到8.0之后又重装了应用,Android ID不一样;
第二,不同签名的APP,获取到的Android ID不一样。
其中第二点可能对于广告联盟之类的有所影响。
三、设备ID的特性分析唯一性
唯一性: 两台不同的设备获取到的设备ID不相同。 分析唯一性,我们可以从ID的分配来入手:
1、按规则构造 比如自增ID(包括分步自增),分段构造的ID(如snowflake算法)等,此类ID能保证唯一性。 设备ID中的IMEI,设备序列号,MAC等,都是按照规则构造的,理论上能保证唯一性。 设备序列号是对厂商本身唯一,全局唯一需要在加上 Build.MANUFACTURER。 不过,设备序列号和MAC的唯一要打个问号,因为要看厂商是否遵守规则。 但随着手机产业的日渐成熟,传统意义上的山寨设备已越来越少,所以大多数情况下还是唯一的。
2、随机生成 比如UUID和Android ID,这类ID有一定的概率会重复,关键是看ID的长度(有多少bit)。
稳定性
稳定性:同一台设备在不同的时间, 获取到设备ID相同。
稳定性有两个层面: 1、ID的生命周期 IMEI,序列号,MAC等都是硬件相关,即使刷机也不会改变; Android ID则稳定性较弱,恢复出厂设置和刷机都会改变Android ID。
2、受版本的变化的影响 随着Android版本的提升,Google对权限是越收越紧了。 获取设备ID的API,要么收起不给用(IMEI), 要么获取变得困难(SERIAL ),要么不同签名的APP获取的值不一样(Android ID)。 同时,Android 10中存储权限也收缩了,之前的那种生成唯一ID写到SD卡的某个角落的,以求卸载重装后读之前的ID等方法也不奏效了。 加强隐私方面的权限,对用户而言是好事,但对开发者而言就比较难受了。 尤其是有的API本来可以用,升级后就获取不到了,这种断崖式的变化,可能会对数据统计造成影响。
四、具体实现private fun matchDeviceId(deviceIdList: List, r: DeviceId): DeviceId? {
if (deviceIdList.isEmpty()) {
return null
}
var maxPriorityDid : DeviceId? = null
var priority = 0
deviceIdList.forEach { did ->
val s = idMatch(did.serial_no, r.serial_no)
val a = idMatch(did.android_id, r.android_id)
val m = idMatch(did.mac, r.mac)
if (s && m && a) {
return did
}
if(priority == 3) return@forEach
if ((s && (a || m)) || (a && m)) {
priority = 3
maxPriorityDid = did
}
if(priority >= 2) return@forEach
val p = idMatch(did.physics_info, r.physics_info)
|| idMatch(did.dark_physics_info, r.dark_physics_info)
if (p && a) {
priority = 2
maxPriorityDid = did
}
if(priority >= 1) return@forEach
if (p && m) {
priority = 1
maxPriorityDid = did
}
}
return maxPriorityDid
}
解释说明:
- 如果设备序列号、Android ID、MAC全都不等,则前面的SQL查询不会返回记录(也就是没有匹配的设备)。
- 如果设备序列号,Android ID 和 MAC 全部相同,直接返回。
- 否则,遍历列表,取优先级最高的deviceId返回。
- 如果只有Android ID 或 MAC 之一相等,但是设备信息都匹配不上的话,也认为不是同一个设备。