以前也写过Android设备唯一标识码的文章,但是很浅显,罗列参数不同,也没给出我的方案,所以这里再写一次,欢迎大家讨论
手机 root 之后可以利用 Xposed框架 HOOK 修改很多参数,比如 DeviceID,详细可以看:
- 获取Android设备DeviceId与反Xposed Hook
为什么要获取设别识别码
这个参数是否台服务器要用的,用在哪呢,为的是处理同一账号同时在多台设备上登录的问题,特别上在电商 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 {
var meid = getMEID()
var imeis = getIMEI().toMutableList()
imeis.add(meid)
return imeis
}
private fun getIMEI(): List {
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
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 设备的万千独特性,非算法不行了
- 首先判断 SERIAL 硬件序列号,这是最能区分设备的,若是拿不到 SERIAL 再往下走
- 然后判断 Build.FINGERPRINT 设备型号,不同的型号自然不是同一个设备,Build.FINGERPRINT 一样再往下走
- 我会上传该设备的所有 IMEI、MEID 和账号捆绑,再传 DeviceID,服务端判断 DeviceID 在不在这个账号捆绑的 IMEI、MEID 里,不再就不是同一个设备,要是没有 IMEI、MEID 的话再往下走
----------------------------------------------分割线----------------------------------------------
到这里基本上手机设备都能判断是不是同一个设备了,剩下的就是一些山寨平板设备了
----------------------------------------------分割线----------------------------------------------
- 最后综合判断 MAC 地址和 androidID,只用 MAC 地址和 androidID 同时变化才算是不是同一台设备,虽然不能排除同一台设备 MAC 地址和 androidID 同时变化,但是相比 UUID、MD5 来说,错误T票的几率已经大大减少了
最后贴一下工具类
class DeviceIdUtils {
companion object {
/**
* 获取 MEID 和 IMEI
*/
@JvmStatic
fun getMEID2IMEI(): List {
var meid = getMEID()
var imeis = getIMEI().toMutableList()
imeis.add(meid)
return imeis
}
/**
* 获取 IMEI,双卡双待手机 IMEI 有2个
*/
@JvmStatic
fun getIMEI(): List {
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
}
}