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 方法就被废弃了,不再推荐使用。而推荐使用 getImeigetMeid 同时这两个方法支持传入卡槽的下标来确定要读取那个卡对应的值。在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

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

推荐阅读更多精彩内容