讨论android的设备唯一码
设备唯一码:是用一串字符或者号码来映射唯一的硬件设备。
有啥子用?
- 统计
用来统计用户行为,设置大数据标签。一般情况下都是用应用账号作为唯一码,但是只能适合部分强登录应用,对于那种非强登录的app而言,比如购物类,看房类app而言,设备唯一码是用来做大数据统计的唯一选择了。 - 风控
防止羊毛党重复注册、重复撸羊毛、恶意访问安全等问题。风控则是最需要稳定可靠的设备唯一码了。 - 推送
保证推送唯一性和准确性 - 错误回传
统计当前设备错误日志,当指定某用户发生重大崩溃,需要统计该设备崩溃信息,做补救措施。比如单一补丁包或者后端特殊处理。
怎么获取?
有很多特征值都能被当做设备唯一码去使用,但是可靠性和稳定性多多少少有点不符合最好的预期效果。
- IMEI
国际移动设备身份码 ,具有GSM/WCDMA/LTE功能的手机才拥有IMEI号,双卡双待手机会有2个IMEI号,可拨号 *#06# 查看。
具备设备唯一性,不会因为恢复出厂设置和刷机等操作改变,是理想的设备ID 。
但是在android6.0以后的手机,必须要获取READ_PHONE_STATE
权限才能获取到IMEI号,在合规改造的趋势下,这种权限用户不一定会赋予。
而且在ANDROID 10.0后,即使拥有该权限也不能获取IMEI。
各大厂的app已经放弃使用IMEI号作为设备唯一码了,都开始设计自己的方案去代替IMEI号了,同时也需要做好设备唯一码迁移适配工作。
获取代码如下,记得加权限
TelephonyManager tm = (TelephonyManager) getSystemService(Context.TELEPHONY_SERVICE);
//不同的手机设备返回IMEI,MEID或者ESN码
String deviceId = tm.getDeviceId();
//返回IMEI
String imei = tm.getImei();
//返回MEID
String meid = tm.getMeid();
//手机SIM卡唯一标识
String simSerialNumber = tm.getSimSerialNumber();
//返回例如独特的用户ID,一个GSM手机的号码。
String subscriberId = tm.getSubscriberId();
//手机号码
String line1Number = tm.getLine1Number();
- ANDROID_ID
设备首次启动时会分配一个64位的数字,并把这个数字以16进制字符串的形式保存下来,这个16进制的字符串就是ANDROID_ID。相对而言,如果不是购物支付类的应用,基本上够用了。
ANDROID_ID注意以下存在状况:
厂商定制的系统可能存在ANDROID_ID再不同设备上是一样的,也有可能为null。刷机或者恢复出厂设置就重新分配新的ANDROID_ID了
如果用户设备是8.0以下,后来卸载了,升级到8.0之后又重装了应用,ANDROID_ID也不一样
String ANDROID_ID = Settings.System.getString(getContentResolver(), Settings.System.ANDROID_ID);
String androidID = Settings.Secure.getString(getContentResolver(), Settings.Secure.ANDROID_ID);
- MAC
获取MAC物理地址,适配性很差。
6.0以下获取方法(不包括6.0)
//必须的权限 <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
WifiManager wifi = (WifiManager)getSystemService(Context.WIFI_SERVICE);
WifiInfo winfo = wifi.getConnectionInfo();
String mac = winfo.getMacAddress();
6.0至7.0获取方法如下(包括6.0,不包括7.0)
private static String getMacAddress() {
String WifiAddress = "02:00:00:00:00:00";
try {
WifiAddress = new BufferedReader(new FileReader(new File("/sys/class/net/wlan0/address"))).readLine();
} catch (IOException e) {
e.printStackTrace();
}
return WifiAddress;
}
7.0以后获取方法
//需要android.permission.INTERNET权限
List<NetworkInterface> all = Collections.list(NetworkInterface.getNetworkInterfaces());
for (NetworkInterface nif : all) {
if (!nif.getName().equalsIgnoreCase("wlan0")) continue;
byte[] macBytes = nif.getHardwareAddress();
if (macBytes == null) {
return null;
}
StringBuilder res1 = new StringBuilder();
for (byte b : macBytes) {
res1.append(String.format("%02X:", b));
}
if (res1.length() > 0) {
res1.deleteCharAt(res1.length() - 1);
}
return res1.toString();
}
说不定以后再升个级,又要适配。还有就是权限依赖问题导致获取不稳定。
google不推荐使用MAC
https://developer.android.com/training/articles/user-data-ids#java
需求要啥?
往往需求决定你的设备唯一码是在那个纬度上,是应用安装生成纬度,启动纬度,物理设备映射纬度等,选择合适你的方案作为设备唯一码,在保证需求同时稳定性也不错。可以参照国家标准:
标识符 | 全称 | 中文 |
---|---|---|
IMEI | International Mobile Equipment Identity | 国际移动设备识别码 |
UDID | Unique Device Identifier | 设备唯一标识符 |
OAID | Open Anonymous Device Identifier | 匿名设备标识符 |
VAID | Open Anonymous Device Identifier | 开发者匿名设备标识符 |
AAID | Application Anonymous Device Identifier | 应用匿名设备标识符 |
设备唯一标识符: 是指设备唯一硬件标识,设备生产时根据特定的硬件信息生成,可用于设备的生产环境及合法性校验。
匿名设备标识符: 是可以连接所有应用数据的标识符,移动智能终端系统首次启动后立即生成,可用于广告业务。
开发者匿名设备标识符: 是指用于开放给开发者的设备标识符,可在应用安装时产生,可用于同一开 发者不同应用之间的推荐。
应用匿名设备标识符: 是指第三方应用获取的匿名设备标识,可在应用安装时产生,可用于用户统计等。
按照上面的划分,知道你的需求需要哪一种标识,用相应的标识,当然其方案也不一样,实现的难度也会不一样。
方案选择?
最直接的方案,当然选择中国信息通信研究院所给的sdk,直接去使用就行了。
根据“移动智能终端补充设备标识体系”技术要求,华为、小米、OPPO、vivo、中兴、努比亚、魅族、联想、三星等设备厂商均将逐步实现本标识体系,联盟计划开发并发布支持多厂商的统一的补充设备标识调用SDK,协助移动应用开发者更便捷的访问移动智能终端补充设备标识体系,推进相关业务。
先给出地址大家看下
移动安全联盟
vivo开发平台移动设备标识服务
小米移动设备标示服务
能用官方的方案当然很好,但其实也有很多兼容性问题。很多厂商合作进入体系毕竟晚,导致部分旧机型可能获取不到上面的标识。所以也可以用自己的方案,当然没有官方的稳定可靠。
这里说几个常用的方案:
- 方案一
直接获取ANDROID_ID后,再使用UUID去格式化一次。
String androidID = Settings.Secure.getString(getContentResolver(), Settings.Secure.ANDROID_ID);
UUID deviceUuid = new UUID(androidID.hashCode(), ((long)androidID.hashCode() << 32));
如果用户体量不大,可以当成设备唯一标识符,但是androidid的问题还是会存在。
- 方案二
安装时生成随机的randomUUID,存储在本地,UUID.randomUUID()对于体量小用户重复概率忽略不计。作为开发者匿名设备标识符是能满足的。
String randomUUID = UUID.randomUUID().toString();
//存储在本地
- 方案三
获取mac地址、imei地址(有就有,无则空)、设备硬件信息、ANDROID_ID等数据后去拼接生成一个新的设备id。当有新的信息变动时候,可以追加为同一设备下的设备id,意思就是说有imei号的生成的设备id和没imei号的生成设备id都视为同一设备id。
有几个核心产数一起拼接作为设备id,去传给后端,不同的策略去处理。
wifiMac为设备mac地址
imeiId 为imei号,如果不能获取到,则拼接0000000000000000的md5值去拼接,后端处理策略会判断ime的值是否存在
deviceBuildID 为不可变的设备参数,比如分辨率(有的机型可以自己改变分辨率,可加可不加)、cpu核心数、cpu型号、屏幕尺寸、内存总大小等参数,这个值很稳定不会变化,但是会重复。可作为imei号没有取到的情况下,作为辅助判断。
deviceBuildIDChange 是刷机会改变的参数,可以作为开发者匿名设备标识符,也可以配合其他参数作为设备唯一码。
/**
* 返回一个xx..16位..xx-xx..16位..xx-xx..16位..xx的id
* 使用md5加密 16位
* @return
*/
public String getDeviceId(){
StringBuilder sbDeviceId = new StringBuilder();
//mac地址 如果mac == 02:00:00:00:00:0 视为未获取到
String wifiMac = md5Decode16(getWifiMac()).toUpperCase();
sbDeviceId.append(wifiMac);
sbDeviceId.append("-");
//imei地址 如果imeiId == "" 视为未获取到
String imeiId = md5Decode16(getImeiId());
sbDeviceId.append(imeiId);
sbDeviceId.append("-");
//几个核心硬件参数生成的uuid 分辨率 cpu核心数 cpu型号 屏幕尺寸 存储
//这些是不会随刷机或者返厂设置而改变 但是这些参数单独作为设备id 会大量重复
String deviceBuildID = md5Decode16(getBuildUUid());
sbDeviceId.append(deviceBuildID);
sbDeviceId.append("-");
//型号 制造商 版本号 android_id
//会随刷机放生变化
String deviceBuildIDChange = md5Decode16(getBuildUUidTwo());
sbDeviceId.append(deviceBuildIDChange);
return sbDeviceId.toString();
/**
* 区分几个纬度,来定义你的需求(假设 wifiMac 和 imeiId 正常获取下 )
* 如果deviceBuildIDChange 改变 ,而 imeiId deviceBuildID没发生改变
* 是不是可判断 同一用户下该设备刷机了,视为同一用户,羊毛党屏蔽掉
*
* 如果 imeiId 发生改变,换手机了(imeiId 还是作为比较强的 设备映射)
*
* 各种策略都可以自己后端定制
*/
}
后面优化,可以加上前端与后端的同步,更加的稳定。
可能上述方案也是有缺陷的,但是当前设备唯一码的现状就是这样。也算上android走向合规、安全的见证人吧。也希望国内的统一方案能解决更多的机型适配问题,这样我们也好偷懒了。