漫谈唯一设备ID

一、前言

设备ID,简单来说就是一串符号(或者数字),映射现实中硬件设备。
如果这些符号和设备是一一对应的,可称之为“唯一设备ID(Unique Device Identifier)”

不幸的是,对于Android平台而言,没有稳定的API可以让开发者获取到这样的设备ID。
开发者通常会遇到这样的困境:
随着项目的演进, 越来越多的地方需要用到设备ID;
然而随着Android版本的升级,获取设备ID却越来越难了。
加上Android平台碎片化的问题,获取设备ID之路,可以说是步履维艰。

二、设备ID的作用

关于设备ID的作用,大概可以分为下面几点:

  • 统计需求
    统计需求是设备ID最常见的用途,包括DAU, MAU的统计,行为统计,广告激活的统计等。

  • 业务需求
    设备ID通常也用于业务中。
    比如结合行为统计做用户画像,以为用户提供个性化的服务,大家感受比较明显的就是新闻类和电商类的APP了。
    这类操作,有利有弊,仁者见仁智者见智。
    又如,定向推送,不一定是广告推送,错误修复,内测推送等也会用到设备ID。
    还有是一些和特定业务结合的用途,比如构造分布式ID等。

  • 风控需求
    设备ID还可用于防刷单,反作弊等。
    当然,风控需求仅靠设备ID是无法完成的,通常需要建立一套反作弊系统。
    关于这方面的内容,难以一言以蔽之,这里我们不多作展开。

三、获取设备ID的API

获取设备标识的API屈指可数,而且都或多或少有一些问题。
常规的API有以下这些:

IMEI

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一样,也被禁止获取了。
总体来说,设备序列号有点鸡肋:食之无味,弃之可惜。

MAC地址

获取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

Android ID 是获取门槛最低的,不需要任何权限,64bit 的取值范围,唯一性算是很好的了。
但是不足之处也很明显:
1、刷机、root、恢复出厂设置等会使得 Android ID 改变;
2、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不一样。
其中第二点可能对于广告联盟之类的有所影响。

Widevine ID

Widevine ID 是用于数字版权管理(DRM)的 Widevine 模块为每个设备分配的唯一标识符。
Widevine ID 也是不需要权限的。
可通过如下方法获取:

 public static String getWidevineIdHash() {
        UUID WIDEVINE_UUID = new UUID(0xEDEF8BA979D64ACEL, 0xA3C827DCD51D21EDL);
        MediaDrm mediaDrm = null;
        try {
            mediaDrm = new MediaDrm(WIDEVINE_UUID);
            byte[] widevineId = mediaDrm.getPropertyByteArray(MediaDrm.PROPERTY_DEVICE_UNIQUE_ID);
            return byteToHex(widevineId);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if (mediaDrm != null) {
                mediaDrm.release();
            }
        }
        return "";
    }

四、 设备ID的特性分析

笔者之前写过一篇文章《Android设备唯一标识的获取和构造》, 文中提到设备ID的两个概念:唯一性和稳定性。
唯一性:两台不同的设备获取到的设备ID不相同;
稳定性:同一台设备在不同的时间, 获取到设备ID相同。

分析唯一性,我们可以从ID的分配来入手:

  • 1、按规则构造
    比如自增ID(包括分步自增),分段构造的ID(如snowflake算法)等,此类ID能保证唯一性。
    设备ID中的IMEI,设备序列号,MAC等,都是按照规则构造的,理论上能保证唯一性。
    设备序列号是对厂商本身唯一,全局唯一需要在加上 Build.MANUFACTURER
    不过,设备序列号和MAC的唯一要打个问号,因为要看厂商是否遵守规则。
    但随着手机产业的日渐成熟,传统意义上的山寨设备已越来越少,所以大多数情况下还是唯一的。

  • 2、随机生成
    比如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次方,微乎其微。

