4-3.1 如何確定Android設(shè)備唯一識別碼

[TOC]
補(bǔ)充:2020-03-02 華為MatePad上設(shè)備號獲取 神奇取得unknown 字符串

應(yīng)用設(shè)備唯一識別碼的解決方案

設(shè)備唯一識別碼.png

唯一標(biāo)識必須滿足兩個(gè)特性才能完美解決定位唯一設(shè)備的問題,但這個(gè)問題的解決卻注定只能極限接近完美

  • 唯一性:標(biāo)識必須在所有使用該應(yīng)用的設(shè)備上保持唯一性
  • 不變性:標(biāo)識必須在同一設(shè)備上保持不變

方向一:使用硬件標(biāo)識

硬件標(biāo)識實(shí)際上在硬件生產(chǎn)之時(shí)就被要求滿足這兩個(gè)特性(依然有人工生產(chǎn)的不確定性),但標(biāo)識的獲取趨于困難性同廉,使得使用硬件標(biāo)識作為唯一識別碼的方案所能使用的范圍越來越狹窄羡洛,不能作為全局方案使用。

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

通過獲取藍(lán)牙或wifi的Mac地址 作為唯一識別號

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

4. 使用 SERIAL NUMBER

通過 android.os.Build.SERIAL來獲取

5. 硬件標(biāo)識的優(yōu)勢與局限性

優(yōu)勢:幾乎完美滿足唯一性與不變性

劣勢:

硬件標(biāo)識 局限
DEVICE_ID - 適用 Android9以下設(shè)備踊餐,但需要申請Manifest.permission.READ_PHONE_STATE權(quán)限
- 非手機(jī)設(shè)備不能使用: 如果只帶有Wifi的設(shè)備或者音樂播放器沒有通話的硬件功能的話就沒有這個(gè)DEVICE_ID
- 有bug:在少數(shù)的一些手機(jī)設(shè)備上景醇,該實(shí)現(xiàn)有漏洞,會返回垃圾吝岭,如:zeros或者asterisks的產(chǎn)品
- Android 10 設(shè)備上即使授予權(quán)限也會報(bào)錯(cuò)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 - 廠商定制系統(tǒng)的Bug: 不同的設(shè)備可能會產(chǎn)生相同的ANDROID_ID:9774d56d682e549c三痰;有些設(shè)備返回的值為null;
適配局限(基本不考慮):在Android <=2.1 or Android >=2.3的版本是可靠、穩(wěn)定的窜管,但在2.2的版本并不是100%可靠的
設(shè)備差異(不考慮):對于CDMA設(shè)備散劫,ANDROID_ID和TelephonyManager.getDeviceId() 返回相同的值
MAC ADDRESS - 硬件限制:并不是所有的設(shè)備都有WiFi和藍(lán)牙硬件
- 獲取的限制:如果WiFi沒有打開過,是無法獲取其Mac地址的幕帆;而藍(lán)牙是只有在打開的時(shí)候才能獲取到其Mac地址
- Android 6.0(API 級別 23)到 Android 9(API 級別 28)中获搏,無法通過第三方 API 使用 Wi-Fi 和藍(lán)牙等本地設(shè)備 Mac 地址。WifiInfo.getMacAddress() 方法和 BluetoothAdapter.getDefaultAdapter().getAddress() 方法都返回 02:00:00:00:00:00失乾。
SERIAL NUMBER 經(jīng)常會返回Unknown

方向二 使用UUID

這也是官方推薦的生成的唯一標(biāo)識碼生成方式常熙,有一點(diǎn)不同的時(shí),官方方案(在這里)將生成的UUID存在應(yīng)用內(nèi)部存儲當(dāng)中碱茁,APP的卸載重裝會導(dǎo)致發(fā)生更改症概;在實(shí)際使用當(dāng)中我們可以存儲到外部存儲,除非人為的刪除早芭、損壞彼城,這樣它的不變性也得到了保障,而它的唯一性則由UUID來保證。

UUID的實(shí)現(xiàn)原理簡析:
Wiki解釋:通用唯一識別碼(英語:Universally Unique Identifier募壕,縮寫:UUID)是用于計(jì)算機(jī)體系中以識別信息數(shù)目的一個(gè)128位標(biāo)識符调炬,還有相關(guān)的術(shù)語:全局唯一標(biāo)識符(GUID)。根據(jù)標(biāo)準(zhǔn)方法生成舱馅,不依賴中央機(jī)構(gòu)的注冊和分配缰泡,UUID具有唯一性,這與其他大多數(shù)編號方案不同代嗤。重復(fù)UUID碼概率接近零棘钞,可以忽略不計(jì)

組成: 8-4-4-4-12 xxxxxxxx-xxxx-Mxxx-Nxxx-xxxxxxxxxxxx M表示 UUID 版本,數(shù)字 N的一至三個(gè)最高有效位表示 UUID 變體

UUID根據(jù)版本不同干毅,依賴的組成有不同的變種宜猜,

