設備ID,簡單來講就是一串符號(或者數字),映射現實中硬件設備。若是這些符號和設備是一一對應的,可稱之爲「惟一設備ID(Unique Device Identifier)」html
不幸的是,對於Android平臺而言,沒有穩定的API可讓開發者獲取到這樣的設備ID。java
開發者一般會遇到這樣的困境:隨着項目的演進, 愈來愈多的地方須要用到設備ID;然而隨着Android版本的升級,獲取設備ID卻愈來愈難了。android
加上Android平臺碎片化的問題,獲取設備ID之路,能夠說是寸步難行。算法
獲取設備標識的API屈指可數,並且都或多或少有一些問題。數組
IMEIapp
IMEI本該最理想的設備ID,具有惟一性,恢復出廠設置不會變化(真正的設備相關),可經過撥打*#06# 查詢手機的imei碼。post
然而,獲取IMEI須要 READ_PHONE_STATE 權限,估計你們也知道這個權限有多麻煩了。ui
尤爲是Android 6.0之後, 這類權限要動態申請,不少用戶可能會選擇拒絕受權。咱們看到,有的APP不受權這個權限就沒法使用, 這可能會下降用戶對APP的好感度。spa
並且,Android 10.0 將完全禁止第三方應用獲取設備的IMEI(即便申請了 READ_PHONE_STATE 權限)。因此,若是是新APP,不建議用IMEI做爲設備標識;code
若是已經用IMEI做爲標識,要趕忙作兼容工做了,尤爲是作新設備標識和IMEI的映射。
設備序列號
在Android 7.1或更早系統(SDK<=25),java可經過android.os.Build.SERIAL得到,由廠商提供。
若是廠商比較規範的話,設備序列號+Build.MANUFACTURER應該能惟一標識設備。但現實是並不是全部廠商都按規範來,尤爲是早期的設備。
最致命的是,Android 8.0及 以上(SDK>=26),android.os.Build.SERIAL 總返回 「unknown」;若要獲取序列號,可調用Build.getSerial() ,可是須要申請 READ_PHONE_STATE 權限。
到了Android 10.0(SDK>=29)以上,則和IMEI同樣,也被禁止獲取了。
在UE4中,使用C++代碼實現以下:
int GetPublicStaticInt(const char *className, const char *fieldName) { #if PLATFORM_ANDROID JNIEnv* env = FAndroidApplication::GetJavaEnv(); if (env != NULL) { jclass clazz = env->FindClass(className); if (clazz != nullptr) { jfieldID fid = env->GetStaticFieldID(clazz, fieldName, "I"); if (fid != nullptr) { return env->GetStaticIntField(clazz, fid); } } } #endif return 0; } FString GetPublicStaticString(const char *className, const char *fieldName) { #if PLATFORM_ANDROID JNIEnv* env = FAndroidApplication::GetJavaEnv(); if (env != NULL) { jclass clazz = env->FindClass(className); if (clazz != nullptr) { jfieldID fid = env->GetStaticFieldID(clazz, fieldName, "Ljava/lang/String;"); if (fid != nullptr) { jstring content = (jstring)env->GetStaticObjectField(clazz, fid); return ANSI_TO_TCHAR(env->GetStringUTFChars(content, 0)); } } } #endif return FString(); } FString GetStaticMethodNoParametersRetString(const char *className, const char *fieldName) { JNIEnv* env = FAndroidApplication::GetJavaEnv(); if (env != NULL) { jclass clazz = env->FindClass(className); if (clazz != nullptr) { // ()中爲參數的類型列表,爲空表示沒有參數 // Ljava/lang/String;爲返回值類型 jmethodID fid = (env)->GetStaticMethodID(clazz, "fieldName", "()Ljava/lang/String;"); if (fid != NULL) { jstring content = (jstring)((Env)->CallStaticObjectMethod(clazz, fid)); serial = ANSI_TO_TCHAR(env->GetStringUTFChars(content, 0)); } } } } FString serial = TEXT("") int sdk = GetPublicStaticInt("android/os/Build$VERSION", "SDK_INT"); if (sdk >= 29 ) // Android Q(>= SDK 29) { } else if (sdk >= 26) // Android 8 and later (>= SDK 26) { serial = GetStaticMethodNoParametersRetString("android/os/Build", "getSerial"); } else //Android 7.1 and earlier(<= SDK 25) { serial = GetPublicStaticString("android/os/Build", "SERIAL"); }
整體來講,設備序列號有點雞肋:食之無味,棄之惋惜。
MAC地址
大多android設備都有wifi模塊,所以,wifi模塊的MAC地址就能夠做爲設備標識。基於隱私考慮,官方不建議獲取
獲取MAC地址也是愈來愈困難了,Android 6.0之後經過 WifiManager 獲取到的mac將是固定的:02:00:00:00:00:00
7.0以後讀取 /sys/class/net/wlan0/address 也獲取不到了(小米6)。
現在只剩下面這種方法能夠獲取(沒有開啓wifi也能夠獲取到):
public static String getWifiMac() { try { Enumeration<NetworkInterface> enumeration = NetworkInterface.getNetworkInterfaces(); if (enumeration == null) { return ""; } while (enumeration.hasMoreElements()) { NetworkInterface netInterface = enumeration.nextElement(); if (netInterface.getName().equals("wlan0")) { return formatMac(netInterface.getHardwareAddress()); } } } catch (Exception e) { Log.e("tag", e.getMessage(), e); } return ""; }
再日後說不許這種方法也行不通了,且用且珍惜~
ANDROID_ID
Android ID 是獲取門檻最低的,不須要任何權限,64bit 的取值範圍,惟一性算是很好的了。
可是不足之處也很明顯:
一、刷機、root、恢復出廠設置等會使得 Android ID 改變;
二、Android 8.0以後,Android ID的規則發生了變化:
對於升級到8.0以前安裝的應用,ANDROID_ID會保持不變。若是卸載後從新安裝的話,ANDROID_ID將會改變。
對於安裝在8.0系統的應用來講,ANDROID_ID根據應用簽名和用戶的不一樣而不一樣。ANDROID_ID的惟一決定於應用簽名、用戶和設備三者的組合。
兩個規則致使的結果就是:
第一,若是用戶安裝APP設備是8.0如下,後來卸載了,升級到8.0以後又重裝了應用,Android ID不同;
第二,不一樣簽名的APP,獲取到的Android ID不同。
其中第二點可能對於廣告聯盟之類的有所影響(若是彼此是用Android ID對比數據的話),因此Google文檔中說「請使用Advertising ID」,
不過你們都知道,Google的服務在國內用不了。
對Android ID作了約束,對隱私保護起到必定做用,而且用來作APP本身的活躍統計也仍是沒有問題的。
用硬件信息拼湊ID
優勢是不須要額外權限,缺點是惟一性不能百分百確保
以下爲一臺小米10的硬件相關信息
BOARD: umi BRAND: Xiaomi DEVICE: umi DISPLAY: QKQ1.191117.002 test-keys HOST: c5-miui-ota-bd074.bj ID: QKQ1.191117.002 MANUFACTURER: Xiaomi MODEL: Mi 10 PRODUCT: umi TAGS: release-keys TYPE: user USER: builder
java代碼實現以下:
import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; public class MD5Tool { private final static String[] hexArray = {"0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "a", "b", "c", "d", "e", "f"}; /*** * 獲取指定的字符串的MD5 */ public static String CalcMD5(String originString) { try { //建立具備MD5算法的信息摘要 MessageDigest md = MessageDigest.getInstance("MD5"); //使用指定的字節數組對摘要進行最後更新,而後完成摘要計算 byte[] bytes = md.digest(originString.getBytes()); //將獲得的字節數組變成字符串返回 String s = byteArrayToHex(bytes); return s.toLowerCase(); } catch (NoSuchAlgorithmException e) { e.printStackTrace(); } return null; } /** * 將字節數組轉換成十六進制,並以字符串的形式返回 * 128位是指二進制位。二進制太長,因此通常都改寫成16進制, * 每一位16進制數能夠代替4位二進制數,因此128位二進制數寫成16進制就變成了128/4=32位。 */ private static String byteArrayToHex(byte[] b){ StringBuffer sb = new StringBuffer(); for (int i = 0; i < b.length; i++) { sb.append(byteToHex(b[i])); } return sb.toString(); } /** * 將一個字節轉換成十六進制,並以字符串的形式返回 */ public static String byteToHex(byte b) { int n = b; if (n < 0) n = n + 256; int d1 = n / 16; int d2 = n % 16; return hexArray[d1]+hexArray[d2]; } } String hardwareInfo = android.os.Build.BOARD + android.os.Build.BRAND + android.os.Build.DEVICE + android.os.Build.DISPLAY + android.os.Build.HOST + android.os.Build.ID + android.os.Build.MANUFACTURER + android.os.Build.MODEL + android.os.Build.PRODUCT + android.os.Build.TAGS + android.os.Build.TYPE +android.os. Build.USER; string md5 = MD5Tool.CalcMD5(hardwareInfo);
在UE4中,使用C++代碼實現以下:
FString board = GetPublicStaticString("android/os/Build", "BOARD"); FString brand = GetPublicStaticString("android/os/Build", "BRAND"); FString device = GetPublicStaticString("android/os/Build", "DEVICE"); FString display = GetPublicStaticString("android/os/Build", "DISPLAY"); FString host = GetPublicStaticString("android/os/Build", "HOST"); FString id = GetPublicStaticString("android/os/Build", "ID"); FString manufacturer = GetPublicStaticString("android/os/Build", "MANUFACTURER"); FString model = GetPublicStaticString("android/os/Build", "MODEL"); FString product = GetPublicStaticString("android/os/Build", "PRODUCT"); FString tags = GetPublicStaticString("android/os/Build", "TAGS"); FString type = GetPublicStaticString("android/os/Build", "TYPE"); FString user = GetPublicStaticString("android/os/Build", "USER"); FString hardwareInfo = board + brand + device + display + host + id + manufacturer + model + product + tags + type + user; FString md5 = FMD5::HashAnsiString(*hardwareInfo)
UE4引擎java代碼所在目錄:UnrealEngine\Engine\Build\Android\Java\src\com\epicgames\ue4
參考