Android惟一設備ID

設備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

 

參考

漫談惟一設備IDlink2

Android設備惟一標識的獲取和構造

相關文章
相關標籤/搜索