[TOC]
補(bǔ)充:2020-03-02 華為MatePad上設(shè)備號獲取 神奇取得unknown 字符串
應(yīng)用設(shè)備唯一識別碼的解決方案
唯一標(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; 適配局限(基本不考慮): 設(shè)備差異(不考慮): |
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-xxxxxxxxxxxxM
表示 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的生成、存儲方案說明:
- 需要在使用之前拿到設(shè)備信息權(quán)限(沒有會導(dǎo)致DeviceID不可取痕寓,但仍然可用)傲醉,外部存儲讀寫權(quán)限(必須,否則不可用)
- 最好在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ī)上 |