Android 上关于设备唯一标识的调研

来自:千山万水迷了鹿

看看官网怎么说的唯一标识符最佳做法

一、先来明白几个概念:

什么是IMEI?

IMEI(International Mobile Equipment Identity)是国际移动设备身份码的缩写,国际移动装备辨识码,是由15位数字组成的"电子串号",它与每台手机一一对应,而且该码是全世界唯一的。每一部手机在组装完成后都将被赋予一个全球唯一的一组号码,这个号码从生产到交付使用都将被制造生产的厂商所记录。IMEI码由GSM(全球移动通信协会)统一分配,授权BABT(英国通信认证管理委员会)审受。

IMEI组成为:
1、前6位数(TAC,Type ApprovalCode)是"型号核准号码",一般代表机型。
2、接着的2位数(FAC,Final Assembly Code)是"最后装配号",一般代表产地。
3、之后的6位数(SNR)是"串号",一般代表生产顺序号。
4、最后1位数(SP)通常是"0",为检验码,备用。

一般在 Android 手机上可以在关于手机里面查看到

什么是IMSI?

国际移动用户识别码(IMSI:International Mobile Subscriber Identification Number)是区别移动用户的标志,储存在SIM卡中,可用于区别移动用户的有效信息。其总长度不超过15位,同样使用0~9的数字。

什么是ICCID?

ICCID:Integrate circuit card identity 集成电路卡识别码(固化在手机SIM卡中),简单来说就是SIM卡序列号,它拥有独一无二的特性,类似于手机的序列号,仅仅指向一张手机卡。共有20位数字组成,不同运营商编码格式不一样。并且前六位数字为运营商代码:比如中国移动的为:898600;中国联通的为:898601,中国电信的为:898603。

简而言之:IMEI / MEID 是和设备相关的,移动设备的唯一标志码;而IMSI 和 ICCID 是和手机卡相关的,信息存储到手机卡上,没有安装手机卡的手机,上面获取这两个值为null。

二、TelephonyManager 相关

//注意代码使用需要在Manifest配置权限
// <uses-permission android:name="android.permission.READ_PHONE_STATE" /> 
//在API 23(Android6.0 )及其以上版本phone组权限需要动态申请
TelephonyManager tm = (TelephonyManager) this.getSystemService(Context.TELEPHONY_SERVICE);  
String deviceid = tm.getDeviceId();   //取 IMEI或者MEID
String tel = tm.getLine1Number();     //取出用户手机号码,手机没有安装SIM卡,值为null
String imsi =tm.getSubscriberId();     //取出IMSI,手机没有安装SIM卡,值为null
String imei =tm.getSimSerialNumber();  //取出ICCID,手机没有安装SIM卡,值为null

其实上面的信息分开来看总共分为两部分,一部分是设备相关的信息,一部分是SIM卡相关的信息。关于SIM卡相关的信息,一般来说没有办法标志设备,因为手机可以任意替换SIM卡,而且现在手机都是双卡双待的,关于双卡手机获取和手机卡相关的信息参考这篇文章

我们这里主要关心设备ID(IMEI或者MEID),解释下这两个东西:IMEI是国际移动设备识别码的简称,而MEID是 动设备识别码的简称,一般IMEI是所有设备都有,而MEID一般只在只有支持CDMA制式的设备才有的。

OPPO 手机:进入手机设置--常规--关于手机--状态信息--IMEI即可看到。
小米手机: 设置--我的设备--全部参数--状态信息---IMEI信息
华为手机:设置---关于手机

注意:平板没有imei号

三、设备唯一ID

1. IMEI或者MEID

Android 系统关于双卡的支持的知识需要知道一些,在 Android4.x 及其以下版本的时候,原生 Android 是不支持双卡的,在 5.x左右开始支持,但是 api 是隐藏的,在Android 6.0 才开始公开开放双卡的 API。

来看下我们获取IMEI或者MEID的基本方法

//注意动态权限申请
TelephonyManager telephonyManager = (TelephonyManager) this.getSystemService(Context.TELEPHONY_SERVICE);
String imei = telephonyManager.getDeviceId();

这个方法在4.x及其以下版本的时候,运行这个方法是没有问题的,因为4.x是不支持双卡的,也就是说4.x的手机要么是GSM要么是CDMA制式的。所以,getDeviceId的文档这样写道:

    /**
     * Returns the unique device ID, for example, the IMEI for GSM and the MEID
     * or ESN for CDMA phones. Return null if device ID is not available.
     * 翻译过来就是:这个方法会返回唯一的设备id,
     * 比如在GSM的手机上返回的是IMEI,而在CDMA 手机上返回的是MEID或者ESN。
     * 如果设备id不可读取,那么返回null。
     */

但是这种情况到了Android 6.0 (先不考虑5.x 那个版本不稳定)上就不一样了,6.0支持双卡,也就是说手机上不可以能只有一个IMEI或者MEID,这个时候就需要根据手机卡槽获取了,这个卡槽里面装的是什么制式的卡,那么对于下标获取的就是IMEI或者MEID:

    public void onClick(View view) {

        TelephonyManager tm = (TelephonyManager) this.getSystemService(Context.TELEPHONY_SERVICE);

        Log.d("Q_M""meid:" + tm.getDeviceId());
        Log.d("Q_M""meid:" + tm.getDeviceId(0));
        Log.d("Q_M""meid:" + tm.getDeviceId(1));

    }

