死磕 android 设备识别码

以前也写过Android设备唯一标识码的文章,但是很浅显,罗列参数不同,也没给出我的方案,所以这里再写一次,欢迎大家讨论

手机 root 之后可以利用 Xposed框架 HOOK 修改很多参数,比如 DeviceID,详细可以看:


为什么要获取设别识别码

这个参数是否台服务器要用的,用在哪呢,为的是处理同一账号同时在多台设备上登录的问题,特别上在电商 app 上这个问题有位严重,因为这样极易让订单产生活混乱和各种莫名其妙的问题,很难排查,处理


哪些参数可以做为唯一设备识别码

1. MAC 地址

MAC 地址也叫局域网内地址,是写在网卡硬件设备内的,具有唯一性

            // 6.0 以下 
            var wifiManager = getSystemService(Context.WIFI_SERVICE) as WifiManager
            var macAddress2 = wifiManager.connectionInfo.macAddress
            Log.d("AA", "MAC 地址2:$macAddress2")

            // 6.0 以上
            var networkInterface = NetworkInterface.getByName("wlan0")
            var hardwareAddress = networkInterface.hardwareAddress

            val buffer = StringBuffer()
            for ((index, value) in hardwareAddress.withIndex()) {
                buffer.append(value)
                if (index < hardwareAddress.size - 1) buffer.append(":")
            }
            var macAddress = buffer.toString()
            Log.d("AA", "MAC 地址1:$macAddress")


            var wifiManager = getSystemService(Context.WIFI_SERVICE) as WifiManager
            var macAddress2 = wifiManager.connectionInfo.macAddress
            Log.d("AA", "MAC 地址2:$macAddress2")

但是 MAC 地址也有自己的问题:

  • 设备只有打开 WIFI 并链上网才能拿到的这个 MAC 地址
  • getSystemService 方式在 6.0 以后失效了,只会返回”02:00:00:00:00:00”的常量
  • 有部分手机对系统的修改可能造成 NetworkInterface 获取不到 MAC 地址
  • 另外有人反应 MAC 地址不知为何会变化
  • 没有 WIFI 的硬件设备是没有 MAC 地址的
  • 需要ACCESS_WIFI_STATE权限
2. IMEI 和 MEID
  • IMEI - GSM/WCDMA 手机入网许可证编号,适配移动,联通卡
  • MEID - CDMA 手机入网许可证编号,适配电信卡

拿现在典型的双卡双待手机来说,2个卡槽分别有一个自己所属的 IMEI 号,但是 MEID 号是2个卡槽共享的,也就是说可以同时插2张联通、移动卡,但是只能插一张电信卡

手机拨号*#06#可以看自己手机的 IMEI 和 MEID

那么问题来了,我们常用的获取 DeviceId 的 API:

private String getPhoneIMEI() {
    TelephonyManager tm = (TelephonyManager) getContext().getSystemService(Service.TELEPHONY_SERVICE);
    return tm.getDeviceId();
}

但是这个方法只能返回卡槽1的入网许可证号,根据卡槽1当前插的什么类型的卡,相应返回 IMEI 或者 MEID 号,因此我们只能拿到一个号。要是用户切换卡槽,或者换了运营商,那么这个 DeviceId 也会跟变。另外不能插卡的设备,比如平板就拿不到 DeviceId,我就碰到过数据分析的同学过来问,为啥好多用户没有 DeviceId 呢

使用官方的 API 是拿不到 MEID 的,IMEI 也需要自行去重

tv_imei.setText("IMEI:" + telephoneManage.getDeviceId());
tv_gsm.setText("GSM: " + telephoneManage.getDeviceId(TelephonyManager.PHONE_TYPE_GSM));
tv_cdma.setText("CDMA:" + telephoneManage.getDeviceId(TelephonyManager.PHONE_TYPE_CDMA));
tv_nona.setText("NONE:" + telephoneManage.getDeviceId(TelephonyManager.PHONE_TYPE_NONE));
tv_is.setText("SIP:" + telephoneManage.getDeviceId(TelephonyManager.PHONE_TYPE_SIP));

所以官方的 API 不要期待了

