设备ID,简单来讲就是一串符号(或者数字),映射现实中一台设备。
若是这些符号和现实中的设备是一一对应的,可称之为“惟一设备ID(Unique Device Identifier)”。
html
不幸的是,对于Android平台而言,没有稳定的API可让开发者获取到这样的设备ID。
开发者一般会遇到这样的困境:
随着项目的演进, 愈来愈多的地方须要用到设备ID;
然而随着Android版本的升级,获取设备ID却愈来愈难了。
加上Android平台碎片化的问题,获取设备ID之路,能够说是寸步难行。java
关于设备ID的做用,大概能够分为下面几点:
android
统计需求
统计需求是设备ID最多见的用途,包括DAU, MAU的统计,行为统计,广告激活的统计等。git
业务需求
设备ID一般也用于业务中。 好比结合行为统计作用户画像,觉得用户提供个性化的服务,你们感觉比较明显的就是新闻类和电商类的APP了。
这类操做,有利有弊,仁者见仁智者见智,咱们这里就很少作讨论了。
又如,定向推送,不必定是广告推送,错误修复,内测推送等也会用到设备ID。
还有是一些和特定业务结合的用途,好比构造分布式ID等,就不一一列举了。github
风控需求
设备ID还可用于防刷单,反做弊等。
固然,风控需求单靠设备ID是没法完成的,一般须要创建一套反做弊系统。
关于这方面的内容,难以一言以蔽之,这里咱们很少做展开。算法
获取设备标识的API屈指可数,并且都或多或少有一些问题。
常规的API有如下这些:sql
IMEI本该最理想的设备ID,具有惟一性,恢复出厂设置不会变化(真正的设备相关)。
然而,获取IMEI须要 READ_PHONE_STATE 权限,估计你们也知道这个权限有多麻烦了。
尤为是Android 6.0之后, 这类权限要动态申请,不少用户可能会选择拒绝受权。
咱们看到,有的APP不受权这个权限就没法使用, 这可能会下降用户对APP的好感度。
并且,Android 10.0 将完全禁止第三方应用获取设备的IMEI, 即便申请了 READ_PHONE_STATE 权限。
因此,若是是新APP,不建议用IMEI做为设备标识;
若是已经用IMEI做为标识,要赶忙作兼容工做了,尤为是作新设备标识和IMEI的映射。网络
经过android.os.Build.SERIAL得到,由厂商提供。
若是厂商比较规范的话,设备序列号+Build.MANUFACTURER应该能惟一标识设备。
但现实是并不是全部厂商都按规范来,尤为是早期的设备。
最致命的是,Android 8.0 以上,android.os.Build.SERIAL 总返回 “unknown”;
若要获取序列号,可调用Build.getSerial() ,可是须要申请 READ_PHONE_STATE 权限。
到了Android 10.0以上,则和IMEI同样,也被禁止获取了。 整体来讲,设备序列号有点鸡肋:食之无味,弃之惋惜。dom
获取MAC地址也是愈来愈困难了,
Android 6.0之后经过 WifiManager 获取到的mac将是固定的:02:00:00:00:00:00, 再后来连读取 /sys/class/net/wlan0/address 也获取不到了。
现在只剩下面这种方法能够获取(没有开启wifi也能够获取到):分布式
public static String getWifiMac() {
try {
Enumeration<NetworkInterface> 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 ID 是获取门槛最低的,不须要任何权限,64bit 的取值范围,惟一性算是很好的了。
可是不足之处也很明显:
一、刷机、root、恢复出厂设置等会使得 Android ID 改变;
二、Android 8.0以后,Android ID的规则发生了变化:
ANDROID_ID
会保持不变。若是卸载后从新安装的话,ANDROID_ID
将会改变。ANDROID_ID
根据应用签名和用户的不一样而不一样。ANDROID_ID
的惟一决定于应用签名、用户和设备三者的组合。两个规则致使的结果就是:
第一,若是用户安装APP设备是8.0如下,后来卸载了,升级到8.0以后又重装了应用,Android ID不同;
第二,不一样签名的APP,获取到的Android ID不同。
其中第二点可能对于广告联盟之类的有所影响(若是彼此是用Android ID对比数据的话),因此Google文档中说“请使用Advertising ID
”,
不过你们都知道,Google的服务在国内用不了。
对Android ID作了约束,对隐私保护起到必定做用,而且用来作APP本身的活跃统计也仍是没有问题的。
笔者以前写过一篇文章《Android设备惟一标识的获取和构造》, 文中提到设备ID的两个概念:惟一性和稳定性。
惟一性:两台不一样的设备获取到相同的设备ID不相同;
稳定性:同一台设备在不一样的时间, 获取到设备ID相同。
分析惟一性,咱们能够从ID的分配来入手:
一、按规则构造
好比自增ID(包括分步自增),分段构造的ID(如snowflake算法)等,此类ID能保证惟一性。
设备ID中的IMEI,设备序列号,MAC等,都是按照规则构造的,理论上能保证惟一性。
设备序列号是对厂商自己惟一,全局惟一须要在加上 Build.MANUFACTURER。
不过,设备序列号和MAC的惟一要打个问号,由于要看厂商是否遵照规则。
但随着手机产业的日渐成熟,传统意义上的山寨设备已愈来愈少,因此大多数状况下仍是惟一的。
二、随机生成
好比UUID和Android ID,这类ID有必定的几率会重复,关键是看ID的长度(有多少bit)。
有人作了这样一张随机数的冲突几率表:
左边第一栏是bit数量,第二栏是对应的取值范围,再后面是元素个数以及对应的冲突几率。
例如,假若有50000个32bit的随机数,则这些随机数中,至少有两个相同数字的几率为25%;
换一种说法,就是假若有四组数,每组都有50000个随机数,则大约其中一组会有重复的数字。
32bit有43亿的取值范围,怎么50000个随机数就有这么高的出现重复的几率?
若是对此感到困惑,能够了解一下生日悖论。
Android ID是长度为16的十六进制字符串,其实就是64bit,咱们来分析一下其重复的几率:
假如APP累计激活量达到50亿的APP,则每两个这样的APP就大约有一个会有重复的Android ID。
不过这里的“重复”不是大量的重复,而是“至少有两个相同”,也就是,若是设备激活量有50亿(不少APP达不到-_-),那么有可能会有少许的重复的Android ID。
整体而言, Android ID的惟一性仍是不错的。
JDK的randomUUID,大体能够认为是128bit的随机数(其中有6bit是固定的),即便到达200亿的数量,有重复的几率也仅仅是10的负18次方,微乎其微。
稳定性有两个层面:
不管是统计需求,仍是业务需求,都要求设备ID是惟一的,稳定的。
若是设备ID有重复,则活跃统计,用户画像,定向推送等通通都不许确了;
其中,影响最深是定向推送,送错快递还有可能追回,推送错了就很差说了,若是推送内容又比较重要,后果不堪设想。
若是设备ID不稳定(ID变化),会影响到活跃统计(会认为是新用户),对用户画像也有较大影响(以前的ID关联的行为数据没法跟踪了)。
为此,有必要设计一套方案,提供相对定稳定的,惟一的设备ID。
首先要明确两个前提条件:
前面分析的设备ID中,在可用的前提下,出现重复的几率较小;
若是必定的频率去观察,好比说天天,整体而言,观察到和昨天不同的几率也是较小的。
如何在原本就较小的几率的前提下,继续下降几率呢?
一种方案是组合设备ID(直接拼接,或者拼接后计算摘要)。
举个例子,假如出现重复的几率和发生变化的的几率都是千分之一,
则对于两台不一样设备,两个设备ID同时重复的几率是百万分之一,两个设备ID至少有一个发生变化约为千分之二。
也就是,拼接ID的效果是大大提升惟一性,可是必定程度上下降稳定性(只要其中一个要素变化,拼接的ID就变了)。
但事实上,现在能拿到的设备ID,最突出的矛盾是不稳定,因此,咱们不能为了提升惟一性而牺牲稳定性。
要提升稳定性,能够引入容错方案。
容错方案有不少,好比网络传输,用checksum去校验报文,若是出错了则重发;
再如磁盘阵列,数据写入两个磁盘,只有当两个磁盘同时出错时才会丢失数据,从而大大下降丢失数据的几率。
可是对于设备ID,以上两种方案都不合适,由于上面的方案须要经过checksum来确认原信息是否被修改,设备ID没有这样的条件。
因此,能够引入相似虚拟货币用到的"拜占庭容错"方案。
简单地说,就是要采集三个设备ID到云端,若是有两个(包括两个以上)的设备ID和以前的记录相同,则认为是同一台设备。
一样假设出现重复的几率和发生变化的的几率都是千分之一,则:
同一台设备的两次采集,认不出是同一台设备的条件为“至少两个设备ID都和上次不同”,几率约为百万分之三。
两台不一样的设备,认为是同一台的条件是为“三个设备ID中,至少有两个设备ID和另外一台设备相同”,几率一样约为百万分之三。
因此,用此方案,惟一性和稳定性都获得了提升。
基本思想是:服务端有一张设备 ID 的表,核心的属性(Column)有:
id | did_1 | did_2 | did_3
客户请求时,上传三个设备 ID,服务端检索:
SELECT * from t_device_id WHERE did_1=? or did_2=? or did_3=?
复制代码
若是检索到记录,其中至少两个did和上传的相同,则返回 id;
不然,插入上传的三个设备 ID,并将新插入记录的 id 返回。
一般状况下,服务端表的主键为自增序列(为了确保插入的有序性),
因此咱们不能直接返回表的主键,不然容易被他人推测其余的设备 ID,以及知晓用户数量。
所以,在主键 ID 以外,咱们须要另一个惟一 ID。
有两种思路:
而后就是,须要三个设备ID……
那么,若是在没有 READ_PHONE_STATE 权限的状况下,以及Android 10.0以后,如何处理?
首先,设备序列号仍是要采集的,毕竟还有部分旧版本的设备能够获取到,能区分一点是一点;
而后,采集一些设备相关的信息,机型,硬件信息等(相同的机型,可能有多种配置,因此同时也采集一下硬件信息)。
最终匹配规则以下:
private fun matchDeviceId(deviceIdList: List<DeviceId>, 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
}
复制代码
若是没有匹配的设备,则认为是新设备;
此时,生成新的udid返回,同时插入新设备的相关信息(设备ID,硬件信息)。
关于硬件信息,需知足一个要求:在设备重启、恢复出厂设置等操做以后,不会变化。
常规信息有CPU核心数,RAM/ROM大小(以Gb为单位采集,而不是精确到比特,不然容易变化),屏幕分辨率和dpi等,结合机型,保守估计有上千甚至上万种可能性,相对Android ID 的 2^64 固然相差很远了,可是仍可做为辅助的参考信息。
试想在设备序列号获取不到,Android ID 和 MAC 地址其中一个发生变化时,检索到的都是只有Android ID 或者 MAC 其中一个匹配的记录,茫茫机海,说不许就有一两台的Android ID 或 MAC是相同的。
这时候选哪个呢? 再加上设备信息,或许就区分开了。
常规的设备信息容易遭到篡改,因此,在常规信息以外,咱们能够挖掘一些冷门的设备特征,好比 NetworkInterface 和 传感器 的相关信息。
当常规信息被篡改时,若是冷门的设备信息还没变,仍可识别出是同一台设备。
至于如何挖掘,那就各显神通了,一般作手机硬件或者ROM的朋友可能会知道更多的API。
为了方便检索,咱们能够用MurmurHash将信息压缩到64bit(Long的长度)。
再者,在获取到udid以后,能够定时(好比每隔两天)就上传udid和设备信息给云端,云端比较一下存储的信息和上传的信息,不相同则更新,这样能够提升udid的稳定性。
比方说,用户在设备是Android 7.0 的时候卸载了APP,在Android 8.0以后安装回来,这时候Android ID 是变化了的,可是凭着MAC和设备信息咱们能够认出这台设备,同时更新其 Android ID;
若是哪一天轮到MAC获取不到了,这时候咱们仍能够根据 Android ID和设备信息识别出这台设备。
本文介绍了设备ID的用途,现状,并分析了现有设备ID的特性,最后提出了一种设备ID的构造方案。
按照这几年的趋势,各类设备ID的API或许还会越收越紧,单从客户端去构造可靠的设备ID 是比较困难的,而基于信息采集和云端综合计算则相对容易。
具体实现,笔者编写了一个Demo,已发布的到github,谨供参考。
项目地址:github.com/No89757/Udi…
参考资料:
www.jianshu.com/p/9d828d259…
www.jianshu.com/p/ad9756fe2…
blog.csdn.net/andoop/arti…
blog.csdn.net/renlonggg/a…
developer.android.com/about/versi…
en.wikipedia.org/wiki/Birthd…