google对隐私管理越来越严格了,华为也出了个OAID来保护用户隐私。对于生成android设备唯一id一直没有个绝对完美的方案,只能说做到尽量唯一吧,这里做一下总结。
一、设备系统版本为Android Q
从 Android Q 开始,应用必须具有 READ_PRIVILEGED_PHONE_STATE
特许权限才能访问设备的不可重置标识符(包含 IMEI 和序列号)。许多用例不需要不可重置的设备标识符。如果您的应用没有该权限,但您仍尝试查询标识符的相关信息,则平台的响应会因目标 SDK 版本而异:
- 如果应用以 Android Q 为目标平台,则会发生
SecurityException
。 - 如果应用以 Android 9(API 级别 28)或更低版本为目标平台,则相应方法会返回
null
或占位符数据(如果应用具有READ_PHONE_STATE
权限)。否则,会发生SecurityException
。
注意:如果您的应用是设备所有者或配置文件所有者应用,那么即使您的应用以 Android Q 为目标平台,您也只需 READ_PHONE_STATE
权限即可访问不可重置的设备标识符。此外,如果您的应用具有特殊运营商权限,则无需任何权限即可访问这些标识符。
如果您的应用将不可重置的设备标识符用于广告跟踪或用户分析目的,请为这些特定用例创建 Android 广告 ID。要了解详情,请参阅唯一标识符的最佳做法。
应用市场合规检测越来越严格了,Android Q以上都用OAID吧
二、设备系统版本为Android P及其以下
1. google官方唯一标识符最佳做法
2. 友盟生成唯一id的方案
反编译了友盟统计analytics-6.1.4.jar,友盟生成唯一id的方案可以总结为:
- SDK_INT<23:imei>mac地址(直接api获取)>android_id>serial number
- SDK_INT=23:imei>mac地址(api获取,读取系统文件)>android_id>serial number
- SDK_INT>23:imei>serial number>android_id>mac地址(api获取,读取系统文件)
反编译的代码如下,稍微修改了下方法名
public class DeviceIdUtil {
private static FileReader fileReader;
private static BufferedReader bufferedReader;
private static String getDeviceUniqueId(Context paramContext) {
String str = "";
if (Build.VERSION.SDK_INT < 23) {
str = getDeviceId(paramContext);
if (TextUtils.isEmpty(str)) {
str = getMacAddressFromWifiManager(paramContext);
if (TextUtils.isEmpty(str)) {
str = Settings.Secure.getString(paramContext.getContentResolver(), "android_id");
if (TextUtils.isEmpty(str)) {
str = getSerial();
}
}
}
} else if (Build.VERSION.SDK_INT == 23) {
str = getDeviceId(paramContext);
if (TextUtils.isEmpty(str)) {
str = getMacAddressFromNetworkInterface();
if (TextUtils.isEmpty(str)) {
if (a.d) {//反编译看代码默认是true
str = getMacAddressFromFile();
} else {
str = getMacAddressFromWifiManager(paramContext);
}
}
if (TextUtils.isEmpty(str)) {
str = Settings.Secure.getString(paramContext.getContentResolver(), "android_id");
if (TextUtils.isEmpty(str)) {
str = getSerial();
}
}
}
} else {
str = getDeviceId(paramContext);
if (TextUtils.isEmpty(str)) {
str = getSerial();
if (TextUtils.isEmpty(str)) {
str = Settings.Secure.getString(paramContext.getContentResolver(), "android_id");
if (TextUtils.isEmpty(str)) {
str = getMacAddressFromNetworkInterface();
if (TextUtils.isEmpty(str)) {
str = getMacAddressFromWifiManager(paramContext);
}
}
}
}
}
return str;
}
private static String getMacAddressFromFile() {
try {
String[] arrayOfString = {"/sys/class/net/wlan0/address", "/sys/class/net/eth0/address", "/sys/devices/virtual/net/wlan0/address"};
for (byte b1 = 0; b1 < arrayOfString.length; b1++) {
try {
String str = a(arrayOfString[b1]);
if (str != null) {
return str;
}
} catch (Throwable throwable) {
}
}
} catch (Throwable throwable) {
}
return null;
}
private static String a(String paramString) {
String str = null;
try {
fileReader = new FileReader(paramString);
bufferedReader = null;
if (fileReader != null) {
try {
bufferedReader = new BufferedReader(fileReader, 1024);
str = bufferedReader.readLine();
} finally {
if (fileReader != null) {
try {
fileReader.close();
} catch (Throwable throwable) {
}
}
if (bufferedReader != null) {
try {
bufferedReader.close();
} catch (Throwable throwable) {
}
}
}
}
} catch (Throwable throwable) {
}
return str;
}
private static String getMacAddressFromNetworkInterface() {
try {
Enumeration enumeration = NetworkInterface.getNetworkInterfaces();
while (enumeration.hasMoreElements()) {
NetworkInterface networkInterface = (NetworkInterface) enumeration.nextElement();
if ("wlan0".equals(networkInterface.getName()) || "eth0".equals(networkInterface.getName())) {
byte[] arrayOfByte = networkInterface.getHardwareAddress();
if (arrayOfByte == null || arrayOfByte.length == 0) {
return null;
}
StringBuilder stringBuilder = new StringBuilder();
for (byte b1 : arrayOfByte) {
stringBuilder.append(String.format("%02X:", new Object[]{Byte.valueOf(b1)}));
}
if (stringBuilder.length() > 0) {
stringBuilder.deleteCharAt(stringBuilder.length() - 1);
}
return stringBuilder.toString().toLowerCase(Locale.getDefault());
}
}
} catch (Throwable throwable) {
}
return null;
}
private static String getSerial() {
String str = "";
if (Build.VERSION.SDK_INT >= 9 && Build.VERSION.SDK_INT < 26) {
str = Build.SERIAL;
} else if (Build.VERSION.SDK_INT >= 26) {
try {
Class clazz = Class.forName("android.os.Build");
Method method = clazz.getMethod("getSerial", new Class[0]);
str = (String) method.invoke(clazz, new Object[0]);
} catch (Throwable throwable) {
}
}
return str;
}
private static String getDeviceId(Context paramContext) {
String str = "";
TelephonyManager telephonyManager = (TelephonyManager) paramContext.getSystemService("phone");
if (telephonyManager != null) {
try {
if (a(paramContext, "android.permission.READ_PHONE_STATE")) {
if (Build.VERSION.SDK_INT > 26) {
Class clazz = Class.forName("android.telephony.TelephonyManager");
Method method = clazz.getMethod("getImei", new Class[]{Integer.class});
str = (String) method.invoke(telephonyManager, new Object[]{method, Integer.valueOf(0)});
if (TextUtils.isEmpty(str)) {
method = clazz.getMethod("getMeid", new Class[]{Integer.class});
str = (String) method.invoke(telephonyManager, new Object[]{method, Integer.valueOf(0)});
if (TextUtils.isEmpty(str)) {
str = telephonyManager.getDeviceId();
}
}
} else {
str = telephonyManager.getDeviceId();
}
}
} catch (Throwable throwable) {
str = "";
}
}
return str;
}
private static String getMacAddressFromWifiManager(Context paramContext) {
try {
WifiManager wifiManager = (WifiManager) paramContext.getSystemService("wifi");
if (a(paramContext, "android.permission.ACCESS_WIFI_STATE")) {
WifiInfo wifiInfo = wifiManager.getConnectionInfo();
return wifiInfo.getMacAddress();
}
return "";
} catch (Throwable throwable) {
return "";
}
}
}
3. 项目里目前采用的方案(唯一id+本地存储)
因为app首次安装启动就要上报唯一id,判断是否是新用户等等,这些操作很可能是在获取权限之前,所以参考友盟和搜来的方案,对唯一id的生成方案做了优化,去掉了mac地址的获取,新增了伪id的生成,加上了存储唯一id到本地。
方案:首次启动就去生成唯一id(优先级:imei>serial number>android_id>伪imei),并存储到SharePreference中。
伪imei可参考
String m_szDevIDShort = "35" + //we make this look like a valid IMEI
Build.BOARD.length()%10+ Build.BRAND.length()%10 +
Build.CPU_ABI.length()%10 + Build.DEVICE.length()%10 +
Build.DISPLAY.length()%10 + Build.HOST.length()%10 +
Build.ID.length()%10 + Build.MANUFACTURER.length()%10 +
Build.MODEL.length()%10 + Build.PRODUCT.length()%10 +
Build.TAGS.length()%10 + Build.TYPE.length()%10 +
Build.USER.length()%10 ; //13 digits
优点:无需关心权限,绝大部分手机都能生成的唯一id,存储到sp中保证了无论是否授予权限,唯一id都不会变化(下面这种情况例外)
缺点:当用户首次安装,启动,卸载后重装,手动到权限管理赋予android.permission.READ_PHONE_STATE权限,再启动,此时生成的唯一id可能会发生变化