鉴于国家对设备标识相关越来越规范,以及Android 10以及更高版本的系统限制。
对于游戏发行SDK,特别是广告分发相关的SDK开发带来了一定的难度,八两也对 Android 设备标识相关的参数进行了一些整理,标题说是分析,其实没什么技术含量,但是比较全。
至于如何应对越来越严苛的系统限制, 后续八两有时间了另开篇幅唠唠
Android ID:
标识说明: Android ID 是随着安卓设备出厂生成的唯一设备ID
稳定性: 可以认为在设备生命周期内不会改变,除非恢复出厂设置或者用户刷ROOM.且几乎所有安卓设备都可稳定获取.
唯一性: 每台设备出厂时随机生成,有 16 位随机字符串,每位取值10位数字加26位小写字母,理论上有 36的16次方种组合,具有较好的唯一性
额外权限: 无需申请任何权限
版本变化: Android 8 以及以上版本,会根据当前设备用户,APK安卓签名,设备,确定作用域,任一不同作用域均会获取到不同的Android ID. 即 Android ID 不再能标识唯一设备
Java实现:
public static String getAndroidId(Context context) {
String id = Settings.System.getString(context.getContentResolver(), Settings.Secure.ANDROID_ID);
return TextUtils.isEmpty(id) ? "" : id;
}
OAID
标识说明: 移动安全联盟倡导的通用ID, 可以被用户手动重置,无需任何权限即可获取。美中不足是OAID是异步获取。
稳定性: 在设备厂商按照标准的情况下,OAID 具有很好的稳定性,但非正常情况下,如虚拟机等,可伪造篡改OAID
唯一性: OAID 由 32 位字符组成,不易出现重复
额外权限: 无,但在部分设备上,OAID是异步获取。
版本变化: 根据厂商不同而不同,通常 Android 10.0 以后,才能获取OAID. 且并非所有厂商均支持 OAID 获取
获取方式: 需申请集成移动安全联盟的SDK,或根据设备自己实现,不赘述
IMEI
标识说明: International Mobile Equipment Identity, 国际移动设备识别码,共有15位数字,前6位(TAC)是型號核准號碼,代表手機類型。接著2位(FAC)是最後裝配號,代表產地。後6位(SNR)是串號,代表生產順序號。最後1位(SP)一般為0,是檢驗碼,備用
稳定性: 在设备厂商按照标准的情况下,IMEI 具有很好的稳定性,但非正常情况下,如虚拟机等,可伪造篡改IMEI
唯一性: IMEI 由 15 位数字组成,不易出现重复
额外权限: 需要申明 android.permission.READ_PHONE_STATE
权限
版本变化: Android 6.0 以后, android.permission.READ_PHONE_STATE
需要用户手动授权, Android 10 以后不再支持获取 IMEI, 尝试获取均返回空
Java实现:
@SuppressLint("MissingPermission")
public static String getIMEI(Context context) {
try {
if (PermissionUtils.hasAndroidPermission(context, Manifest.permission.READ_PHONE_STATE)) {
TelephonyManager telephonyManager = (TelephonyManager) context.getSystemService(Activity.TELEPHONY_SERVICE);
if (telephonyManager != null) return TextUtils.isEmpty(telephonyManager.getDeviceId()) ? "" : telephonyManager.getDeviceId();
} else {
Logger.warning(Logger.TOOLS_TAG, "Can not get IMEI since no READ_PHONE_STATE permission");
}
} catch (Exception e) {
Logger.warning(Logger.TOOLS_TAG, "Can not get IMEI because of Exception, this is expected in Android 10 and above ", e);
}
return "";
}
Mac 地址
标识说明: 设备唯一网络标识,也叫网卡地址,长度 48 位,通常表示成类似 00-16-EA-AE-3C-40
稳定性: 因为是唯一网络标识,可基本认为稳定,不过若设备从未联网,以及部分情况下产商的限制,可能出现获取不到或者获取到某固定值的情况
唯一性: 48 位二进制数,理论上有 2 ^ 48(281474976710656) 次方种可能
额外权限: 需要声明 android.permission.ACCESS_WIFI_STATE
和 android.permission.INTERNET
权限
版本变化: Android 6.0 以上获取方式略有修改, Android 11 以及以上,每次设备联网都会生成新的随机 Mac 地址,故其不再具有唯一性
Java实现:
public static String getMacAddress(Context context) {
String mac = "";
if (Util.getCurrentAndroidVersion() >= Build.VERSION_CODES.M) {
try {
Enumeration interfaces = NetworkInterface.getNetworkInterfaces();
if(interfaces == null) return mac;
List all = Collections.list(interfaces);
for (NetworkInterface nif : all) {
if (nif.getName().equalsIgnoreCase("wlan0")) {
byte[] macBytes = nif.getHardwareAddress();
if (macBytes == null) {
break;
}
StringBuilder sb = new StringBuilder();
for (byte b : macBytes) {
sb.append(String.format("%02X:", b));
}
if (sb.length() > 0) sb.deleteCharAt(sb.length() - 1);
mac = sb.toString();
break;
}
}
} catch (SocketException e) {
Logger.warning(Logger.ERR_TAG, "Get Mac address via NetworkInterface exception", e);
}
} else {
WifiManager wifiManager = (WifiManager) context.getApplicationContext().getSystemService(Context.WIFI_SERVICE);
if(wifiManager != null) {
WifiInfo wifiInfo = wifiManager.getConnectionInfo();
if(wifiInfo != null) mac = wifiInfo.getMacAddress();
}
}
return (TextUtils.isEmpty(mac) || "02:00:00:00:00:00".equals(mac)) ? "" : mac;
}
Android 序列号
标识说明: Android 序列号,Android 硬件序列号,与硬件信息强相关
稳定性: Android 序列号与硬件信息强相关,硬件设备无修改则此值不会发生变化
唯一性: 由于各硬件厂商有自己的序列号生成规则,故不同厂商之间,有序列号重复的可能,但重复可能性很低。序列号本身有 16 位,每位是26个字母中的随机值
额外权限: Android 7.1 以前无权限,7.0以后需要申明 android.permission.READ_PHONE_STATE
权限
版本变化: Android 7.1 以前可直接通过 Build.SERIAL
获取, Android 10 以前可通过申请 android.permission.READ_PHONE_STATE
权限以反射方式获取, Android 10 以后需要 android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE
权限,理论上只有系统应用或者设备厂商才能获取
Java实现:
/**
* Trying to get SERIAL number in different way.
* As test, this method work until Android 9 and below.
* @return Serial Number , return "" if unable to get Serial Number or Serial Number is "unknown"
*/
public static String getSerial() {
if(!TextUtils.isEmpty(Build.SERIAL) && !"unknown".equalsIgnoreCase(Build.SERIAL)) {
return Build.SERIAL;
}
try {
Class> claz = Class.forName("android.os.Build");
Method method = claz.getMethod("getSerial");
String serial = (String) method.invoke(null);
return TextUtils.isEmpty(serial) || "unknown".equalsIgnoreCase(serial) ? "" : serial;
} catch (Exception e) {
Logger.warning(Logger.EXP_TAG, "Get serial Reflection exception", e);
return "";
}
}
出厂时间
标识说明: 记录在 Android 设备上的出厂时间信息
稳定性: 由于每台 Android 设备均有固定的出厂时间,故此值通常情况下不会变化,除非以下情况:恢复出厂设置,刷 room。而实际由于各个厂商的实现不同,部分情况会无法获取出厂时间。
唯一性: 出厂时间理论上可精确到毫秒,实际中大多数只精确到秒,然而每秒出厂的手机有限,依旧有较好的唯一性,单独使用不建议,配合其他信息使用也可有较好的唯一性
额外权限: 无需申请额外权限
版本变化: Android 10 前是否能获取到与设备型号、有较大的关系,Android 11 以后由于权限限制,目前实现也无法获取,需调研其他获取方式
Java实现:
/**
* Trying to get device first factory time, this "factory time" will not change until factory reset.
* @return Factory Access Time with digital, record in /cache if could get it, otherwise return "" if any exception happen.
* e.g: 202007141154500800
*/
public static String getFactoryTime() {
StringBuilder result = new StringBuilder();
ProcessBuilder cmd;
try {
String[] args = {"/system/bin/stat", "/cache"};
cmd = new ProcessBuilder(args);
Process process = cmd.start();
InputStream in = process.getInputStream();
byte[] re = new byte[24];
while (in.read(re) != -1) {
result.append(new String(re));
}
in.close();
} catch (Exception e) {
Logger.warning("Exception happen to get factory time ", e);
}
String[] lines = result.toString().split("\n");
StringBuilder time = new StringBuilder();
for (String line: lines) {
if(line.contains("Access:") && !line.contains("Uid:")) {
for (char s: line.toCharArray()) {
if(Character.isDigit(s)){
time.append(s);
}
}
}
}
return time.toString().equals("19700101080000000000000") ? "" : time.toString().replace("000000000", "");
}
Media Drm ID
标识说明: MediaDrm 是数字音频版权框架,其被安卓在安卓架构中原生支持。Media Drm ID 是数字音频用于追踪,保护版权所需的唯一设备ID.
稳定性: 其在框架中被原生支持,且用来追踪数字音频版权,故具有较好的稳定性,通常情况不会变化,且由框架加密,不易伪造和篡改。目前尚未测试刷机,恢复出厂设置等场景
唯一性: 根据不同数字版权提供商算法,可获取不同的 MediaDrm ID, 但都基于 64 位 16 进制数字计算而来,故有 16^64次方种组合,不易重复
额外权限:无
版本变化:Android 4.3 以上才可正常获取,Android 8 以及以后,Media Drm ID 会根据底层包名不同而不同,与安卓签名无关
Java实现:
加密 WIDEVINE 获取方式
@RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN_MR2)
public static String getWidevineId() {
String sRet = "";
UUID WIDEVINE_UUID = new UUID(-0x121074568629b532L, -0x5c37d8232ae2de13L);
MediaDrm mediaDrm = null;
try {
mediaDrm = new MediaDrm(WIDEVINE_UUID);
byte[] widevineId = mediaDrm.getPropertyByteArray(MediaDrm.PROPERTY_DEVICE_UNIQUE_ID);
MessageDigest md = MessageDigest.getInstance("SHA-256");
md.update(widevineId);
sRet = bytesToHex(md.digest()); //we convert byte[] to hex for our purposes
} catch (Exception e) {
//WIDEVINE is not available
Logger.error("getWidevineSN.WIDEVINE is not available", e);
} finally {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
if(null!=mediaDrm) {
mediaDrm.close();
}
} else {
if(null!=mediaDrm) {
mediaDrm.release();
}
}
}
return sRet;
}
private static final char[] HEX_ARRAY = "0123456789ABCDEF".toCharArray();
public static String bytesToHex(byte[] bytes) {
char[] hexChars = new char[bytes.length * 2];
for (int j = 0; j < bytes.length; j++) {
int v = bytes[j] & 0xFF;
hexChars[j * 2] = HEX_ARRAY[v >>> 4];
hexChars[j * 2 + 1] = HEX_ARRAY[v & 0x0F];
}
return new String(hexChars);
}
WIDEVINE ID 获取方式,返回 bytes 数组
@RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN_MR2)
public static String getWidevineBytes() {
UUID wideVineUuid = new UUID(-0x121074568629b532L, -0x5c37d8232ae2de13L);
try {
MediaDrm wvDrm = new MediaDrm(wideVineUuid);
byte[] wideVineId = wvDrm.getPropertyByteArray(MediaDrm.PROPERTY_DEVICE_UNIQUE_ID);
return Arrays.toString(wideVineId);
} catch (Exception e) {
// Inspect exception
return null;
}
// Close resources with close() or release() depending on platform API
// Use ARM on Android P platform or higher, where MediaDrm has the close() method
}
IP
标识说明: 设备IP地址
稳定性: 较无稳定性,随着用户网络条件与物理位置的不同,IP地址也会变化
唯一性: 相同地区的 IP 可能相同
额外权限: 需要声明 android.permission.ACCESS_WIFI_STATE
和 android.permission.INTERNET
权限
版本变化: 无
Java实现:
/**
*
* As it said, simply get IP address,
*
* First it will try to get IP address from {@link NetworkInterface}
*
*
* Then it will try to get IP address from {@link android.net.wifi.WifiInfo#getIpAddress()}
*
*
* Return empty string if any exception happen.
*
*
*
* @param context activity context
* @return IP Address, support both IPV4 & IPV6 address, support mobile network & WIFI
* "" if exception happen
*/
public static String getIpAddress(Context context) {
Enumeration en = null;
try {
en = NetworkInterface.getNetworkInterfaces();
} catch (SocketException e) {
Logger.warning(Logger.ERR_TAG, "Create NetworkInterface occur Exception", e);
}
if (en != null) {
while (en.hasMoreElements()) {
NetworkInterface intf = en.nextElement();
Enumeration enumIpAddr = intf.getInetAddresses();
while (enumIpAddr.hasMoreElements()) {
InetAddress inetAddress = enumIpAddr.nextElement();
if (!inetAddress.isLoopbackAddress() && !inetAddress.isAnyLocalAddress() && !inetAddress.isLinkLocalAddress()) {
return inetAddress.getHostAddress().toUpperCase(Locale.US);
}
}
}
}
// Can not get IP address in network interface, use wifi ip address
try {
WifiManager manager = (WifiManager) context.getApplicationContext().getSystemService(Context.WIFI_SERVICE);
if(manager == null) return "";
int ipAddress = manager.getConnectionInfo().getIpAddress();
return String.format(Locale.getDefault(), "%d.%d.%d.%d", (ipAddress & 0xff), (ipAddress >> 8 & 0xff), (ipAddress >> 16 & 0xff), (ipAddress >> 24 & 0xff));
} catch (Exception e) {
// Exception could happen if wifi doesn't connect
Logger.warning(Logger.ERR_TAG, "Get IP address from wifi manager exception ", e);
return "";
}
}
UUID
标识说明: 随机序列码
稳定性: 无任何稳定性,每次均生成不同值
唯一性: UUID由32位 16 进制字符构成,每个字符可有16种可能,有较好唯一性
额外权限: 无
版本变化: 无
Java实现:
public static String getUUID() {
return UUID.randomUUID().toString();
}
其他调研参数
以下参数,无特别说明,均无版本变化,无需申请权限,但有较高局限性,绝大多数均无法判别唯一设备,但可加入唯一设备标识的计算
参数获取基础代码:
private static String getProperty(String prop) {
try {
Class> cls = Class.forName("android.os.SystemProperties");
Method get = cls.getDeclaredMethod("get", new Class>[]{String.class, String.class});
String value = (String) get.invoke(null, new Object[]{prop, ""});
return TextUtils.isEmpty(value) ? "" : value;
} catch (Exception e) {
Logger.warning(Logger.EXP_TAG, "Get property %s Reflection exception", prop, e);
return "";
}
}
开机时间
标识说明: 系统运行到现在的开机时间
稳定性: 与用户习惯强相关,取决于用户开关机频率
唯一性: Unix 时间戳,精确到毫秒,有一定的唯一性
Java实现:
public static String getFirstBoot() {
return getProperty("ro.runtime.firstboot");
}
品牌
标识说明: 硬件手机品牌
稳定性: 理论上一定稳定,均可获得定值
唯一性: 可筛选区分不同品牌的设备,然同一品牌设备众多,故几乎无唯一性
Java实现:
public static String getDeviceBrand() {
return TextUtils.isEmpty(Build.BRAND) ? "unknown" : Build.BRAND;
}
型号
标识说明: 与品牌略有不同,是其品牌下的细分型号。
public static String getDeviceModel() {
return TextUtils.isEmpty(Build.MODEL) ? "unknown" : Build.MODEL;
}
制造厂商
标识说明: 与品牌类似,返回设备具体制造厂商,通常此值与品牌相同,若出现手机代工等情况,其值可能不同
public static String getDeviceManufacture() {
return TextUtils.isEmpty(Build.MANUFACTURER) ? "unknown " : Build.MANUFACTURER;
}