稳定性有两个层面:

  • 1、ID的生命周期
    IMEI,序列号,MAC等都是硬件相关,即使刷机也不会改变;
    Android ID则稳定性较弱,恢复出厂设置和刷机都会改变Android ID。
  • 2、受版本的变化的影响
    随着Android版本的提升,Google对权限是越收越紧了。
    获取设备ID的API,要么收起不给用(IMEI), 要么获取变得困难(SERIAL ),要么不同签名的APP获取的值不一样(Android ID)。
    同时,Android 10中存储权限也收缩了,之前的那种生成唯一ID写到SD卡的某个角落的,以求卸载重装后读之前的ID等方法也不奏效了。
    加强隐私方面的权限,对用户而言是好事,但对开发者而言就比较难受了。
    尤其是有的API本来可以用,升级后就获取不到了,这种断崖式的变化,可能会对数据统计造成影响。

五、设备ID的构造

无论是统计需求,还是业务需求,都要求设备ID是唯一的,稳定的。
如果设备ID有重复,则活跃统计,用户画像,定向推送等统统都不准确了;
其中,影响最深是定向推送,送错快递还有可能追回,推送错了就不好说了,如果推送内容又比较重要,后果不堪设想。
如果设备ID不稳定(ID变化),会影响到活跃统计(会认为是新用户),对用户画像也有较大影响(之前的ID关联的行为数据无法跟踪了)。

为此,有必要设计一套方案,提供相对定稳定的,唯一的设备ID。
首先要明确两个前提条件:
前面分析的设备ID中,在可用的前提下,出现重复的概率较小;
如果一定的频率去观察,比如说每天,总体而言,观察到和昨天不一样的概率也是较小的。
如何在本来就较小的概率的前提下,继续降低概率呢?

5.1 方案分析

一种方案是组合设备ID(直接拼接,或者拼接后计算摘要)。
举个例子,假如出现重复的概率和发生变化的的概率都是千分之一,
则对于两台不同设备,两个设备ID同时重复的概率是百万分之一,两个设备ID至少有一个发生变化约为千分之二。
也就是,拼接ID的效果是大大提高唯一性,但是一定程度上降低稳定性(只要其中一个要素变化,拼接的ID就变了)。
但事实上,如今能拿到的设备ID,最突出的矛盾是不稳定,所以,我们不能为了提高唯一性而牺牲稳定性。

要提高稳定性,可以引入容错方案。
容错方案有很多,比如网络传输,用checksum去校验报文,如果出错了则重发;
再如磁盘阵列,数据写入两个磁盘,只有当两个磁盘同时出错时才会丢失数据,从而大大降低丢失数据的概率。
但是对于设备ID,以上两种方案都不合适,因为上面的方案需要通过checksum来确认原信息是否被修改,设备ID没有这样的条件。

所以,可以引入类似虚拟货币用到的"拜占庭容错"方案。
简单地说,就是要采集三个设备ID到云端,如果有两个(包括两个以上)的设备ID和之前的记录相同,则认为是同一台设备。
同样假设出现重复的概率和发生变化的的概率都是千分之一,则:
同一台设备的两次采集,认不出是同一台设备的条件为“至少两个设备ID都和上次不一样”,概率约为百万分之三。
两台不同的设备,认为是同一台的条件是为“三个设备ID中,至少有两个设备ID和另一台设备相同”,概率同样约为百万分之三。
所以,用此方案,唯一性和稳定性都能得到提高。

5.2 具体实现

基本思想是:服务端有一张设备 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。
有两种思路:

  • 随机化,比如用randomUUID
    这种方案优点是具有隐蔽性,好处是UUID完全不可能得知主键ID,
    缺点是占空间,检索效率一般,输入不友好(很多时候我们需要输入设备ID去查询一些数据)。
  • 根据主键 id 加密(混淆)出另一个Long类型的id。
    此方案优点是节省空间,检索快,但是要求和主键ID一一映射,以确保不会重复。
    具体方法可参考《如何加密Long类型数值》

然后就是,需要三个设备ID……

  • Android ID 和 MAC地址都还可以取到,再加一个,实施方案的条件就凑齐了。
  • IMEI 需要 READ_PHONE_STATE 权限,所以如果不想申请READ_PHONE_STATE 权限,可以不采集IMEI了;
    而且,即使申请了 READ_PHONE_STATE,Android 10.0以后也获取不到了。
  • 但是设备序列号只有在Android 8.0之前才可以免权限获取;
    在8.0之后,10.0之前,需READ_PHONE_STATE 权限;
    10.0之后, 有READ_PHONE_STATE权限也获取不到了。

