google對隱私管理越來越嚴格了,華為也出了個OAID來保護用戶隱私。對于生成android設備唯一id一直沒有個絕對完美的方案,只能說做到盡量唯一吧,這里做一下總結(jié)。
一、設備系統(tǒng)版本為Android Q
從 Android Q 開始,應用必須具有 READ_PRIVILEGED_PHONE_STATE 特許權限才能訪問設備的不可重置標識符(包含 IMEI 和序列號)。許多用例不需要不可重置的設備標識符。如果您的應用沒有該權限,但您仍嘗試查詢標識符的相關信息,則平臺的響應會因目標 SDK 版本而異:
- 如果應用以 Android Q 為目標平臺,則會發(fā)生
SecurityException。 - 如果應用以 Android 9(API 級別 28)或更低版本為目標平臺,則相應方法會返回
null或占位符數(shù)據(jù)(如果應用具有READ_PHONE_STATE權限)。否則,會發(fā)生SecurityException。
注意:如果您的應用是設備所有者或配置文件所有者應用,那么即使您的應用以 Android Q 為目標平臺,您也只需 READ_PHONE_STATE 權限即可訪問不可重置的設備標識符。此外,如果您的應用具有特殊運營商權限,則無需任何權限即可訪問這些標識符。
如果您的應用將不可重置的設備標識符用于廣告跟蹤或用戶分析目的,請為這些特定用例創(chuàng)建 Android 廣告 ID。要了解詳情,請參閱唯一標識符的最佳做法。
應用市場合規(guī)檢測越來越嚴格了,Android Q以上都用OAID吧
二、設備系統(tǒng)版本為Android P及其以下
1. google官方唯一標識符最佳做法
2. 友盟生成唯一id的方案
反編譯了友盟統(tǒng)計analytics-6.1.4.jar,友盟生成唯一id的方案可以總結(jié)為:
- SDK_INT<23:imei>mac地址(直接api獲取)>android_id>serial number
- SDK_INT=23:imei>mac地址(api獲取,讀取系統(tǒng)文件)>android_id>serial number
- SDK_INT>23:imei>serial number>android_id>mac地址(api獲取,讀取系統(tǒng)文件)
反編譯的代碼如下,稍微修改了下方法名
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的生成方案做了優(yōu)化,去掉了mac地址的獲取,新增了偽id的生成,加上了存儲唯一id到本地。
方案:首次啟動就去生成唯一id(優(yōu)先級: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
優(yōu)點:無需關心權限,絕大部分手機都能生成的唯一id,存儲到sp中保證了無論是否授予權限,唯一id都不會變化(下面這種情況例外)
缺點:當用戶首次安裝,啟動,卸載后重裝,手動到權限管理賦予android.permission.READ_PHONE_STATE權限,再啟動,此時生成的唯一id可能會發(fā)生變化