网上有朋友通过反射可以获取到手机内的 IMEI 和 MEID


    fun getMEID2IMEI(): List<String> {
        var meid = getMEID()
        var imeis = getIMEI().toMutableList()
        imeis.add(meid)
        return imeis
    }
    
    private fun getIMEI(): List<String> {
        try {
            val clazz = Class.forName("android.os.SystemProperties")
            val method = clazz.getMethod("get", String::class.java, String::class.java)
            var imei = method.invoke(null, "ril.gsm.imei", "") as String
            if (!TextUtils.isEmpty(imei)) {
                var split = imei.split(",".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()
                if (split == null) {
                    return listOf()
                }
                return split.toList()
            }
        } catch (e: NoSuchMethodException) {
            e.printStackTrace()
            Log.w("IMEI", "getIMEI error : " + e.message)
        } catch (e: IllegalAccessException) {
            e.printStackTrace()
            Log.w("IMEI", "getIMEI error : " + e.message)
        } catch (e: InvocationTargetException) {
            e.printStackTrace()
            Log.w("IMEI", "getIMEI error : " + e.message)
        } catch (e: ClassNotFoundException) {
            e.printStackTrace()
            Log.w("IMEI", "getIMEI error : " + e.message)
        }

        return listOf()
    }

    private fun getMEID(): String {
        try {
            val clazz = Class.forName("android.os.SystemProperties")
            val method = clazz.getMethod("get", String::class.java, String::class.java)

            val meid = method.invoke(null, "ril.cdma.meid", "") as String
            if (!TextUtils.isEmpty(meid) && !"unknown".equals(meid)) {
                return meid
            }
        } catch (e: NoSuchMethodException) {
            e.printStackTrace()
            Log.w("MEID", "getMEID error : " + e.message)
        } catch (e: IllegalAccessException) {
            e.printStackTrace()
            Log.w("MEID", "getMEID error : " + e.message)
        } catch (e: InvocationTargetException) {
            e.printStackTrace()
            Log.w("MEID", "getMEID error : " + e.message)
        } catch (e: ClassNotFoundException) {
            e.printStackTrace()
            Log.w("MEID", "getMEID error : " + e.message)
        }

        return ""
    }

    Log.d("AA", "MEID/IMEI 号:${getMEID2IMEI()}")
D/AA: MEID/IMEI 号:[866778032858841, 866778032858858, 86677803285884]

想分开拿的自己改下方法就好

3. SERIAL

SERIAL 是设备出厂的硬件序列号,不会变动且是唯一的

// 低版本
Build.SERIAL
// 高版本
Build.getSerial()

SERIAL 其实最好的唯一设备识别号,但是蛋疼的是有极个别设备没有 SERIAL...

4. ANDROID_ID

ANDROID_ID 是 android 设备在首次启动时生成的一个 64位数字

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

ANDROID_ID 面临的问题很多:

  • root、刷机、回复出厂设置、升级之后 ANDROID_ID 都会变化
  • 有的厂商定制的系统可能会返回 null 或者产生相同的 ANDROID_ID
  • CDMA 设备,ANDROID_ID 和 TelephonyManager.getDeviceId() 返回相同的值
5. Build.FINGERPRINT

Build.FINGERPRINT 是设备的硬件名称,同型号的设备 Build.FINGERPRINT 是一样的

这是本人 Meizu 16th 的:

Log.d("AA", "硬件名称: ${Build.FINGERPRINT}")
D/AA: 硬件名称: Meizu/meizu_16th_CN/16th:8.1.0/OPM1.171019.026/1539591513:user/release-keys

Build.FINGERPRINT 也有问题,他是编译时决定的值,他的值是很多信息拼凑在一起的


Snip20200811_45.png
6. UUID

UUID 可以生成通用唯一识别码,重复的几率非常之小,可以忽略不计,但是每次使用 UUID 生成的代码是不一样的

java.util.UUID.randomUUID().toString();

我们可以在 UUID 的构造函数中传入指定参数以形成固定不变的 UUID 号码

public static String getDevUUID(Context mContext) {
        synchronized (DevInfo.class) {
            if (uniqueId == null) {
                final TelephonyManager tm = (TelephonyManager) mContext.getSystemService(Context.TELEPHONY_SERVICE);
                final String tmDevice, tmSerial, tmPhone, androidId;
                tmDevice = "" + tm.getDeviceId();
                tmSerial = "" + tm.getSimSerialNumber();
                androidId = "" + android.provider.Settings.Secure.getString(mContext.getContentResolver(), android.provider.Settings.Secure.ANDROID_ID);
                UUID deviceUuid = new UUID(androidId.hashCode(), ((long) tmDevice.hashCode() << 32) | tmSerial.hashCode());
                uniqueId = deviceUuid.toString();
            }
        }
        return uniqueId;
    }
7. Build 参数大全
android.os.Build.BOARD:获取设备基板名称
android.os.Build.BOOTLOADER:获取设备引导程序版本号
android.os.Build.BRAND:获取设备品牌
android.os.Build.CPU_ABI:获取设备指令集名称(CPU的类型)
android.os.Build.CPU_ABI2:获取第二个指令集名称
android.os.Build.DEVICE:获取设备驱动名称
android.os.Build.DISPLAY:获取设备显示的版本包(在系统设置中显示为版本号)和ID一样
android.os.Build.FINGERPRINT:设备的唯一标识。由设备的多个信息拼接合成。
android.os.Build.HARDWARE:设备硬件名称,一般和基板名称一样(BOARD)
android.os.Build.HOST:设备主机地址
android.os.Build.ID:设备版本号。
android.os.Build.MODEL :获取手机的型号 设备名称。
android.os.Build.MANUFACTURER:获取设备制造商
android:os.Build.PRODUCT:整个产品的名称
android:os.Build.RADIO:无线电固件版本号,通常是不可用的 显示unknown
android.os.Build.TAGS:设备标签。如release-keys 或测试的 test-keys 
android.os.Build.TIME:时间
android.os.Build.TYPE:设备版本类型  主要为"user" 或"eng".
android.os.Build.USER:设备用户名 基本上都为android-build
android.os.Build.VERSION.RELEASE:获取系统版本字符串。如4.1.2 或2.2 或2.3等
android.os.Build.VERSION.CODENAME:设备当前的系统开发代号,一般使用REL代替
android.os.Build.VERSION.INCREMENTAL:系统源代码控制值,一个数字或者git hash值
android.os.Build.VERSION.SDK:系统的API级别 一般使用下面大的SDK_INT 来查看
android.os.Build.VERSION.SDK_INT:系统的API级别 数字表示

OK,能用来做唯一识别码的上面都列举了,但是每个参数都有这样那样的问题,没法直接用作唯一识别码,下面我列举下常见的做法


UUID 方案

  • 方案1:UUID + SharePreference(存取)
    APP首次使用时,创建UUID,并保存到SharePreference中。
    以后再次使用时,直接从SharePreference取出来即可;
  • 方案2:UUID + SD卡(存取)
    APP首次使用时,创建UUID,并保存到SD卡中。
    以后再次使用时,直接从SD卡取出来即可;
  • 方案3:imei + android_id + serial + 硬件uuid(自生成)

UUID 必须和持久化存储联系起来,不管是 SharePreference 还是 SD 卡,都不能排除用户手欠删了,再生成的可能的 UUID 就不一样了,imei、android_id 可是会变化的

UUID 上述3个方案用的人很多,但是在我看来并不能虽然能大部分时候保证设备唯一性,但是有个不可忽略的弊端:就是可能用户的在本机上行为导致 UUID 变化,从而错误的向用户发送 T票推送,导致用户必须重新登录。我以前就接到过这样的投诉


MD5 方案

这个方案比较小众,我只见过一次,是用上述的一些参数构建 MD5,弊端和 UUID 一样,只要有参数变化了,MD5 就会变,就会错误的发送T票推送

代码不上了,找不到了


我司方案

说说我司的方案吧,是我定的,有点自卖自夸的嫌疑啊 ,我的思路是我上传一些参数,然后在服务端哪里写逻辑判断,而不是再单独依赖一个 UUID、MD5 识别码了,单一识别码承载不了 android 设备的万千独特性,非算法不行了

  1. 首先判断 SERIAL 硬件序列号,这是最能区分设备的,若是拿不到 SERIAL 再往下走
  2. 然后判断 Build.FINGERPRINT 设备型号,不同的型号自然不是同一个设备,Build.FINGERPRINT 一样再往下走
  3. 我会上传该设备的所有 IMEI、MEID 和账号捆绑,再传 DeviceID,服务端判断 DeviceID 在不在这个账号捆绑的 IMEI、MEID 里,不再就不是同一个设备,要是没有 IMEI、MEID 的话再往下走

----------------------------------------------分割线----------------------------------------------

到这里基本上手机设备都能判断是不是同一个设备了,剩下的就是一些山寨平板设备了

----------------------------------------------分割线----------------------------------------------

  1. 最后综合判断 MAC 地址和 androidID,只用 MAC 地址和 androidID 同时变化才算是不是同一台设备,虽然不能排除同一台设备 MAC 地址和 androidID 同时变化,但是相比 UUID、MD5 来说,错误T票的几率已经大大减少了

最后贴一下工具类

class DeviceIdUtils {

    companion object {

        /**
         * 获取 MEID 和 IMEI
         */
        @JvmStatic
        fun getMEID2IMEI(): List<String> {
            var meid = getMEID()
            var imeis = getIMEI().toMutableList()
            imeis.add(meid)
            return imeis
        }

        /**
         * 获取 IMEI,双卡双待手机 IMEI 有2个
         */
        @JvmStatic
        fun getIMEI(): List<String> {
            try {
                val clazz = Class.forName("android.os.SystemProperties")
                val method = clazz.getMethod("get", String::class.java, String::class.java)
                var imei = method.invoke(null, "ril.gsm.imei", "") as String
                if (!TextUtils.isEmpty(imei)) {
                    var split = imei.split(",".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()
                    if (split == null) {
                        return listOf()
                    }
                    return split.toList()
                }
            } catch (e: NoSuchMethodException) {
                e.printStackTrace()
                Log.w("IMEI", "getIMEI error : " + e.message)
            } catch (e: IllegalAccessException) {
                e.printStackTrace()
                Log.w("IMEI", "getIMEI error : " + e.message)
            } catch (e: InvocationTargetException) {
                e.printStackTrace()
                Log.w("IMEI", "getIMEI error : " + e.message)
            } catch (e: ClassNotFoundException) {
                e.printStackTrace()
                Log.w("IMEI", "getIMEI error : " + e.message)
            }

            return listOf()
        }

        /**
         * 获取 MEID,MEID 只有一个
         */
        @JvmStatic
        fun getMEID(): String {
            try {
                val clazz = Class.forName("android.os.SystemProperties")
                val method = clazz.getMethod("get", String::class.java, String::class.java)

                val meid = method.invoke(null, "ril.cdma.meid", "") as String
                if (!TextUtils.isEmpty(meid) && !"unknown".equals(meid)) {
                    return meid
                }
            } catch (e: NoSuchMethodException) {
                e.printStackTrace()
                Log.w("MEID", "getMEID error : " + e.message)
            } catch (e: IllegalAccessException) {
                e.printStackTrace()
                Log.w("MEID", "getMEID error : " + e.message)
            } catch (e: InvocationTargetException) {
                e.printStackTrace()
                Log.w("MEID", "getMEID error : " + e.message)
            } catch (e: ClassNotFoundException) {
                e.printStackTrace()
                Log.w("MEID", "getMEID error : " + e.message)
            }

            return ""
        }

        /**
         * 获取 MAC 地址
         */
        @JvmStatic
        fun getMacAdrresss(context: Context): String {

            var result: String = ""

            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
                var networkInterface = NetworkInterface.getByName("wlan0")
                var hardwareAddress = networkInterface.hardwareAddress

                val buffer = StringBuffer()
                for ((index, value) in hardwareAddress.withIndex()) {
                    buffer.append(value)
                    if (index < hardwareAddress.size - 1) buffer.append(":")
                }
                result = buffer.toString()
            } else {
                var wifiManager = context.applicationContext.getSystemService(Context.WIFI_SERVICE) as WifiManager
                result = wifiManager.connectionInfo.macAddress
            }
            return result
        }


        /**
         * 获取硬件序列号l
         */
        @JvmStatic
        fun getSerial(context: Context): String {

            var result: String = ""
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
                PermissionManage
                        .with(context)
                        .permission(Manifest.permission.READ_PHONE_STATE)
                        .onSuccess {
                            result = Build.getSerial()
                        }
                        .onDenial {}
                        .onDontShow {}
                        .run()
            } else {
                result = Build.SERIAL
            }
            return result
        }
    }

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