Android唯一設備ID


設備ID,簡單來說就是一串符號(或者數字),映射現實中硬件設備。如果這些符號和設備是一一對應的,可稱之為“唯一設備ID(Unique Device Identifier)”

不幸的是,對於Android平台而言,沒有穩定的API可以讓開發者獲取到這樣的設備ID。

開發者通常會遇到這樣的困境:隨着項目的演進, 越來越多的地方需要用到設備ID;然而隨着Android版本的升級,獲取設備ID卻越來越難了。

加上Android平台碎片化的問題,獲取設備ID之路,可以說是步履維艱。

獲取設備標識的API屈指可數,而且都或多或少有一些問題。

 

IMEI

IMEI本該最理想的設備ID,具備唯一性,恢復出廠設置不會變化(真正的設備相關),可通過撥打*#06# 查詢手機的imei碼。

然而,獲取IMEI需要 READ_PHONE_STATE 權限,估計大家也知道這個權限有多麻煩了。

尤其是Android 6.0以后, 這類權限要動態申請,很多用戶可能會選擇拒絕授權。我們看到,有的APP不授權這個權限就無法使用, 這可能會降低用戶對APP的好感度。

而且,Android 10.0 將徹底禁止第三方應用獲取設備的IMEI(即使申請了 READ_PHONE_STATE 權限)。所以,如果是新APP,不建議用IMEI作為設備標識;

如果已經用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 "";
}

再往后說不准這種方法也行不通了,且用且珍惜~

另外,安卓9.0及以上版本默認使用了“隨機MAC地址”。進入“設置” -- “WLAN”,點擊進入對應的wifi進行關閉


ANDROID_ID

Android ID 是獲取門檻最低的,不需要任何權限,64bit 的取值范圍,唯一性算是很好的了。

但是不足之處也很明顯:

1、刷機、root、恢復出廠設置等會使得 Android ID 改變;
2、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

 

構建android包時,會生成最終GameActivity.java在如下目錄中(arm64包):Intermediate\Android\arm64\src\com\epicgames\ue4

 

 

 

參考

漫談唯一設備IDlink2

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

 

 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM