4-3-1-如何确定Android设备唯一识别码

[TOC]

应用设备唯一识别码的解决方案

设备唯一识别码.png

唯一标识必须满足两个特性才能完美解决定位唯一设备的问题,但这个问题的解决却注定只能极限接近完美

  • 唯一性:标识必须在所有使用该应用的设备上保持唯一性
  • 不变性:标识必须在同一设备上保持不变

方向一:使用硬件标识

硬件标识实际上在硬件生产之时就被要求满足这两个特性(依然有人工生产的不确定性),但标识的获取趋于困难性,使得使用硬件标识作为唯一识别码的方案所能使用的范围越来越狭窄,不能作为全局方案使用。

1. 使用 DEVICE_ID

TelephonyManager tm = (TelephonyManager) getSystemService(TELEPHONY_SERVICE);
String deviceId = tm.getDeviceId();

2. 使用 ANDROID_ID

String androidId = Settings.Secure.getString(getContentResolver(), Settings.Secure.ANDROID_ID);

3. 使用 MAC ADDRESS

通过获取蓝牙或wifi的Mac地址 作为唯一识别号

wifiManager = ((WifiManager) getApplicationContext().getSystemService(Context.WIFI_SERVICE));
String macAddress = wifiManager.getConnectionInfo().getMacAddress();

4. 使用 SERIAL NUMBER

通过 android.os.Build.SERIAL来获取

5. 硬件标识的优势与局限性

优势:几乎完美满足唯一性与不变性

劣势:

硬件标识 局限
DEVICE_ID - 适用 Android9以下设备,但需要申请Manifest.permission.READ_PHONE_STATE权限
- 非手机设备不能使用: 如果只带有Wifi的设备或者音乐播放器没有通话的硬件功能的话就没有这个DEVICE_ID
- 有bug:在少数的一些手机设备上,该实现有漏洞,会返回垃圾,如:zeros或者asterisks的产品
- Android 10 设备上即使授予权限也会报错Process: com.sj.d_1_adaptiveversion, PID: 8768 java.lang.SecurityException: getUniqueDeviceId: The user 10285 does not meet the requirements to access device identifiers.
ANDROID_ID - 厂商定制系统的Bug: 不同的设备可能会产生相同的ANDROID_ID:9774d56d682e549c;有些设备返回的值为null;
适配局限(基本不考虑):在Android <=2.1 or Android >=2.3的版本是可靠、稳定的,但在2.2的版本并不是100%可靠的
设备差异(不考虑):对于CDMA设备,ANDROID_ID和TelephonyManager.getDeviceId() 返回相同的值
MAC ADDRESS - 硬件限制:并不是所有的设备都有WiFi和蓝牙硬件
- 获取的限制:如果WiFi没有打开过,是无法获取其Mac地址的;而蓝牙是只有在打开的时候才能获取到其Mac地址
- Android 6.0(API 级别 23)到 Android 9(API 级别 28)中,无法通过第三方 API 使用 Wi-Fi 和蓝牙等本地设备 Mac 地址。WifiInfo.getMacAddress() 方法和 BluetoothAdapter.getDefaultAdapter().getAddress() 方法都返回 02:00:00:00:00:00
SERIAL NUMBER 经常会返回Unknown

方向二 使用UUID

这也是官方推荐的生成的唯一标识码生成方式,有一点不同的时,官方方案(在这里)将生成的UUID存在应用内部存储当中,APP的卸载重装会导致发生更改;在实际使用当中我们可以存储到外部存储,除非人为的删除、损坏,这样它的不变性也得到了保障,而它的唯一性则由UUID来保证。

UUID的实现原理简析:
Wiki解释:通用唯一识别码(英语:Universally Unique Identifier,缩写:UUID)是用于计算机体系中以识别信息数目的一个128位标识符,还有相关的术语:全局唯一标识符(GUID)。根据标准方法生成,不依赖中央机构的注册和分配,UUID具有唯一性,这与其他大多数编号方案不同。重复UUID码概率接近零,可以忽略不计

组成: 8-4-4-4-12 xxxxxxxx-xxxx-Mxxx-Nxxx-xxxxxxxxxxxx M表示 UUID 版本,数字 N的一至三个最高有效位表示 UUID 变体

UUID根据版本不同,依赖的组成有不同的变种,

基于时间的UUID版本是通过计算当前时间戳、随机数和机器MAC地址得到 。UUID的核心算法保证了即使在多处理器同时生成的UUID重复性为0,因为他们所在的时间、空间(节点:通常是MAC地址)必然不一致。

