android设备获取唯一性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不一样。

其中第二点可能对于广告联盟之类的有所影响(如果彼此是用Android

ID对比数据的话),所以Google文档中说“请使用Advertising ID”,

不过大家都知道,Google的服务在国内用不了。

 

对Android ID做了约束,对隐私保护起到一定作用,并且用来做APP自己的活跃统计也还是没有问题的。

 

DEVICE_ID

 

这是Android系统为开发者提供的用于标识手机设备的串号,也是各种方法中普适性较高的,可以说几乎所有的设备都可以返回这个串号,并且唯一性良好。

 

这个DEVICE_ID可以同通过下面的方法获取:

 

TelephonyManager tm = (TelephonyManager)getSystemService(Context.TELEPHONY_SERVICE); String DEVICE_ID = tm.getDeviceId();

 

它会根据不同的手机设备返回IMEI,MEID或者ESN码,但在使用的过程中有以下问题:

 

  • 非手机设备:最开始搭载Android系统都手机设备,而现在也出现了非手机设备:如平板电脑、电子书、电视、音乐播放器等。这些设备没有通话的硬件功能,系统中也就没有TELEPHONY_SERVICE,自然也就无法通过上面的方法获得DEVICE_ID。
  • 权限问题:获取DEVICE_ID需要READ_PHONE_STATE权限,如果只是为了获取DEVICE_ID而没有用到其他的通话功能,申请这个权限一来大才小用,二来部分用户会怀疑软件的安全性。
  • 厂商定制系统中的Bug:少数手机设备上,由于该实现有漏洞,会返回垃圾,如:zeros或者asterisks

 

先说结论】

UDID是移动端相对最靠谱的设备标识码,不论Android或iOS。

 

【名词释义】

 

  • Device ID:设备ID。
  • IMEI:International Mobile Equipment Identity,国际移动设备身份码的缩写。是由15位数字组成的“电子串号”,它与每台手机一一对应,每个IMEI在世界上都是唯一的。
  • IDFA:Identifier For Advertising,iOS独有的广告标识符。
  • UDID:Unique Device Identifier,唯一设备标识码。
  • UUID:Universally Unique Identifier,通用唯一识别码。目前最广泛应用的UUID,是微软公司的全局唯一标识符GUID。其目的是让分布式系统中的所有元素,都能有唯一的辨识信息,而不需要通过中央控制端来做辨识信息的指定。

 

【再说过程】

名词释义是不是看得一脸懵逼?莫慌,下图介绍了各个设备标识码的区别,以及目前的情况。

 

image

 

点击图片查看清晰大图

 

Android设备标识码

 

  1. device ID。
    又叫设备ID。可以用系统提供的TelephonyManager服务来获取,具有唯一性。其中又包括IMEI和MEID/ESN。
  2. IMEI:GSM设备返回的,并且是写在主板上的,重装APP不会改变IMEI。Android 6.0以上系统需要用户授予read_phone_state权限,如果用户拒绝就无法获得。
  3. mac地址:硬件标识符,包括WiFi mac地址和蓝牙mac地址。Android 6.0之后被禁止,若获取则会被判定为有害应用。
  4. MEID/ESN:CDMA设备返回的,一般不使用。
  5. UDID:用来标示设备的唯一性。获取UUID后,写入.so文件生成UDID。即使APP重装,值也不变,除非root手机(普通用户做不到)
  6. UUID:APP重装后会改变。

 

四 最后的方案

 

  • 采取三个id,存储在云端,可以认为其中有两项id一致,则认为这个手机是一个手机。

 

4.1 方案分析

 

一种方案是组合设备ID(直接拼接,或者拼接后计算摘要)。

举个例子,假如出现重复的概率和发生变化的的概率都是千分之一,

则对于两台不同设备,两个设备ID同时重复的概率是百万分之一,两个设备ID至少有一个发生变化约为千分之二。

也就是,拼接ID的效果是大大提高唯一性,但是一定程度上降低稳定性(只要其中一个要素变化,拼接的ID就变了)。

但事实上,如今能拿到的设备ID,最突出的矛盾是不稳定,所以,我们不能为了提高唯一性而牺牲稳定性。

 

要提高稳定性,可以引入容错方案。

容错方案有很多,比如网络传输,用checksum去校验报文,如果出错了则重发;

再如磁盘阵列,数据写入两个磁盘,只有当两个磁盘同时出错时才会丢失数据,从而大大降低丢失数据的概率。

但是对于设备ID,以上两种方案都不合适,因为上面的方案需要通过checksum来确认原信息是否被修改,设备ID没有这样的条件。

 

所以,可以引入类似虚拟货币用到的"拜占庭容错"方案。

简单地说,就是要采集三个设备ID到云端,如果有两个(包括两个以上)的设备ID和之前的记录相同,则认为是同一台设备。

同样假设出现重复的概率和发生变化的的概率都是千分之一,则:

同一台设备的两次采集,认不出是同一台设备的条件为“至少两个设备ID都和上次不一样”,概率约为百万分之三。

两台不同的设备,认为是同一台的条件是为“三个设备ID中,至少有两个设备ID和另一台设备相同”,概率同样约为百万分之三。

所以,用此方案,唯一性和稳定性都能得到提高。

 

4.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 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
}
复制代码

 

  • 如果设备序列号、Android ID、MAC全都不等,则前面的SQL查询不会返回记录(也就是没有匹配的设备)。
  • 如果设备序列号,Android ID 和 MAC 全部相同,直接返回。
  • 否则,遍历列表,取优先级最高的deviceId返回。
  • 如果只有Android ID 或 MAC 之一相等,但是设备信息都匹配不上的话,也认为不是同一个设备。

 

如果没有匹配的设备,则认为是新设备;

此时,生成新的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是比较困难的,而基于信息采集和云端综合计算则相对容易。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值