基于時(shí)間的UUID版本是通過計(jì)算當(dāng)前時(shí)間戳、隨機(jī)數(shù)和機(jī)器MAC地址得到 硝逢。UUID的核心算法保證了即使在多處理器同時(shí)生成的UUID重復(fù)性為0姨拥,因?yàn)樗麄兯诘臅r(shí)間、空間(節(jié)點(diǎn):通常是MAC地址)必然不一致渠鸽。

由于在算法中使用了MAC地址叫乌,這個(gè)版本的UUID可以保證在全球范圍的唯一性。但與此同時(shí)徽缚,使用MAC地址會帶來安全性問題憨奸,這就是這個(gè)版本UUID受到批評的地方。如果應(yīng)用只是在局域網(wǎng)中使用凿试,也可以使用退化的算法膀藐,以IP地址來代替MAC地址--Java的UUID往往是這樣實(shí)現(xiàn)的(當(dāng)然也考慮了獲取MAC的難度)。

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

趨于完美的方案

盡可能的獲取硬件標(biāo)識來滿足兩個(gè)特性红省,在有限制或其他因素的條件下额各,盡可能滿足不變性,將UUID存儲在外部環(huán)境來進(jìn)行讀寫吧恃。

方案思路
盡可能的獲取硬件標(biāo)識
硬件標(biāo)識為空虾啦,進(jìn)行UUID的生成、存儲

方案說明:

  1. 需要在使用之前拿到設(shè)備信息權(quán)限(沒有會導(dǎo)致DeviceID不可取痕寓,但仍然可用)傲醉,外部存儲讀寫權(quán)限(必須,否則不可用)
  2. 最好在Application中使用呻率,唯一標(biāo)識在app與服務(wù)器直接交互很常用硬毕,放在全局統(tǒng)一的地方方便管理使用