由于在算法中使用了MAC地址,这个版本的UUID可以保证在全球范围的唯一性。但与此同时,使用MAC地址会带来安全性问题,这就是这个版本UUID受到批评的地方。如果应用只是在局域网中使用,也可以使用退化的算法,以IP地址来代替MAC地址--Java的UUID往往是这样实现的(当然也考虑了获取MAC的难度)。

String uniqueID = UUID.randomUUID().toString();

趋于完美的方案

尽可能的获取硬件标识来满足两个特性,在有限制或其他因素的条件下,尽可能满足不变性,将UUID存储在外部环境来进行读写。

方案思路
尽可能的获取硬件标识
硬件标识为空,进行UUID的生成、存储

方案说明:

  1. 需要在使用之前拿到设备信息权限(没有会导致DeviceID不可取,但仍然可用),外部存储读写权限(必须,否则不可用)
  2. 最好在Application中使用,唯一标识在app与服务器直接交互很常用,放在全局统一的地方方便管理使用

还有一种方案是拿到设备的某些唯一信息,生成特定的UUID,这样保持不变就可以跳过存储,但是既然拿到了唯一信息,那为啥还要生成UUID呢?

public class UniqueIDUtils {
    private static final String TAG = "UniqueIDUtils";
    private static String uniqueID;
    private static String uniqueKey = "unique_id";
    private static String uniqueIDDirPath = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOCUMENTS).getAbsolutePath();
    private static String uniqueIDFile = "unique.txt";


    public static String getUniqueID(Context context) {
        //三步读取:内存中,存储的SP表中,外部存储文件中
        if (!TextUtils.isEmpty(uniqueID)) {
            Log.e(TAG, "getUniqueID: 内存中获取" + uniqueID);
            return uniqueID;
        }
        uniqueID = PreferenceManager.getDefaultSharedPreferences(context).getString(uniqueKey, "");
        if (!TextUtils.isEmpty(uniqueID)) {
            Log.e(TAG, "getUniqueID: SP中获取" + uniqueID);
            return uniqueID;
        }
        readUniqueFile(context);
        if (!TextUtils.isEmpty(uniqueID)) {
            Log.e(TAG, "getUniqueID: 外部存储中获取" + uniqueID);
            return uniqueID;
        }
        //两步创建:硬件获取;自行生成与存储
        getDeviceID(context);
        getAndroidID(context);
        getSNID();
        createUniqueID(context);
        PreferenceManager.getDefaultSharedPreferences(context).edit().putString(uniqueKey, uniqueID);
        return uniqueID;
    }

    @SuppressLint("MissingPermission")
    private static void getDeviceID(Context context) {
        if (!TextUtils.isEmpty(uniqueID)) {
            return;
        }
        if (Build.VERSION.SDK_INT > Build.VERSION_CODES.O_MR1) {
            return;
        }
        String deviceId = null;
        try {
            deviceId = ((TelephonyManager) context.getSystemService(TELEPHONY_SERVICE)).getDeviceId();
            if (TextUtils.isEmpty(deviceId)) {
                return;
            }
        } catch (Exception e) {
            e.printStackTrace();
            return;
        }
        uniqueID = deviceId;
        Log.e(TAG, "getUniqueID: DeviceId获取成功" + uniqueID);
    }

    private static void getAndroidID(Context context) {
        if (!TextUtils.isEmpty(uniqueID)) {
            return;
        }
        String androidID = null;
        try {
            androidID = Settings.Secure.getString(context.getContentResolver(), Settings.Secure.ANDROID_ID);
            if (TextUtils.isEmpty(androidID) || "9774d56d682e549c".equals(androidID)) {
                return;
            }
        } catch (Exception e) {
            e.printStackTrace();
            return;
        }
        uniqueID = androidID;
        Log.e(TAG, "getUniqueID: AndroidID获取成功" + uniqueID);
    }

    private static void getSNID() {
        if (!TextUtils.isEmpty(uniqueID)) {
            return;
        }
        String snID = Build.SERIAL;
        if (TextUtils.isEmpty(snID)) {
            return;
        }
        uniqueID = snID;
        Log.e(TAG, "getUniqueID: SNID获取成功" + uniqueID);
    }


    private static void createUniqueID(Context context) {
        if (!TextUtils.isEmpty(uniqueID)) {
            return;
        }
        uniqueID = UUID.randomUUID().toString();
        Log.e(TAG, "getUniqueID: UUID生成成功" + uniqueID);
        File filesDir = new File(uniqueIDDirPath + File.separator + context.getApplicationContext().getPackageName());
        if (!filesDir.exists()) {
            filesDir.mkdir();
        }
        File file = new File(filesDir, uniqueIDFile);
        if (!file.exists()) {
            try {
                file.createNewFile();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }

        FileOutputStream outputStream = null;
        try {
            outputStream = new FileOutputStream(file);
            outputStream.write(uniqueID.getBytes());
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (outputStream != null) {
                try {
                    outputStream.flush();
                    outputStream.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    private static void readUniqueFile(Context context) {
        File filesDir = new File(uniqueIDDirPath + File.separator + context.getApplicationContext().getPackageName());
        File file = new File(filesDir, uniqueIDFile);
        if (file.exists()) {
            FileInputStream inputStream = null;
            try {
                inputStream = new FileInputStream(file);
                byte[] bytes = new byte[(int) file.length()];
                inputStream.read(bytes);
                uniqueID = new String(bytes);
            } catch (FileNotFoundException e) {
                e.printStackTrace();
            } catch (IOException e) {
                e.printStackTrace();
            } finally {
                if (inputStream != null) {
                    try {
                        inputStream.close();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    }

    public static void clearUniqueFile(Context context) {
        File filesDir = new File(uniqueIDDirPath + File.separator + context.getApplicationContext().getPackageName());
        deleteFile(filesDir);
    }

    private static void deleteFile(File file) {
        if (file.isDirectory()) {
            for (File listFile : file.listFiles()) {
                deleteFile(listFile);
            }
        } else {
            file.delete();
        }
    }
}

希望但又矛盾的完美方案

硬件标识既然对获取方关闭,那提供基于硬件标识生成的标识(类似UUID)暴露给获取方,但Android10上对于设备隐私的控制又明确了Google是不想app能够长久定位同一台设备的。不过如果基于硬件标识及app包名来生成的呢?

名词解释

设备码缩写(全称) 定义
IMEI(International Mobile Equipment Identity) 国际移动电话设备识别码:由15位数字组成的"电子串号",它与每台手机一一对应,而且该码是全世界唯一的
UUID(Universally Unique Identifier) 全局唯一标识符:指在一台机器上生成的数字,它保证对在同一时空中的所有机器都是唯一的,由以下几部分的组合:当前日期和时间(UUID的第一个部分与时间有关,如果你在生成一个UUID之后,过几秒又生成一个UUID,则第一个部分不同,其余相同),时钟序列,全局唯一的IEEE机器识别号(如果有网卡,从网卡获得,没有网卡以其他方式获得)
MEID(Mobile Equipment IDentifier ) 是全球唯一的56bit CDMA制式移动终端标识号。标识号会被烧入终端里,并且不能被修改。可用来对CDMA制式移动式设备进行身份识别和跟踪
IMEI是手机的身份证,MEID是CDMA制式(电信运营的)的专用身份证;IMEI是15位,MEID是14位
DEVICE_ID Android系统为开发者提供的用于标识手机设备的串号 ; 它根据不同的手机设备返回IMEI,MEID或者ESN码 ;它返回的是设备的真实标识(因此Android10上更新的隐私保护上无法对它进行正常获取了)
ANDROID_ID 在设备首次启动时,系统会随机生成一个64位的数字,并把这个数字以16进制字符串的形式保存下来 。 当设备被wipe后该值会被重置 (wipe:手机恢复出厂设置、刷机或其他类似操作)
Serial Number SN码是Serial Number的缩写,有时也叫SerialNo,也就是产品序列号,产品序列是为了验证“产品的合法身份”而引入的一个概念,它是用来保障用户的正版权益,享受合法服务的;一套正版的产品只对应一组产品序列号。SN码别称:机器码、认证码、注册申请码等
MAC ADDRESS 媒体访问控制地址,也称为局域网地址(LAN Address),以太网地址(Ethernet Address)或物理地址(Physical Address),它是一个用来确认网络设备位置的地址。 在OSI模型中,第三层网络层负责IP地址,第二层数据链接层则负责MAC地址。MAC地址用于在网络中唯一标示一个网卡,一台设备若有一或多个网卡,则每个网卡都需要并会有一个唯一的MAC地址。详细参考WIKI百科
ESN码 (Electronic Serial Number ) 美国联邦通信委员会规定的,每一台移动设备(例如移动电话、智能手机、平板电脑等)独有的参数,其长度为32位
ESN码一开始使用于AMPS和D-AMPS手机上,当前则于CDMA手机上最为常见;IMEI则最常使用在GSM制式的手机上

推荐阅读更多精彩内容