前言
我們?cè)陂_(kāi)發(fā)中可能會(huì)使用到一些第三方的應(yīng)用統(tǒng)計(jì)SDK楼雹,用于統(tǒng)計(jì)應(yīng)用的用戶量等等邑彪,如何區(qū)分每個(gè)用戶呢选浑?當(dāng)然就需要每個(gè)設(shè)備對(duì)應(yīng)一個(gè)唯一的標(biāo)識(shí)办素,Android中當(dāng)然也提供了這樣的API來(lái)獲取到設(shè)備相關(guān)標(biāo)識(shí),但遺憾的是隨著Android版本的迭代欣尼,官方對(duì)于用戶隱私的權(quán)限越來(lái)越嚴(yán)格爆雹,在最新的Android 10版本中甚至已經(jīng)無(wú)法通過(guò)原來(lái)的一些API來(lái)獲取到設(shè)備相關(guān)標(biāo)識(shí)了。本文就來(lái)探究一下Android中的各種設(shè)備相關(guān)標(biāo)識(shí)符愕鼓,介紹幾種在Android 10限制下獲取設(shè)備相關(guān)標(biāo)識(shí)的方案钙态。
1.Android中的幾個(gè)設(shè)備相關(guān)標(biāo)識(shí)
- IMEI
IMEI(International Mobile Equipment Identity)是國(guó)際移動(dòng)設(shè)備識(shí)別碼的縮寫(xiě),由15-17位數(shù)字組成菇晃,與手機(jī)是一一對(duì)應(yīng)的關(guān)系册倒,該碼是全球唯一的,并且永遠(yuǎn)不會(huì)改變谋旦。
在Android 8.0(API Level 26)以下剩失,可以通過(guò)TelephonyManager的getDeviceId()
方法獲取到設(shè)備的IMEI碼(其實(shí)這里的說(shuō)法不準(zhǔn)確,該方法是會(huì)根據(jù)手機(jī)設(shè)備的制式(GSM或CDMA)返回相應(yīng)的設(shè)備碼(IMEI册着、MEID和ESN))拴孤,該方法在Android 8.0及之后的版本已經(jīng)被廢棄了,取而代之的是getImei()
方法甲捏。獲取設(shè)備IMEI碼的示例代碼如下:
private String getIMEI(Context context) {
TelephonyManager tm = (TelephonyManager) context.getSystemService(Service.TELEPHONY_SERVICE);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
return tm.getImei();
} else {
return tm.getDeviceId();
}
}
無(wú)論是getDeviceId()
方法還是getImei()
方法都可以傳入一個(gè)參數(shù)slotIndex演熟,用于設(shè)備中插入了雙卡的情況,這里就不展示了司顿。
IMEI碼的獲取方式很簡(jiǎn)單芒粹,也能保證唯一性和不變性,目前很多應(yīng)用都使用IMEI碼作為設(shè)備的唯一標(biāo)識(shí)大溜,但眾所周知化漆,在Android 6.0以上獲取IMEI碼是需要?jiǎng)討B(tài)申請(qǐng)READ_PHONE_STATE權(quán)限的,一旦用戶拒絕了該權(quán)限就獲取不到了钦奋。這還不是最要命的座云,在Android 10中官方已經(jīng)明確說(shuō)明第三方應(yīng)用無(wú)法獲取到IMEI碼疙赠,詳細(xì)內(nèi)容可以查看Android 10 中的隱私權(quán)變更,這里附上一張圖朦拖。
下面我們分幾種情況來(lái)驗(yàn)證一下IMEI碼的獲取情況:
-
Android 6.0以下:無(wú)需申請(qǐng)權(quán)限圃阳,可以通過(guò)
getDeviceId()
方法獲取到IMEI碼 -
Android 6.0-Android 8.0:需要申請(qǐng)READ_PHONE_STATE權(quán)限,可以通過(guò)
getDeviceId()
方法獲取到IMEI碼璧帝,如果用戶拒絕了權(quán)限捍岳,會(huì)拋出java.lang.SecurityException異常 -
Android 8.0-Android 10:需要申請(qǐng)READ_PHONE_STATE權(quán)限,可以通過(guò)
getImei()
方法獲取到IMEI碼睬隶,如果用戶拒絕了權(quán)限锣夹,會(huì)拋出java.lang.SecurityException異常 -
Android 10及以上:分為以下兩種情況:
-
targetSdkVersion<29:沒(méi)有申請(qǐng)權(quán)限的情況,通過(guò)
getImei()
方法獲取IMEI碼時(shí)拋出java.lang.SecurityException異常苏潜;申請(qǐng)了權(quán)限晕城,通過(guò)getImei()
方法獲取到IMEI碼為null -
targetSdkVersion=29:無(wú)論是否申請(qǐng)了權(quán)限,通過(guò)
getImei()
方法獲取IMEI碼時(shí)都會(huì)直接拋出java.lang.SecurityException異常
-
targetSdkVersion<29:沒(méi)有申請(qǐng)權(quán)限的情況,通過(guò)
不難看出窖贤,IMEI碼在Android 10之后已經(jīng)無(wú)法獲取到了,而且甚至?xí)苯訏伋霎惓?dǎo)致程序崩潰贰锁,在Android 10以下版本雖然可以獲取到IMEI碼赃梧,但是需要在應(yīng)用獲取到了READ_PHONE_STATE權(quán)限的前提下,我們依然無(wú)法保證這一點(diǎn)豌熄。
- 設(shè)備序列號(hào)
設(shè)備序列號(hào)是手機(jī)生產(chǎn)廠商提供的授嘀,如果拼接上廠商名稱(Build.MANUFACTURER)基本上可以保證唯一性。在Android 8.0以下版本锣险,可以通過(guò)android.os.Build.SERIAL
獲取到設(shè)備序列號(hào)蹄皱,同樣的,這種方式在Android 8.0及以上版本被廢棄了芯肤,通過(guò)Build.SERIAL
在Android 8.0及以上設(shè)備獲取到設(shè)備的序列號(hào)始終為“unknown”巷折,取而代之的是使用android.os.Build.getSerial()
方法。獲取設(shè)備序列號(hào)的示例代碼如下:
private String getSerial() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
return Build.getSerial();
} else {
return Build.SERIAL;
}
}
和getImei()
方法的弊端相同崖咨,Build.getSerial()
方法在Android 6.0及以上版本是需要?jiǎng)討B(tài)申請(qǐng)READ_PHONE_STATE權(quán)限的锻拘,并且該方法在Android 10上同樣無(wú)法獲取到設(shè)備序列號(hào)。
我們同樣來(lái)看一下幾種情況下獲取設(shè)備序列號(hào)的情況:
-
Android 8.0以下:無(wú)需申請(qǐng)權(quán)限击蹲,可以通過(guò)
Build.SERIAL
獲取到設(shè)備序列號(hào) -
Android 8.0-Android 10:需要申請(qǐng)READ_PHONE_STATE權(quán)限署拟,可以通過(guò)
Build.getSerial()
獲取到設(shè)備序列號(hào),如果用戶拒絕了權(quán)限歌豺,會(huì)拋出java.lang.SecurityException異常 -
Android 10及以上:分為以下兩種情況:
-
targetSdkVersion<29:沒(méi)有申請(qǐng)權(quán)限的情況推穷,調(diào)用
Build.getSerial()
方法時(shí)拋出java.lang.SecurityException異常;申請(qǐng)了權(quán)限类咧,通過(guò)Build.getSerial()
方法獲取到的設(shè)備序列號(hào)為“unknown” -
targetSdkVersion=29:無(wú)論是否申請(qǐng)了權(quán)限馒铃,調(diào)用
Build.getSerial()
方法時(shí)都會(huì)直接拋出java.lang.SecurityException異常
-
targetSdkVersion<29:沒(méi)有申請(qǐng)權(quán)限的情況推穷,調(diào)用
可以看出蟹腾,和IMEI碼一樣,官方同樣限制了設(shè)備序列號(hào)的獲取骗露。此外岭佳,由于序列號(hào)是手機(jī)生產(chǎn)廠商提供的,無(wú)法保證各個(gè)廠商的規(guī)范性萧锉,甚至有些廠商的手機(jī)獲取不到設(shè)備序列號(hào)珊随。
- MAC地址
MAC地址(Media Access Control Address),直譯為媒體存取控制位址柿隙,也稱為局域網(wǎng)地址叶洞、以太網(wǎng)地址或物理地址,由48位二進(jìn)制數(shù)組成禀崖。與我們熟悉的IP地址不同衩辟,mac地址只由設(shè)備的網(wǎng)卡決定,每個(gè)網(wǎng)卡都會(huì)有一個(gè)唯一的mac地址波附,只要不更換設(shè)備的網(wǎng)卡艺晴,mac地址就不會(huì)變,因此mac地址符合我們對(duì)于設(shè)備標(biāo)識(shí)的要求掸屡。
在Android 6.0以下版本可以通過(guò)下面的代碼獲取到設(shè)備的mac地址:
private String getMacAddress(Context context) {
WifiManager wm = (WifiManager) context.getSystemService(Context.WIFI_SERVICE);
return wm.getConnectionInfo().getMacAddress();
}
通過(guò)該方法獲取mac地址需要聲明ACCESS_WIFI_STATE權(quán)限封寞,并且設(shè)備需要開(kāi)啟wifi。但是從Android 6.0開(kāi)始仅财,使用該方法獲取到的mac地址都為02:00:00:00:00:00狈究。替代方案是通過(guò)讀取系統(tǒng)文件/sys/class/net/wlan0/address來(lái)獲取mac地址,示例代碼如下:
private String getMacAddress() {
return new BufferedReader(new FileReader(new File("/sys/class/net/wlan0/address"))).readLine();
}
不幸的是盏求,該方法在Android 7.0開(kāi)始也行不通了抖锥,執(zhí)行上面的代碼會(huì)拋出java.io.FileNotFoundException: /sys/class/net/wlan0/address (Permission denied)異常,也就是說(shuō)我們沒(méi)有權(quán)限讀取該文件碎罚。但好在目前還是有獲取mac地址的方法的磅废,即通過(guò)掃描所有的網(wǎng)絡(luò)接口,示例代碼如下:
private String getMacAddress() {
try {
List<NetworkInterface> all = Collections.list(NetworkInterface.getNetworkInterfaces());
for (NetworkInterface nif : all) {
if (!nif.getName().equalsIgnoreCase("wlan0")) {
continue;
}
byte[] macBytes = nif.getHardwareAddress();
if (macBytes == null) {
return "";
}
StringBuilder res1 = new StringBuilder();
for (byte b : macBytes) {
res1.append(String.format("%02X:", b));
}
if (res1.length() > 0) {
res1.deleteCharAt(res1.length() - 1);
}
return res1.toString();
}
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
目前我在Android 10的真機(jī)和模擬器上測(cè)試了該方法魂莫,都能獲取到mac地址还蹲,甚至都不需要聯(lián)網(wǎng),而且每次獲取到mac地址都是一樣的耙考∶蘸埃可以看出,mac地址的獲取相對(duì)來(lái)說(shuō)是最麻煩的一個(gè)倦始,但好在目前還是能獲取到的斗遏,因此我們可以考慮使用mac地址來(lái)作為設(shè)備標(biāo)識(shí)。
后來(lái)很多同學(xué)提出了Android 10 mac地址隨機(jī)化的問(wèn)題鞋邑,每次連接wifi網(wǎng)絡(luò)獲取到的MAC地址都是隨機(jī)的诵次,因此不能使用mac地址作為設(shè)備的唯一標(biāo)識(shí)账蓉。其實(shí)我此前在官網(wǎng)上也看到過(guò)隨機(jī)分配 MAC 地址這個(gè)特性,但是我自己測(cè)試的情況確實(shí)每次獲取到的mac地址都是固定的逾一,起初還是很疑惑的铸本,通過(guò)查找資料和咨詢其他大佬才知道m(xù)ac地址隨機(jī)化這個(gè)特性并不是所有Android 10的手機(jī)都支持的,目前大部分手機(jī)還不支持這個(gè)特性遵堵,因此獲取到的mac地址就是固定的箱玷。如何判斷手機(jī)是否支持mac地址隨機(jī)化呢,我們可以打開(kāi)手機(jī)的開(kāi)發(fā)者選項(xiàng)陌宿,如果有看到“連接時(shí)隨機(jī)選擇MAC網(wǎng)址”這個(gè)選項(xiàng)锡足,就說(shuō)明手機(jī)是支持這個(gè)特性的,當(dāng)開(kāi)啟了這個(gè)選項(xiàng)后壳坪,每次切換wifi網(wǎng)絡(luò)獲取到的mac地址就是隨機(jī)的了舶得。
總結(jié)一下,目前支持mac地址隨機(jī)化的手機(jī)還比較少爽蝴,因此我們還是可以考慮使用mac地址作為設(shè)備標(biāo)識(shí)的沐批,但是隨著各大廠商手機(jī)的更新?lián)Q代,當(dāng)市面上大部分手機(jī)都支持了這一特性后蝎亚,這種方案就不太可行了珠插。
- ANDROID_ID
ANDROID_ID是設(shè)備的系統(tǒng)首次啟動(dòng)時(shí)隨機(jī)生成的一串字符,由16個(gè)16進(jìn)制數(shù)(64位)組成颖对,基本上還是可以保證唯一性的,獲取ANDROID_ID的示例代碼如下:
String androidId = Settings.System.getString(getContentResolver(), Settings.Secure.ANDROID_ID);
相比于上面幾種設(shè)備相關(guān)標(biāo)識(shí)磨隘,ANDROID_ID的獲取門(mén)檻是最低的缤底,不需要任何權(quán)限,但哪里有十全十美的事番捂,ANDROID_ID也存在一些缺點(diǎn)个唧,就是無(wú)法保證穩(wěn)定性,root设预、刷機(jī)或恢復(fù)出廠設(shè)置都會(huì)導(dǎo)致設(shè)備的ANDROID_ID發(fā)生改變徙歼。此外,我看到部分文章中有提到某些廠商定制系統(tǒng)的Bug會(huì)導(dǎo)致不同的設(shè)備可能會(huì)產(chǎn)生相同的ANDROID_ID鳖枕,而且某些設(shè)備獲取到的ANDROID_ID為null魄梯。總體來(lái)說(shuō)宾符,相比于其他幾種設(shè)備標(biāo)識(shí)或多或少都有被官方“照顧”過(guò)酿秸,ANDROID_ID還是比較穩(wěn)定的,如果應(yīng)用對(duì)于設(shè)備標(biāo)識(shí)的要求不是特別高的話還是一個(gè)值得考慮的方案魏烫。
2.Android 10中獲取設(shè)備相關(guān)標(biāo)識(shí)的方案
根據(jù)上文對(duì)幾個(gè)常見(jiàn)設(shè)備標(biāo)識(shí)的分析我們可以看出列出的幾種設(shè)備標(biāo)識(shí)都有或多或少的缺陷辣苏,那么針對(duì)Android 10的限制肝箱,我們應(yīng)該如何獲取到穩(wěn)定的設(shè)備標(biāo)識(shí)呢?關(guān)于這個(gè)問(wèn)題我查閱一些相關(guān)文章稀蟋,大體總結(jié)出了以下幾種方案:
- 方案一煌张、使用ANDROID_ID
ANDROID_ID的獲取不需要任何權(quán)限,并且可以很好地保證唯一性退客,缺點(diǎn)就是無(wú)法保證穩(wěn)定性骏融,即一些操作可能導(dǎo)致ANDROID_ID的改變。
- 方案二井辜、使用mac地址
目前來(lái)說(shuō)mac地址仍然是可以獲取到的绎谦,也能很好地保證唯一性和穩(wěn)定性,缺點(diǎn)是不能保證以后官方是否會(huì)限制mac地址的獲取粥脚,并且隨著各大廠商手機(jī)的更新窃肠,啟用mac地址隨機(jī)化的手機(jī)會(huì)越來(lái)越多,mac地址就無(wú)法再作為設(shè)備標(biāo)識(shí)來(lái)使用了刷允。
- 方案三冤留、自定義一個(gè)生成規(guī)則
我們同樣可以自定義一個(gè)設(shè)備標(biāo)識(shí)的生成規(guī)則,在應(yīng)用首次安裝后將生成的標(biāo)識(shí)保存到本地树灶。生成的規(guī)則其實(shí)有很多種纤怒,最簡(jiǎn)單的是直接使用UUID或GUID,復(fù)雜一些的可以在此基礎(chǔ)上拼接上設(shè)備生產(chǎn)廠商的信息天通。我這里就不具體介紹各種生成方案了泊窘,感興趣的話可以查找一下相關(guān)文章。
- 方案四像寒、使用移動(dòng)安全聯(lián)盟(MSA)提出的補(bǔ)充設(shè)備標(biāo)識(shí)
這其實(shí)是我主要想介紹的一個(gè)方案烘豹,是由移動(dòng)安全聯(lián)盟提出的,包含以下三個(gè)標(biāo)識(shí):
名稱 | 說(shuō)明 |
---|---|
OAID | 匿名設(shè)備標(biāo)識(shí)符诺祸,最長(zhǎng)64為携悯,所有應(yīng)用都獲取到同一個(gè)ID,但是用戶可關(guān)閉筷笨、可重置 |
AAID | 應(yīng)用匿名設(shè)備標(biāo)識(shí)符憔鬼,最長(zhǎng)64為,每個(gè)應(yīng)用獲取到各自的ID |
VAID | 開(kāi)發(fā)者匿名設(shè)備標(biāo)識(shí)符胃夏,最長(zhǎng)64為轴或,同一開(kāi)發(fā)者不同應(yīng)用獲取到的一致 |
目前文檔中給出的覆蓋設(shè)備范圍如下:
可以看出目前主流的廠商都做出了相應(yīng)的適配,這后三個(gè)廠商是啥情況仰禀。侮叮。。不過(guò)我看SDK更新的信息中有提到這三個(gè)廠商的支持悼瘾。
具體集成步驟和獲取方法我這里就不介紹了囊榜,官方提供了詳細(xì)的文檔审胸,可以到官網(wǎng)下載,我也已經(jīng)相關(guān)文件上傳到了github卸勺,附上地址砂沛。
當(dāng)然這種方案的覆蓋機(jī)型也不是100%的,SDK提供的API可以判斷設(shè)備是否支持獲取補(bǔ)充設(shè)備標(biāo)識(shí)曙求,對(duì)于不支持的設(shè)備我們依然可以選擇使用此前介紹過(guò)的幾種設(shè)備標(biāo)識(shí)碍庵。
最后說(shuō)一下我個(gè)人的方案吧,其實(shí)針對(duì)那些對(duì)設(shè)備標(biāo)識(shí)要求不高的應(yīng)用來(lái)說(shuō)悟狱,使用ANDROID_ID是最好也是最簡(jiǎn)單的方案了静浴,如果應(yīng)用對(duì)設(shè)備標(biāo)識(shí)的要求比較高,可以嘗試使用MSA提出的補(bǔ)充設(shè)備標(biāo)識(shí)(如OAID)挤渐,該方案對(duì)于國(guó)產(chǎn)手機(jī)廠商的支持還是比較好的苹享,后續(xù)的適配也還在進(jìn)行,首先要判斷一下設(shè)備是否支持獲取補(bǔ)充設(shè)備標(biāo)識(shí)浴麻,支持的話就直接使用得问,不支持的話仍然可以使用ANDROID_ID或者mac地址等設(shè)備標(biāo)識(shí),如果說(shuō)覺(jué)得同時(shí)綜合幾種標(biāo)識(shí)會(huì)導(dǎo)致格式(位數(shù))不統(tǒng)一软免,可以在此基礎(chǔ)上進(jìn)行一個(gè)統(tǒng)一的處理宫纬,比如MD5加密等等,最后獲取到的就是一個(gè)格式統(tǒng)一的設(shè)備標(biāo)識(shí)碼了膏萧。當(dāng)然上面這種想法只是我個(gè)人的見(jiàn)解漓骚,還有很多方案可選擇,但是最終目標(biāo)都是一致的榛泛,就是盡量多地適配各種設(shè)備并且保證標(biāo)識(shí)的唯一性和穩(wěn)定性认境,如果大家覺(jué)得不妥或是有更好的方案歡迎提出,一起交流挟鸠。
3.總結(jié)
Android的碎片化一直都很讓開(kāi)發(fā)者頭痛,目前國(guó)內(nèi)更是各大廠商“百花齊放”亩冬,在適配方面我們往往需要根據(jù)廠商的不同進(jìn)行各自的處理艘希,解決方案就是需要針對(duì)各大廠商的差異性提出一個(gè)統(tǒng)一的適配方案,就像文中介紹的補(bǔ)充設(shè)備標(biāo)識(shí)以及統(tǒng)一推送聯(lián)盟這樣硅急。隨著Android版本的迭代覆享,官方對(duì)于設(shè)置隱私的限制越來(lái)也高,我們很難找到一個(gè)穩(wěn)定獲取設(shè)備標(biāo)識(shí)的方案营袜,不過(guò)我相信在未來(lái)隨著補(bǔ)充設(shè)備標(biāo)識(shí)SDK版本的更新撒顿,適配性會(huì)越來(lái)越好。
相關(guān)代碼我已經(jīng)上傳到了github荚板,可以進(jìn)行參考凤壁。
4.參考文章
漫談唯一設(shè)備ID
談?wù)?Android 中的各種設(shè)備標(biāo)識(shí)符
Android 10 中的隱私權(quán)變更