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设备惟一标识的获取和构造

相关文章
相关标签/搜索