Android设备唯一标识

前言

近期想要了解一下获取设备的唯一标识,然后我就头疼了。然后。。。。。。
今天让我们来归纳总结一下。

IMEI,MEID,ESN,IMSI,android_id 之间的区别

我们先来确定几个概念,有利于下面的开发:
1.IMEI

  • (国际移动设备识别码(IMEI:International Mobile Equipment Identification Number)是区别移动设备的标志,储存在移动设备中,可用于监控被窃或无效的移动设备。IMEI组成如下图所示,移动终端设备通过键入“*#06#” 即可查得。
  • 其总长为15位,每位数字仅使用0~9的数字。其中TAC代表型号装配码,由欧洲型号标准中心分配;FAC代表装配厂家号码;SNR为产品序号,用于区别同一个TAC和FAC中的每台移动设备;SP是备用编码。

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

串码的作用:
IMEI为TAC + FAC + SNR + SP。IMEI(International Mobile Equipment Identity)是国际 移动设备身份码的缩写,国际移动装备辨识码,是由15位数字组成的"电子串号",它与每台手机一一对应,而且该码是全世界唯一的。每一只手机在组装完成后都将被赋予一个全球唯一的一组号码,这个号码从生产到交付使用都将被制造生产的厂商所记录。
当手机被盗的时候,如知道IEMI码,可以通过手机供应商进行手机锁定,即:获知被盗之后的手机号码,中止手机的通话功能,获知手机的方位。一般情况下,供应商不会对个人或单位提出的定位或锁定手机的请求进行受理。在国内,有关的国家安全部门会对手机串号进行一定程度的管理。

2.MEID

  • Mobile Equipment IDentifier(MEID)是全球唯一的56bit移动终端标识号。标识号会被烧入终端里,并且不能被修改。可用来对移动式设备进行身份识别和跟踪。由于ESN号段是有限的资源,基本上耗尽,可能还有少量回收利用的号段,所以制定了56bits的MEID号段,用来取代32bits的ESN号段。MEID主要分配给CDMA制式的手机。
  • 由于CDMA移动设备增多,导致原来8位的ESN不够用,所以56bits=(56/4=14bytes)的MEID横空出世。现在的CDMA手机一般ESN/MEID两者都有。MEID也是用16进制来表示。

MEID由14个十六进制数字标识,第15位为校验位,不参与空中传输。
RR:范围A0-FF,由官方分配
XXXXXX:范围 000000-FFFFFF,由官方分配
ZZZZZZ:范围 000000-FFFFFF,厂商分配给每台终端的流水号
C/CD:0-F,校验码

手机中的IMEI和MEID号码就如同我们生活中的身份证一样,它是识别手机身份的重要依据,如用虚假号码的手机,网络运营商可随时通过技术手段关闭此手机在网络中的运营,这将给手机的使用者带来巨大的使用风险。手机中所使用的IMEI或MEID等号段均可透过摩尔实验室等相关机构合法申请。
另外,MEID的申请,是需要付费的。目前的价格是每1M范围的MEID的费用是8000美元,每增加1M范围的MEID号码需要额外付费8000美元。
MEID号码是由Telecommunications Industry Association(TIA)进行分配管理的。
  MEID号码的查看,目前没有一个通用的方法,由各手机制造商自己设置。可以通过查看手机说明书得到查看MEID号码的方法。

3.ESN

  • 电子序列号,在CDMA 系统中,是鉴别一个物理硬件设备唯一的标识。也就是说每个手机都用这个唯一的ID来鉴别自己, 就跟人的身份证一样。CDMA中的ESN对应于GSM网络中的IMEI。 一个ESN有32 bits,
  • 也就是 32/4 = 8 bytes。随着CDMA移动设别的增多,ESN已经不够用了,所以推出了位数更多的MEID。ESN用16进制来表示。

4.IMSI

  • 国际移动用户识别码(IMSI:International Mobile Subscriber Identification Number)是区别移动用户的标志,储存在SIM卡中,可用于区别移动用户的有效信息。
  • IMSI总长度不超过15位,同样使用0~9 的数字。其中MCC是移动用户所属国家代号,占3位数字,中国的MCC规定为460;MNC是移动网号码,最多由两位数字组成,用于识别移动用户所归属的移动通信网;
  • MSIN是移动用户识别码,用以识别某一移动通信网中的移动用户。

5.android_id

  • android设备的唯一识别码

根据上面的总结获取到自己手机的相关的信息如下::
meid[99000939335539]
imei1[863254036690765]
imei2[863254036690773]
simSerialNumber [89860117801033225378]
subscriberId [460011691516816]
下面是华为手机:
MEID:A000007F2C4002
IMEI1:866957038217380
IMEI2:866957038364067
PESN:802B5D2A
SN:8BN02179015937
因为我的手机是双卡的手机所以有两个imei的号码。

获取Android 唯一标识的分析

1.DEVICE_ID,不同的手机设备返回IMEI,MEID或者ESN码

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

这个DEVICE_ID可以同通过下面的方法获取:
 TelephonyManager tm = (TelephonyManager)getSystemService(Context.TELEPHONY_SERVICE);`
 String DEVICE_ID = tm.getDeviceId();`

上面的方法在api26已经放弃。官方建议使用getImei()和getMeid()这两个方法得到相应的值。

非手机设备: 如果只带有Wifi的设备或者音乐播放器没有通话的硬件功能的话就没有这个DEVICE_ID
权限: 获取DEVICE_ID需要READ_PHONE_STATE权限,但如果我们只为了获取它,没有用到其他的通话功能,那这个权限有点大才小用

1.1 Sim Serial Number 手机SIM卡唯一标识

代码如下:

  TelephonyManager tm = (TelephonyManager)context.getSystemService(Context.TELEPHONY_SERVICE);
    String SimSerialNumber = tm.getSimSerialNumber();

注意:对于CDMA设备,返回的是一个空值。

1.2 TelephonyManager可以得到的其他的一些值
TelephonyManager tm = (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE);

 String deviceId = tm.getDeviceId();//不同的手机设备返回IMEI,MEID或者ESN码
 String imei = tm.getImei();//返回IMEI
 String meid = tm.getMeid();//返回MEID
 String simSerialNumber = tm.getSimSerialNumber();//手机SIM卡唯一标识 
 String subscriberId = tm.getSubscriberId();//返回例如独特的用户ID,一个GSM手机的号码。
 String line1Number = tm.getLine1Number();//手机号码

TelephonyManager的详情可以参考这篇博客telephonymanager详解

2.MAC 地址

MAC 地址具有全局唯一性,无法由用户重置,在恢复出厂设置后也不会发生变化。一般不建议使用 MAC 地址进行任何形式的用户标识。因此,从 Android M 开始,无法再通过第三方 API 获得本地设备 MAC 地址(例如,WLAN 和蓝牙)。[WifiInfo.getMacAddress()] 方法和 [BluetoothAdapter.getDefaultAdapter().getAddress()] 方法都会返回 02:00:00:00:00:00

//wifi mac地址 
WifiManager wifi = (WifiManager) 
context.getSystemService(Context.WIFI_SERVICE); 
WifiInfo info = wifi.getConnectionInfo(); 
String wifiMac = info.getMacAddress(); 
上面的方法在安卓6.0以前是可以得到的mac地址的,但是在安卓6.0以后得到的都是02:00:00:00:00:00这个地址。

网上有方法获取8.0wifi的mac地址:

    public static String getMacAddressFromIp(Context context) {
        String mac_s = "";
        StringBuilder buf = new StringBuilder();
        try {
            byte[] mac;
            NetworkInterface ne = NetworkInterface.getByInetAddress(InetAddress.getByName(getIpAddress(context)));
            mac = ne.getHardwareAddress();
            for (byte b : mac) {
                buf.append(String.format("%02X:", b));
            }
            if (buf.length() > 0) {
                buf.deleteCharAt(buf.length() - 1);
            }
            mac_s = buf.toString();
        } catch (Exception e) {
            e.printStackTrace();
        }
        return mac_s;
    }


    public static String getIpAddress(Context context) {
        NetworkInfo info = ((ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE)).getActiveNetworkInfo();
        if (info != null && info.isConnected()) {
            // 3/4g网络
            if (info.getType() == ConnectivityManager.TYPE_MOBILE) {
                try {
                    for (Enumeration<NetworkInterface> en = NetworkInterface.getNetworkInterfaces(); en.hasMoreElements(); ) {
                        NetworkInterface intf = en.nextElement();
                        for (Enumeration<InetAddress> enumIpAddr = intf.getInetAddresses(); enumIpAddr.hasMoreElements(); ) {
                            InetAddress inetAddress = enumIpAddr.nextElement();
                            if (!inetAddress.isLoopbackAddress() && inetAddress instanceof Inet4Address) {
                                return inetAddress.getHostAddress();
                            }
                        }
                    }
                } catch (SocketException e) {
                    e.printStackTrace();
                }
            } else if (info.getType() == ConnectivityManager.TYPE_WIFI) {
                //  wifi网络
                WifiManager wifiManager = (WifiManager) context.getSystemService(Context.WIFI_SERVICE);
                WifiInfo wifiInfo = wifiManager.getConnectionInfo();
                String ipAddress = intIP2StringIP(wifiInfo.getIpAddress());
                return ipAddress;
            } else if (info.getType() == ConnectivityManager.TYPE_ETHERNET) {
                // 有限网络
                return getLocalIp();
            }
        }
        return null;
    }

    private static String intIP2StringIP(int ip) {
        return (ip & 0xFF) + "." +
                ((ip >> 8) & 0xFF) + "." +
                ((ip >> 16) & 0xFF) + "." +
                (ip >> 24 & 0xFF);
    }


    // 获取有限网IP
    private static String getLocalIp() {
        try {
            for (Enumeration<NetworkInterface> en = NetworkInterface.getNetworkInterfaces(); en.hasMoreElements(); ) {
                NetworkInterface intf = en.nextElement();
                for (Enumeration<InetAddress> enumIpAddr = intf.getInetAddresses(); enumIpAddr.hasMoreElements(); ) {
                    InetAddress inetAddress = enumIpAddr.nextElement();
                    if (!inetAddress.isLoopbackAddress() && inetAddress instanceof Inet4Address) {
                        return inetAddress.getHostAddress();
                    }
                }
            }
        } catch (SocketException ex) {

        }
        return "0.0.0.0";
    }

上面的方式在小米安卓8.0系统的手机和华为荣耀8.0系统的手机都是可以的。其他的手机未测试。

注意:
要想获取mac地址,是必须wifi联网的,否则得不到相应的名称。

3.ANDROID_ID

在设备首次启动时,系统会随机生成一个64位的数字,并把这个数字以16进制字符串的形式保存下来,这个16进制的字符串就是ANDROID_ID,当设备被wipe后该值会被重置。
可以通过下面的方法获取:

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

ANDROID_ID可以作为设备标识,但需要注意:
1. 厂商定制系统的Bug:不同的设备可能会产生相同的ANDROID_ID:9774d56d682e549c。
2. 厂商定制系统的Bug:有些设备返回的值为null。
3. 设备差异:对于CDMA设备,ANDROID_ID和TelephonyManager.getDeviceId() 返回相同的值。

4.Serial Number 串号

Android系统2.3版本以上可以通过下面的方法得到Serial Number,且非手机设备也可以通过该接口获取。

String serial= android.os.Build.SERIAL;
4.1 android.os.Build里面能够获取的一些值

直接上代码:

设备序列号(Serial Number, SN)
String serialNum = android.os.Build.SERIAL;

制造商 (Manufacturer)
String manufacturer = android.os.Build.MANUFACTURER;

型号(Model)
String model = android.os.Build.MODEL;

品牌(Brand)
String brand = android.os.Build.BRAND;

设备名 (Device)
String device = android.os.Build.DEVICE;

下面是华为荣耀9和小米得到的相关的信息:

Serial_Num: 8BN0217919015937
Manufacturer: HUAWEI
Model: STF-AL00
Brand: HONOR
Device: HWSTF
下面是小米6的相关信息
Sim_SN: 89860117801033225378
Manufacturer: Xiaomi
Model: MI 6
Brand: Xiaomi
Device: sagit

5. Installtion ID 安装ID

这种方式的原理是在程序安装后第一次运行时生成一个ID,该方式和设备唯一标识不一样,不同的应用程序会产生不同的ID,同一个程序重新安装也会不同。所以这不是设备的唯一ID,但是可以保证每个用户的ID是不同的。可以说是用来标识每一份应用程序的唯一ID(即Installtion ID),可以用来跟踪应用的安装数量等。
直接上代码:

/**
 * @author yzzCool
 * @desc 安装ID 的生成和获取,就是和UUID存取SP是一个道理
 * create at 2018/8/28
 */
public class Installation {
    private static String sID = null;
    private static final String INSTALLATION = "INSTALLATION";

    public synchronized static String id(Context context) {
        if (sID == null) {
            File installation = new File(context.getFilesDir(), INSTALLATION);
            try {
                if (!installation.exists())
                    writeInstallationFile(installation);
                sID = readInstallationFile(installation);
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
        }
        return sID;
    }

    private static String readInstallationFile(File installation) throws IOException {
        RandomAccessFile f = new RandomAccessFile(installation, "r");
        byte[] bytes = new byte[(int) f.length()];
        f.readFully(bytes);
        f.close();
        return new String(bytes);
    }

    private static void writeInstallationFile(File installation) throws IOException {
        FileOutputStream out = new FileOutputStream(installation);
        String id = UUID.randomUUID().toString();
        out.write(id.getBytes());
        out.close();
    }
}

上面的代码也可以用SharedPreferences实现:

SharedPreferences和UUID的配合:
从SharedPreferences里面获取uuid,如果有直接返回,如果没有生成
String uuid = UUID.randomUUID().toString();
然后返回uuid和把这次生成的uuid存储到SharedPreferences里面。

标识符使用实战

首先,我们先要弄清自己跟踪设备的具体需求。目前看来,需求无非两种:

  • 1.跟踪用户设备使用周期层次上的设备。
    意思是将每次用户的擦除设备、恢复出厂设置动作后将设备视为一台新的设备。
  • 2.跟踪硬件层次上的设备。
    意思是无论设备擦除数据或者恢复出厂设置后都需要将该设备视为同一台设备。

1.跟踪用户使用层次上的设备

理论上,Android_ID 这一个值就已经足够我们实现这样的需求,不过正是因为 Android_ID 存在缺陷,所以我们无法直接拿来识别设备。这里我们使用多个值拼凑来规避这些缺点。
与用户设备使用周期有关的标识符我推荐使用Android_ID和Sim Serial Number。另外可以加上Device_ID,通过 UUID 或者 MD5 等来计算生成设备的标识符。

  • UUID 实现:
UUID deviceUuid = new UUID(androidId.hashCode(), ((long)deviceId.hashCode() << 32) | simSerialNum.hashCode());
String deviceId = deviceUuid.toString();

结果类似:00000000-15e4-6485-ca79-2ee02e8e8bdc
  • MD5 实现(MD5 算法见下文):
String md5ID = md5(androidId + deviceId + simSerialNum);
结果类似:8d78296180f8c488de11837384350bce

2.跟踪硬件层次上的设备

跟踪硬件层次上的设备建议使用硬件的标识符,比如设备ID(DeviceId)、Mac 地址、设备序列号(SN)或者设备的品牌,型号名等,这些值在用户擦除数据或者恢复出厂设置后也不会改变。同样的,为了提升稳定性及排除单一标识符所存在的缺陷,我们使用多个标识符拼接,然后通过 UUID 或者 MD5 算法计算得出我们需要的设备标识符。

  • UUID 实现:
UUID deviceUuid = new UUID(sn.hashCode(), ((long)deviceId.hashCode() << 32) | macAddress.hashCode());
String deviceId = deviceUuid.toString();

结果类似:00000000-15e4-6485-ca79-2ee02e8e8bdc
  • MD5 实现(MD5 算法见下文):
String md5ID = md5(sn+ deviceId + macAddress);
结果类似:8d78296180f8c488de11837384350bce

补充上面所需的md5的方法:

// MD5加密,32位小写
    public static String md5(String str) {
        MessageDigest md5 = null;
        try {
            md5 = MessageDigest.getInstance("MD5");
        } catch (Exception e) {
            e.printStackTrace();
            return "";
        }
        md5.update(str.getBytes());
        byte[] md5Bytes = md5.digest();
        StringBuilder hexValue = new StringBuilder();
        for (int i = 0; i < md5Bytes.length; i++) {
            int val = ((int) md5Bytes[i]) & 0xff;
            if (val < 16) {
                hexValue.append("0");
            }
            hexValue.append(Integer.toHexString(val));
        }
        return hexValue.toString();
    }

上面的实战重要的是思想。

总结

上面的总结,总结的我都快疯了。这一块比较琐碎,没有一种系统的感觉。东一块系一块然后拼接。我的天啊!!!

推荐阅读更多精彩内容