前言
我們在開發中可能會使用到一些第三方的應用統計SDK,用于統計應用的用戶量等等,如何區分每個用戶呢?當然就需要每個設備對應一個唯一的標識,Android中當然也提供了這樣的API來獲取到設備相關標識,但遺憾的是隨著Android版本的迭代,官方對于用戶隱私的權限越來越嚴格,在最新的Android 10版本中甚至已經無法通過原來的一些API來獲取到設備相關標識了。本文就來探究一下Android中的各種設備相關標識符,介紹幾種在Android 10限制下獲取設備相關標識的方案。
1.Android中的幾個設備相關標識
- IMEI
IMEI(International Mobile Equipment Identity)是國際移動設備識別碼的縮寫,由15-17位數字組成,與手機是一一對應的關系,該碼是全球唯一的,并且永遠不會改變。
在Android 8.0(API Level 26)以下,可以通過TelephonyManager的getDeviceId()
方法獲取到設備的IMEI碼(其實這里的說法不準確,該方法是會根據手機設備的制式(GSM或CDMA)返回相應的設備碼(IMEI、MEID和ESN)),該方法在Android 8.0及之后的版本已經被廢棄了,取而代之的是getImei()
方法。獲取設備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();
}
}
無論是getDeviceId()
方法還是getImei()
方法都可以傳入一個參數slotIndex,用于設備中插入了雙卡的情況,這里就不展示了。
IMEI碼的獲取方式很簡單,也能保證唯一性和不變性,目前很多應用都使用IMEI碼作為設備的唯一標識,但眾所周知,在Android 6.0以上獲取IMEI碼是需要動態申請READ_PHONE_STATE權限的,一旦用戶拒絕了該權限就獲取不到了。這還不是最要命的,在Android 10中官方已經明確說明第三方應用無法獲取到IMEI碼,詳細內容可以查看Android 10 中的隱私權變更,這里附上一張圖。
下面我們分幾種情況來驗證一下IMEI碼的獲取情況:
-
Android 6.0以下:無需申請權限,可以通過
getDeviceId()
方法獲取到IMEI碼 -
Android 6.0-Android 8.0:需要申請READ_PHONE_STATE權限,可以通過
getDeviceId()
方法獲取到IMEI碼,如果用戶拒絕了權限,會拋出java.lang.SecurityException異常 -
Android 8.0-Android 10:需要申請READ_PHONE_STATE權限,可以通過
getImei()
方法獲取到IMEI碼,如果用戶拒絕了權限,會拋出java.lang.SecurityException異常 -
Android 10及以上:分為以下兩種情況:
-
targetSdkVersion<29:沒有申請權限的情況,通過
getImei()
方法獲取IMEI碼時拋出java.lang.SecurityException異常;申請了權限,通過getImei()
方法獲取到IMEI碼為null -
targetSdkVersion=29:無論是否申請了權限,通過
getImei()
方法獲取IMEI碼時都會直接拋出java.lang.SecurityException異常
-
targetSdkVersion<29:沒有申請權限的情況,通過
不難看出,IMEI碼在Android 10之后已經無法獲取到了,而且甚至會直接拋出異常導致程序崩潰,在Android 10以下版本雖然可以獲取到IMEI碼,但是需要在應用獲取到了READ_PHONE_STATE權限的前提下,我們依然無法保證這一點。
- 設備序列號
設備序列號是手機生產廠商提供的,如果拼接上廠商名稱(Build.MANUFACTURER)基本上可以保證唯一性。在Android 8.0以下版本,可以通過android.os.Build.SERIAL
獲取到設備序列號,同樣的,這種方式在Android 8.0及以上版本被廢棄了,通過Build.SERIAL
在Android 8.0及以上設備獲取到設備的序列號始終為“unknown”,取而代之的是使用android.os.Build.getSerial()
方法。獲取設備序列號的示例代碼如下:
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及以上版本是需要動態申請READ_PHONE_STATE權限的,并且該方法在Android 10上同樣無法獲取到設備序列號。
我們同樣來看一下幾種情況下獲取設備序列號的情況:
-
Android 8.0以下:無需申請權限,可以通過
Build.SERIAL
獲取到設備序列號 -
Android 8.0-Android 10:需要申請READ_PHONE_STATE權限,可以通過
Build.getSerial()
獲取到設備序列號,如果用戶拒絕了權限,會拋出java.lang.SecurityException異常 -
Android 10及以上:分為以下兩種情況:
-
targetSdkVersion<29:沒有申請權限的情況,調用
Build.getSerial()
方法時拋出java.lang.SecurityException異常;申請了權限,通過Build.getSerial()
方法獲取到的設備序列號為“unknown” -
targetSdkVersion=29:無論是否申請了權限,調用
Build.getSerial()
方法時都會直接拋出java.lang.SecurityException異常
-
targetSdkVersion<29:沒有申請權限的情況,調用
可以看出,和IMEI碼一樣,官方同樣限制了設備序列號的獲取。此外,由于序列號是手機生產廠商提供的,無法保證各個廠商的規范性,甚至有些廠商的手機獲取不到設備序列號。
- MAC地址
MAC地址(Media Access Control Address),直譯為媒體存取控制位址,也稱為局域網地址、以太網地址或物理地址,由48位二進制數組成。與我們熟悉的IP地址不同,mac地址只由設備的網卡決定,每個網卡都會有一個唯一的mac地址,只要不更換設備的網卡,mac地址就不會變,因此mac地址符合我們對于設備標識的要求。
在Android 6.0以下版本可以通過下面的代碼獲取到設備的mac地址:
private String getMacAddress(Context context) {
WifiManager wm = (WifiManager) context.getSystemService(Context.WIFI_SERVICE);
return wm.getConnectionInfo().getMacAddress();
}
通過該方法獲取mac地址需要聲明ACCESS_WIFI_STATE權限,并且設備需要開啟wifi。但是從Android 6.0開始,使用該方法獲取到的mac地址都為02:00:00:00:00:00。替代方案是通過讀取系統文件/sys/class/net/wlan0/address來獲取mac地址,示例代碼如下:
private String getMacAddress() {
return new BufferedReader(new FileReader(new File("/sys/class/net/wlan0/address"))).readLine();
}
不幸的是,該方法在Android 7.0開始也行不通了,執行上面的代碼會拋出java.io.FileNotFoundException: /sys/class/net/wlan0/address (Permission denied)異常,也就是說我們沒有權限讀取該文件。但好在目前還是有獲取mac地址的方法的,即通過掃描所有的網絡接口,示例代碼如下:
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的真機和模擬器上測試了該方法,都能獲取到mac地址,甚至都不需要聯網,而且每次獲取到mac地址都是一樣的。可以看出,mac地址的獲取相對來說是最麻煩的一個,但好在目前還是能獲取到的,因此我們可以考慮使用mac地址來作為設備標識。
后來很多同學提出了Android 10 mac地址隨機化的問題,每次連接wifi網絡獲取到的MAC地址都是隨機的,因此不能使用mac地址作為設備的唯一標識。其實我此前在官網上也看到過隨機分配 MAC 地址這個特性,但是我自己測試的情況確實每次獲取到的mac地址都是固定的,起初還是很疑惑的,通過查找資料和咨詢其他大佬才知道mac地址隨機化這個特性并不是所有Android 10的手機都支持的,目前大部分手機還不支持這個特性,因此獲取到的mac地址就是固定的。如何判斷手機是否支持mac地址隨機化呢,我們可以打開手機的開發者選項,如果有看到“連接時隨機選擇MAC網址”這個選項,就說明手機是支持這個特性的,當開啟了這個選項后,每次切換wifi網絡獲取到的mac地址就是隨機的了。
總結一下,目前支持mac地址隨機化的手機還比較少,因此我們還是可以考慮使用mac地址作為設備標識的,但是隨著各大廠商手機的更新換代,當市面上大部分手機都支持了這一特性后,這種方案就不太可行了。
- ANDROID_ID
ANDROID_ID是設備的系統首次啟動時隨機生成的一串字符,由16個16進制數(64位)組成,基本上還是可以保證唯一性的,獲取ANDROID_ID的示例代碼如下:
String androidId = Settings.System.getString(getContentResolver(), Settings.Secure.ANDROID_ID);
相比于上面幾種設備相關標識,ANDROID_ID的獲取門檻是最低的,不需要任何權限,但哪里有十全十美的事,ANDROID_ID也存在一些缺點,就是無法保證穩定性,root、刷機或恢復出廠設置都會導致設備的ANDROID_ID發生改變。此外,我看到部分文章中有提到某些廠商定制系統的Bug會導致不同的設備可能會產生相同的ANDROID_ID,而且某些設備獲取到的ANDROID_ID為null。總體來說,相比于其他幾種設備標識或多或少都有被官方“照顧”過,ANDROID_ID還是比較穩定的,如果應用對于設備標識的要求不是特別高的話還是一個值得考慮的方案。
2.Android 10中獲取設備相關標識的方案
根據上文對幾個常見設備標識的分析我們可以看出列出的幾種設備標識都有或多或少的缺陷,那么針對Android 10的限制,我們應該如何獲取到穩定的設備標識呢?關于這個問題我查閱一些相關文章,大體總結出了以下幾種方案:
- 方案一、使用ANDROID_ID
ANDROID_ID的獲取不需要任何權限,并且可以很好地保證唯一性,缺點就是無法保證穩定性,即一些操作可能導致ANDROID_ID的改變。
- 方案二、使用mac地址
目前來說mac地址仍然是可以獲取到的,也能很好地保證唯一性和穩定性,缺點是不能保證以后官方是否會限制mac地址的獲取,并且隨著各大廠商手機的更新,啟用mac地址隨機化的手機會越來越多,mac地址就無法再作為設備標識來使用了。
- 方案三、自定義一個生成規則
我們同樣可以自定義一個設備標識的生成規則,在應用首次安裝后將生成的標識保存到本地。生成的規則其實有很多種,最簡單的是直接使用UUID或GUID,復雜一些的可以在此基礎上拼接上設備生產廠商的信息。我這里就不具體介紹各種生成方案了,感興趣的話可以查找一下相關文章。
- 方案四、使用移動安全聯盟(MSA)提出的補充設備標識
這其實是我主要想介紹的一個方案,是由移動安全聯盟提出的,包含以下三個標識:
名稱 | 說明 |
---|---|
OAID | 匿名設備標識符,最長64為,所有應用都獲取到同一個ID,但是用戶可關閉、可重置 |
AAID | 應用匿名設備標識符,最長64為,每個應用獲取到各自的ID |
VAID | 開發者匿名設備標識符,最長64為,同一開發者不同應用獲取到的一致 |
目前文檔中給出的覆蓋設備范圍如下:
可以看出目前主流的廠商都做出了相應的適配,這后三個廠商是啥情況。。。不過我看SDK更新的信息中有提到這三個廠商的支持。
具體集成步驟和獲取方法我這里就不介紹了,官方提供了詳細的文檔,可以到官網下載,我也已經相關文件上傳到了github,附上地址。
當然這種方案的覆蓋機型也不是100%的,SDK提供的API可以判斷設備是否支持獲取補充設備標識,對于不支持的設備我們依然可以選擇使用此前介紹過的幾種設備標識。
最后說一下我個人的方案吧,其實針對那些對設備標識要求不高的應用來說,使用ANDROID_ID是最好也是最簡單的方案了,如果應用對設備標識的要求比較高,可以嘗試使用MSA提出的補充設備標識(如OAID),該方案對于國產手機廠商的支持還是比較好的,后續的適配也還在進行,首先要判斷一下設備是否支持獲取補充設備標識,支持的話就直接使用,不支持的話仍然可以使用ANDROID_ID或者mac地址等設備標識,如果說覺得同時綜合幾種標識會導致格式(位數)不統一,可以在此基礎上進行一個統一的處理,比如MD5加密等等,最后獲取到的就是一個格式統一的設備標識碼了。當然上面這種想法只是我個人的見解,還有很多方案可選擇,但是最終目標都是一致的,就是盡量多地適配各種設備并且保證標識的唯一性和穩定性,如果大家覺得不妥或是有更好的方案歡迎提出,一起交流。
3.總結
Android的碎片化一直都很讓開發者頭痛,目前國內更是各大廠商“百花齊放”,在適配方面我們往往需要根據廠商的不同進行各自的處理,解決方案就是需要針對各大廠商的差異性提出一個統一的適配方案,就像文中介紹的補充設備標識以及統一推送聯盟這樣。隨著Android版本的迭代,官方對于設置隱私的限制越來也高,我們很難找到一個穩定獲取設備標識的方案,不過我相信在未來隨著補充設備標識SDK版本的更新,適配性會越來越好。
相關代碼我已經上傳到了github,可以進行參考。