還有一種方案是拿到設(shè)備的某些唯一信息,生成特定的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) {
        //三步讀染录埂:內(nèi)存中童谒,存儲的SP表中,外部存儲文件中
        if (!TextUtils.isEmpty(uniqueID)) {
            Log.e(TAG, "getUniqueID: 內(nèi)存中獲取" + 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;
        }
        //兩步創(chuàng)建:硬件獲然Ω帷饥伊;自行生成與存儲
        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();
//華為MatePad上,神奇的獲得unknown蔫饰,特此修復(fù)
            if (TextUtils.isEmpty(deviceId)||"unknown".equals(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();
        }
    }
}

希望但又矛盾的完美方案

硬件標(biāo)識既然對獲取方關(guān)閉琅豆,那提供基于硬件標(biāo)識生成的標(biāo)識(類似UUID)暴露給獲取方,但Android10上對于設(shè)備隱私的控制又明確了Google是不想app能夠長久定位同一臺設(shè)備的篓吁。不過如果基于硬件標(biāo)識及app包名來生成的呢茫因?

名詞解釋

設(shè)備碼縮寫(全稱) 定義
IMEI(International Mobile Equipment Identity) 國際移動電話設(shè)備識別碼:由15位數(shù)字組成的"電子串號",它與每臺手機(jī)一一對應(yīng)越除,而且該碼是全世界唯一的
UUID(Universally Unique Identifier) 全局唯一標(biāo)識符:指在一臺機(jī)器上生成的數(shù)字,它保證對在同一時(shí)空中的所有機(jī)器都是唯一的外盯,由以下幾部分的組合:當(dāng)前日期和時(shí)間(UUID的第一個(gè)部分與時(shí)間有關(guān)摘盆,如果你在生成一個(gè)UUID之后,過幾秒又生成一個(gè)UUID饱苟,則第一個(gè)部分不同孩擂,其余相同),時(shí)鐘序列箱熬,全局唯一的IEEE機(jī)器識別號(如果有網(wǎng)卡类垦,從網(wǎng)卡獲得,沒有網(wǎng)卡以其他方式獲得)
MEID(Mobile Equipment IDentifier ) 是全球唯一的56bit CDMA制式移動終端標(biāo)識號城须。標(biāo)識號會被燒入終端里蚤认,并且不能被修改「夥ィ可用來對CDMA制式移動式設(shè)備進(jìn)行身份識別和跟蹤
IMEI是手機(jī)的身份證砰琢,MEID是CDMA制式(電信運(yùn)營的)的專用身份證;IMEI是15位良瞧,MEID是14位
DEVICE_ID Android系統(tǒng)為開發(fā)者提供的用于標(biāo)識手機(jī)設(shè)備的串號 陪汽; 它根據(jù)不同的手機(jī)設(shè)備返回IMEI,MEID或者ESN碼 褥蚯;它返回的是設(shè)備的真實(shí)標(biāo)識(因此Android10上更新的隱私保護(hù)上無法對它進(jìn)行正常獲取了)
ANDROID_ID 在設(shè)備首次啟動時(shí)挚冤,系統(tǒng)會隨機(jī)生成一個(gè)64位的數(shù)字,并把這個(gè)數(shù)字以16進(jìn)制字符串的形式保存下來 赞庶。 當(dāng)設(shè)備被wipe后該值會被重置 (wipe:手機(jī)恢復(fù)出廠設(shè)置训挡、刷機(jī)或其他類似操作)
Serial Number SN碼是Serial Number的縮寫澳骤,有時(shí)也叫SerialNo,也就是產(chǎn)品序列號舍哄,產(chǎn)品序列是為了驗(yàn)證“產(chǎn)品的合法身份”而引入的一個(gè)概念宴凉,它是用來保障用戶的正版權(quán)益,享受合法服務(wù)的表悬;一套正版的產(chǎn)品只對應(yīng)一組產(chǎn)品序列號弥锄。SN碼別稱:機(jī)器碼、認(rèn)證碼蟆沫、注冊申請碼等
MAC ADDRESS 媒體訪問控制地址籽暇,也稱為局域網(wǎng)地址(LAN Address),以太網(wǎng)地址(Ethernet Address)或物理地址(Physical Address)饭庞,它是一個(gè)用來確認(rèn)網(wǎng)絡(luò)設(shè)備位置的地址戒悠。 在OSI模型中,第三層網(wǎng)絡(luò)層負(fù)責(zé)IP地址舟山,第二層數(shù)據(jù)鏈接層則負(fù)責(zé)MAC地址绸狐。MAC地址用于在網(wǎng)絡(luò)中唯一標(biāo)示一個(gè)網(wǎng)卡,一臺設(shè)備若有一或多個(gè)網(wǎng)卡累盗,則每個(gè)網(wǎng)卡都需要并會有一個(gè)唯一的MAC地址寒矿。詳細(xì)參考WIKI百科
ESN碼 (Electronic Serial Number ) 美國聯(lián)邦通信委員會規(guī)定的,每一臺移動設(shè)備(例如移動電話若债、智能手機(jī)符相、平板電腦等)獨(dú)有的參數(shù),其長度為32位
ESN碼一開始使用于AMPS和D-AMPS手機(jī)上蠢琳,當(dāng)前則于CDMA手機(jī)上最為常見啊终;IMEI則最常使用在GSM制式的手機(jī)上
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市傲须,隨后出現(xiàn)的幾起案子蓝牲,更是在濱河造成了極大的恐慌,老刑警劉巖泰讽,帶你破解...
    沈念sama閱讀 206,602評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件搞旭,死亡現(xiàn)場離奇詭異,居然都是意外死亡菇绵,警方通過查閱死者的電腦和手機(jī)肄渗,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,442評論 2 382
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來咬最,“玉大人翎嫡,你說我怎么就攤上這事∮牢冢” “怎么了惑申?”我有些...
    開封第一講書人閱讀 152,878評論 0 344
  • 文/不壞的土叔 我叫張陵具伍,是天一觀的道長。 經(jīng)常有香客問我圈驼,道長人芽,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,306評論 1 279
  • 正文 為了忘掉前任绩脆,我火速辦了婚禮萤厅,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘靴迫。我一直安慰自己惕味,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,330評論 5 373
  • 文/花漫 我一把揭開白布玉锌。 她就那樣靜靜地躺著名挥,像睡著了一般。 火紅的嫁衣襯著肌膚如雪主守。 梳的紋絲不亂的頭發(fā)上禀倔,一...
    開封第一講書人閱讀 49,071評論 1 285
  • 那天,我揣著相機(jī)與錄音参淫,去河邊找鬼救湖。 笑死,一個(gè)胖子當(dāng)著我的面吹牛黄刚,可吹牛的內(nèi)容都是我干的捎谨。 我是一名探鬼主播民效,決...
    沈念sama閱讀 38,382評論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼憔维,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了畏邢?” 一聲冷哼從身側(cè)響起业扒,我...
    開封第一講書人閱讀 37,006評論 0 259
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎舒萎,沒想到半個(gè)月后程储,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,512評論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡臂寝,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,965評論 2 325
  • 正文 我和宋清朗相戀三年章鲤,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片咆贬。...
    茶點(diǎn)故事閱讀 38,094評論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡败徊,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出掏缎,到底是詐尸還是另有隱情皱蹦,我是刑警寧澤煤杀,帶...
    沈念sama閱讀 33,732評論 4 323
  • 正文 年R本政府宣布,位于F島的核電站沪哺,受9級特大地震影響沈自,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜辜妓,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,283評論 3 307
  • 文/蒙蒙 一枯途、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧嫌拣,春花似錦柔袁、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,286評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至灰瞻,卻和暖如春腥例,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背酝润。 一陣腳步聲響...
    開封第一講書人閱讀 31,512評論 1 262
  • 我被黑心中介騙來泰國打工燎竖, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人要销。 一個(gè)月前我還...
    沈念sama閱讀 45,536評論 2 354
  • 正文 我出身青樓构回,卻偏偏與公主長得像,于是被迫代替她去往敵國和親疏咐。 傳聞我的和親對象是個(gè)殘疾皇子纤掸,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,828評論 2 345

推薦閱讀更多精彩內(nèi)容