测试结果来自网络,我没有验证(我对不插卡的情况存在疑问):

我的两张联通卡分别获取的是imei1和imei2

    1、不插卡(或两张卡都是GSM卡)

      getDeviceId()  返回 imei1
      getDeviceId(0) 返回 imei1
      getDeviceId(1) 返回 imei2

      2、卡1插CDMA卡,卡2不插卡(或卡2插GSM卡)

      getDeviceId()   返回 meid
      getDeviceId(0) 返回 meid
      getDeviceId(1) 返回 imei2

      3、卡1不插卡(或卡1插GSM卡)卡2插CDMA卡

      getDeviceId()   返回 imei1
      getDeviceId(0) 返回 imei1
      getDeviceId(1) 返回 meid

后来到了Android 8.0 ,方法控制更为精细了,所以这个getDeviceId 方法就被废弃了,不再推荐使用。而推荐使用 getImei和getMeid 同时这两个方法支持传入卡槽的下标来确定要读取那个卡对应的值。在8.0 及其以上的手机上可以这么严重:

    public void onClick(View view) {

        TelephonyManager tm = (TelephonyManager) this.getSystemService(Context.TELEPHONY_SERVICE);

        //手机上存在两个imei值,分配给两个卡槽
        Log.d("Q_M""imei:" + tm.getImei());
        Log.d("Q_M""imei0:" + tm.getImei(0));
        Log.d("Q_M""imei1:" + tm.getImei(1));

        //手机上只会存在一个meid,两个卡槽获取的一样
        Log.d("Q_M""meid:" + tm.getMeid());
        Log.d("Q_M""meid0:" + tm.getMeid(0));
        Log.d("Q_M""meid1:" + tm.getMeid(1));
    }

最后来看一眼友盟的代码里面获取 Imei 的方式:

    public static String getImeiNew(Context var0) {
        String var1 = null;

        try {
            if(var0 != null) {
                TelephonyManager var2 = (TelephonyManager)var0.getSystemService("phone");
                if(var2 != null && checkPermission(var0, "android.permission.READ_PHONE_STATE")) {
                    if(VERSION.SDK_INT >= 26) {
                        try {
                            Method var3 = var2.getClass().getMethod("getImei"new Class[0]);
                            var3.setAccessible(true);
                            var1 = (String)var3.invoke(var2, new Object[0]);
                        } catch (Exception var4) {
                            ;
                        }

                        if(TextUtils.isEmpty(var1)) {
                            var1 = var2.getDeviceId();
                        }
                    } else {
                        var1 = var2.getDeviceId();
                    }
                }
            }
        } catch (Exception var5) {
            if(AnalyticsConstants.UM_DEBUG) {
                MLog.w("No IMEI.", var5);
            }
        }

        return var1;
    }

2. ANDROIDID

在设备首次启动时,系统会随机生成一个64位的数字,并把这个数字以16进制字符串的形式保存下来,这个16进制的字符串就是ANDROID_ID,当设备被wipe后该值会被重置。设备恢复出厂设置,这个值也会改变。如果设备被root,这个值可以任意改变。

String androidID = Settings.Secure.getString(context.getContentResolver(), Settings.Secure.ANDROID_ID);

3. Serial Number (设备序列号)

这个东西理论上来说是来自硬件,出厂是就设置好了,但是有些设备厂商会随便写一个值

 Build.SERIAL
在 api>=26的时候
可以这么获取 Build.getSerial();

4. Mac地址

<!--访问WIFI的权限-->
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/> 

Mac地址,在6.0以上不能按正常方式获取,7.0以上很难获取,并且Mac地址也不一定唯一。

  1. 在6.0 以下后去mac地址方式

WifiManager wifi = (WifiManager) context.getSystemService(Context.WIFI_SERVICE);
WifiInfo winfo = wifi.getConnectionInfo();
String mac =  winfo.getMacAddress();
  1. 在6.0 及其以上8.0以下
    上面的方法在7.0 的设备获取的永远是02:00:00:00:00:00

    /**
     * 通过网络接口取
     * 记得添加网络权限
     * <uses-permission android:name="android.permission.INTERNET" />
     *
     * @return mac 地址字符串
     */

    private static String getNewMac() {
        try {
            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();
            }
        } catch (Exception ex) {
            ex.printStackTrace();
        }
        return null;
    }

注:通过adb命令行可以查看手机mac地址,不过需要root手机:

命令行
$ adb shell 
$ su 
$ cat /sys/class/net/wlan0/address

【参考文章】
http://www.cnblogs.com/sfbrzkh/p/5165873.html
https://developer.android.com/training/articles/user-data-ids
https://blog.csdn.net/yangbin0513/article/details/68490291

推荐↓↓↓
安卓开发
上一篇:Android 序列化总结 下一篇:Android 全面屏启动页适配的一些坑