那么,如果在没有 READ_PHONE_STATE 权限的情况下,以及Android 10.0之后,如何处理?
首先,设备序列号还是要采集的,毕竟还有部分旧版本的设备可以获取到,能区分一点是一点;
然后,采集一些设备相关的信息,机型,硬件信息等(相同的机型,可能有多种配置,所以同时也采集一下硬件信息)。
最终匹配规则如下:

  // 相等且不为零方为匹配
    private fun idMatch(a: Long, b: Long): Boolean {
        return a != 0L && a == b
    }

    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 a = idMatch(did.androidId, r.androidId)
            val w = idMatch(did.widevineId, r.widevineId)
            val d = idMatch(did.deviceHash, r.deviceHash)
            if (w && d && a) {
                return did
            }
            if (priority < 2 && (a && w)) {
                priority = 2
                maxPriorityDid = did
            }
            if (priority < 1 && ((a && d) || (w && d))) {
                priority = 1
                maxPriorityDid = did
            }
        }
        return maxPriorityDid
    }

  • 如果Android ID、Widevine ID 和 deviceHash 全都不等,则前面的SQL查询不会返回记录(也就是没有匹配的设备)。
  • 如果Android ID、 Widevine ID 和 deviceHash 全部相同,直接返回。
  • 否则,遍历列表,取优先级最高的deviceId返回。

如果没有匹配的设备,则认为是新设备;
此时,生成新的udid返回,同时插入新设备的相关信息(设备ID,硬件信息)。

关于硬件信息,需满足一个要求:在设备重启、恢复出厂设置等操作之后,不会变化。
常规的信息包括CPU,内存,屏幕,传感器,网关等。
为了方便检索,我们可以用MurmurHash将信息压缩到64bit(Long的长度)。

再者,在获取到udid之后,可以定时(比如每隔两天)就上传udid和设备信息给云端,云端比较一下存储的信息和上传的信息,不相同则更新,这样可以提高udid的稳定性。
比方说,用户在设备是Android 7.0 的时候卸载了APP,在Android 8.0之后安装回来,这时候 Android ID 是变化了的,但是凭着Widevine IDdeviceHash 我们可以认出这台设备,同时更新其 Android ID;
如果哪一天轮到 Widevine ID 获取不到了,这时候我们仍可以根据 Android IDdeviceHash 和设备信息识别出这台设备

六、离线设备ID

如果觉得在线构造设备ID麻烦,同时APP用户规模不大,对设备ID的稳定性要求没那么高,那就是考虑离线设备ID了。
比如可以拼接 Android ID, Widevine ID 和 设备信息,并计算其摘要。
至于有什么设备信息可以用来标识设备,文末的地址有一些参考代码,同时也欢迎大家补充,相互学习。

七、总结

本文介绍了设备ID的用途,现状,并分析了现有设备ID的特性,最后提出了一套设备ID的构造方案。
按照这几年的趋势,各种设备ID的API或许还会越收越紧,仅从客户端去构造可靠的设备ID是比较困难的,而基于信息采集和云端综合计算则相对容易。
具体实现,笔者编写了一个Demo,已发布的到github,谨供参考。
项目地址:https://github.com/BillyWei001/Udid

参考资料:
https://www.jianshu.com/p/9d828d259270
https://www.jianshu.com/p/ad9756fe21c8
https://blog.csdn.net/andoop/article/details/54633077
https://blog.csdn.net/renlonggg/article/details/78435986
https://developer.android.com/about/versions/oreo/android-8.0-changes?hl=zh-cn
https://en.wikipedia.org/wiki/Birthday_attack

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 161,780评论 4 369
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 68,424评论 1 305
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 111,397评论 0 254
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,576评论 0 218
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,997评论 3 295
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,945评论 1 224
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 32,107评论 2 317
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,850评论 0 208
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,625评论 1 250
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,804评论 2 253
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,285评论 1 265
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,613评论 3 261
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,291评论 3 242
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,164评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,963评论 0 201
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 36,096评论 2 285
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,886评论 2 278

推荐阅读更多精彩内容