在上一篇谷歌博客 (識(shí)別應(yīng)用安裝) 中,谷歌介紹了Android 中一些常用的標(biāo)識(shí)符,并提出了合理識(shí)別應(yīng)用每次安裝的辦法。指出通過獲取設(shè)備可靠,唯一,穩(wěn)定標(biāo)識(shí)符來追蹤設(shè)備可能產(chǎn)生的錯(cuò)誤,并簡(jiǎn)單介紹了 Android 中一些設(shè)備標(biāo)識(shí)符可能存在的問題。今天,我會(huì)介紹一下 Android 中的一些標(biāo)識(shí)符以及如何獲取它們,以及獲取這些標(biāo)識(shí)符過程中可能存在的坑。
標(biāo)識(shí)符(identifier)
設(shè)備ID(DeviceId)
- 獲取辦法
android.telephony.TelephonyManager tm = (android.telephony.TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE);
String deviceId = tm.getDeviceId();
- 當(dāng)設(shè)備為手機(jī)時(shí),返回設(shè)備的唯一ID。手機(jī)制式為 GSM 時(shí),返回手機(jī)的 IMEI 。手機(jī)制式為 CDMA 時(shí),返回手機(jī)的 MEID 或 ESN 。
- 非電話設(shè)備或者 Device ID 不可用時(shí),返回 null .
- 屬于比較穩(wěn)定的設(shè)備標(biāo)識(shí)符。
- 需要
READ_PHONE_STATE權(quán)限。 (Android 6.0 以上需要用戶手動(dòng)賦予該權(quán)限)。 - 某些設(shè)備上該方法存在 Bug ,返回的結(jié)果可能是一串0或者一串*號(hào)。
Sim 序列號(hào)(Sim Serial Number)
- 獲取辦法:
android.telephony.TelephonyManager tm = (android.telephony.TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE);
String simSerialNum = tm.getSimSerialNumber();
- 不同 Sim 卡的序列號(hào)不同.
- Sim 卡序列號(hào),當(dāng)手機(jī)上裝有 Sim 卡并且可用時(shí),返回該值。手機(jī)未裝 Sim 卡或者不可用時(shí),返回
null. - 需要
READ_PHONE_STATE權(quán)限。 (Android 6.0 以上需要用戶手動(dòng)賦予該權(quán)限)
Mac 地址(Mac Address)
- 獲取辦法:
android.net.wifi.WifiManager wifi = (android.net.wifi.WifiManager) context.getSystemService(Context.WIFI_SERVICE);
String macAddress = wifi.getConnectionInfo().getMacAddress();
- 沒有 WiFi 硬件或者 WiFi 不可用的設(shè)備可能返回 null 或空,注意判空.
- 比較穩(wěn)定的硬件標(biāo)識(shí)符。
- 需要
ACCESS_WIFI_STATE權(quán)限。 - Android 6.0開始,谷歌為保護(hù)用戶數(shù)據(jù),用此方法獲取到的 Wi-Fi mac 地址都為
02:00:00:00:00:00(更多信息查看此處) - 如果 app 在裝有谷歌框架的設(shè)備中讀取了mac地址,會(huì)被谷歌檢測(cè)為有害應(yīng)用提示用戶卸載。這也是為什么像友盟、TalkingData 等數(shù)據(jù)統(tǒng)計(jì) sdk 提供商專門針對(duì) Google Play 提供特供版的 sdk.

設(shè)備序列號(hào)(Serial Number, SN)
- 獲取辦法:
String serialNum = android.os.Build.SERIAL;
- 比較穩(wěn)定的設(shè)備硬件標(biāo)識(shí)符,在上一篇文章中谷歌也未提到有啥缺點(diǎn)。
ANDROID_ID
- 獲取辦法:
String androidId = android.provider.Settings.Secure.getString(context.getContentResolver(), android.provider.Settings.Secure.ANDROID_ID);
- 在設(shè)備第一次啟動(dòng)的時(shí)候生成并保存,并且可能會(huì)在恢復(fù)出廠設(shè)置后重置該值。理論上是大部分是重置的。(API 中原話是:The value
maychange if a factory reset is performed on the device.) - 在 Android 2.2 中不可靠.
- 部分設(shè)備由于制造商錯(cuò)誤實(shí)現(xiàn),導(dǎo)致會(huì)返回相同的 Android_ID.
- 在 Android 4.2 及以上, 設(shè)備啟用多用戶功能后,每個(gè)用戶的 Android_ID 不相同.
制造商 (Manufacturer)
- 獲取辦法:
String manufacturer = android.os.Build.MANUFACTURER;
型號(hào)(Model)
- 獲取辦法:
String model = android.os.Build.MODEL;
品牌(Brand)
- 獲取辦法:
String brand = android.os.Build.BRAND;
設(shè)備名 (Device)
- 獲取辦法:
String device = android.os.Build.DEVICE;
以下是我的一臺(tái) Nexus 4 所獲取的全部值:
以下的值僅作為舉例,并非真實(shí)
Identifier_Device_ID: 355136021808056
Identifier_Mac_Address: 10:68:3f:81:ed:ff
Identifier_Android_ID: 6ae48d23d1887323
Identifier_Serial_Num: 01b4549262d6a4a2
Identifier_Sim_SN: 898600e6111551111111
Identifier_Manufacturer: LGE
Identifier_Model: Nexus 4
Identifier_Brand: google
Identifier_Device: mako
如何合理使用標(biāo)識(shí)符跟蹤設(shè)備
介紹完了一些常見的、可能的作為標(biāo)識(shí)符的值,現(xiàn)在來談?wù)勅绾魏侠淼厥褂眠@些標(biāo)識(shí)符跟蹤設(shè)備。
首先,我們先要弄清自己跟蹤設(shè)備的具體需求。目前看來,需求無非兩種:
- 跟蹤用戶設(shè)備使用周期層次上的設(shè)備。
意思是將每次用戶的擦除設(shè)備、恢復(fù)出廠設(shè)置動(dòng)作后將設(shè)備視為一臺(tái)新的設(shè)備。 - 跟蹤硬件層次上的設(shè)備。
意思是無論設(shè)備擦除數(shù)據(jù)或者恢復(fù)出廠設(shè)置后都需要將該設(shè)備視為同一臺(tái)設(shè)備。
跟蹤用戶使用層次上的設(shè)備
方案 1:
這個(gè)層次上的設(shè)備跟蹤,我比較推薦使用谷歌官方推薦的辦法來跟蹤, App 首次啟動(dòng)時(shí)生成一個(gè) Random UUID 并保存在本地存儲(chǔ),以后每次啟動(dòng)時(shí)檢查該 UUID 文件。具體可以查看我的上一篇翻譯文章),其中有具體的代碼實(shí)現(xiàn)。
方案 2:
如果你不喜歡谷歌推薦的這種方式,或者覺得這種方式涉及到文件讀寫太過繁瑣等。我們也可以通過以上介紹的這些標(biāo)識(shí)符來跟蹤設(shè)備。因?yàn)樾枰獙⒃O(shè)備擦除數(shù)據(jù)或恢復(fù)出廠設(shè)置后將其視為一臺(tái)新的設(shè)備,所以需要使用一些與當(dāng)前用戶設(shè)備使用周期有關(guān)的值。
理論上,Android_ID 這一個(gè)值就已經(jīng)足夠我們實(shí)現(xiàn)這樣的需求,不過正是因?yàn)?Android_ID 存在缺陷,所以我們無法直接拿來識(shí)別設(shè)備。這里我們使用多個(gè)值拼湊來規(guī)避這些缺點(diǎn)。
與用戶設(shè)備使用周期有關(guān)的標(biāo)識(shí)符我推薦使用Android_ID和Sim Serial Number。另外可以加上Device_ID,通過 UUID 或者 MD5 等來計(jì)算生成設(shè)備的標(biāo)識(shí)符。
以下是一個(gè)簡(jiǎn)單的實(shí)現(xiàn),參考了 Stack Overflow 上的這個(gè)問題下面的回答。
- UUID 實(shí)現(xiàn):
UUID deviceUuid = new UUID(androidId.hashCode(), ((long)deviceId.hashCode() << 32) | simSerialNum.hashCode());
String deviceId = deviceUuid.toString();
結(jié)果類似:00000000-54b3-e7c7-0000-000046bffd97
- 或者你也可以使用 MD5 實(shí)現(xiàn)(MD5 算法見下文):
String md5ID = md5(androidId + deviceId + simSerialNum);
結(jié)果類似:f87b20b3c359c4af608b3eb26b26a1b8
跟蹤硬件層次上的設(shè)備
跟蹤硬件層次上的設(shè)備建議使用硬件的標(biāo)識(shí)符,比如設(shè)備ID(DeviceId)、Mac 地址、設(shè)備序列號(hào)(SN)或者設(shè)備的品牌,型號(hào)名等,這些值在用戶擦除數(shù)據(jù)或者恢復(fù)出廠設(shè)置后也不會(huì)改變。同樣的,為了提升穩(wěn)定性及排除單一標(biāo)識(shí)符所存在的缺陷,我們使用多個(gè)標(biāo)識(shí)符拼接,然后通過 UUID 或者 MD5 算法計(jì)算得出我們需要的設(shè)備標(biāo)識(shí)符。
以下是一個(gè)簡(jiǎn)單的實(shí)現(xiàn),使用了設(shè)備序列號(hào)(SN)、設(shè)備ID(DeviceId)和 Mac 地址。
拼接后的字符串類似于:01b4549262d6a4a235513602180805610:68:3f:81:ed:ff
同時(shí)為了不暴露用戶的設(shè)備具體信息,這里我們同樣采用 MD5 對(duì)拼接后的字符串進(jìn)行Hash操作:
String md5ID = md5("01b4549262d6a4a235513602180805610:68:3f:81:ed:ff");
拼湊的標(biāo)識(shí)符選擇,拼接的順序,MD5或者UUID的選擇并無絕對(duì),重要的是思想。
其他
- 以上這些值在使用前都建議判空。
- 因?yàn)橛布笔Щ蛘卟豢捎?,獲取標(biāo)識(shí)符過程中也可能返回 null 對(duì)象。為了避免 NullPointerException,建議獲取標(biāo)識(shí)符操作全部在 try...catch 中操作。
- 安卓設(shè)備的用戶不乏極客,修改 Android_ID 或者 Build 文件對(duì)他們來說并非難題。所以一定程度上說,沒有絕對(duì)準(zhǔn)確的跟蹤設(shè)備的標(biāo)識(shí)符。
*以下是一個(gè)Demo,項(xiàng)目建立后添加權(quán)限后即可使用
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
String deviceId = "";
String macAddress = "";
String androidId = "";
String serialNum = "";
String simSerialNum = "";
//需要READ_PHONE_STATE權(quán)限
android.telephony.TelephonyManager tm = (android.telephony.TelephonyManager) this
.getSystemService(Context.TELEPHONY_SERVICE);
if(checkPermission(this, Manifest.permission.READ_PHONE_STATE)){
deviceId = tm.getDeviceId();
simSerialNum = tm.getSimSerialNumber();
}
//需要ACCESS_WIFI_STATE權(quán)限
android.net.wifi.WifiManager wifi = (android.net.wifi.WifiManager) this
.getSystemService(Context.WIFI_SERVICE);
macAddress = wifi.getConnectionInfo().getMacAddress();
androidId = android.provider.Settings.Secure.getString(this.getContentResolver(),
android.provider.Settings.Secure.ANDROID_ID);
serialNum = Build.SERIAL;
String deviceManufacturer = Build.MANUFACTURER;
String deviceModel = Build.MODEL;
String deviceBrand = Build.BRAND;
String device = Build.DEVICE;
//==============
Log.e("Identifier_Device_ID", validate(deviceId));
Log.e("Identifier_Mac_Address", validate(macAddress));
Log.e("Identifier_Android_ID", validate(androidId));
Log.e("Identifier_Serial_Num", validate(serialNum));
Log.e("Identifier_Sim_SN", validate(simSerialNum));
Log.e("Identifier_Manufacturer", validate(deviceManufacturer));
Log.e("Identifier_Model", validate(deviceModel));
Log.e("Identifier_Brand", validate(deviceBrand));
Log.e("Identifier_Device", validate(device));
UUID deviceUserLifetimeUUID = new UUID(validate(androidId).hashCode(), ((long)validate(deviceId).hashCode() << 32) | validate(simSerialNum).hashCode());
String deviceUserLifetimeId = deviceUserLifetimeUUID.toString();
String deviceHardwareId = md5(validate(serialNum) + validate(deviceId) + validate(macAddress));;
Log.e("deviceUserLifetimeId", deviceUserLifetimeId);
Log.e("deviceHardwareId", deviceHardwareId);
}
// MD5加密,32位小寫
public static String md5(String str) {
MessageDigest md5 = null;
try {
md5 = MessageDigest.getInstance("MD5");
} catch (Exception e) {
e.printStackTrace();
return "";
}
md5.update(str.getBytes());
byte[] md5Bytes = md5.digest();
StringBuilder hexValue = new StringBuilder();
for (int i = 0; i < md5Bytes.length; i++) {
int val = ((int) md5Bytes[i]) & 0xff;
if (val < 16) {
hexValue.append("0");
}
hexValue.append(Integer.toHexString(val));
}
return hexValue.toString();
}
//檢查權(quán)限,READ_PHONE_STATE在API>=23需要用戶手動(dòng)賦予權(quán)限
public static boolean checkPermission(Context context, String permission) {
boolean result = false;
if (Build.VERSION.SDK_INT >= 23) {
if (context.checkSelfPermission(permission) == PackageManager.PERMISSION_GRANTED) {
result = true;
}
} else {
PackageManager pm = context.getPackageManager();
if (pm.checkPermission(permission, context.getPackageName()) == PackageManager.PERMISSION_GRANTED) {
result = true;
}
}
return result;
}
//判空
private String validate(String value) {
if(value == null) {
return "";
}
return value;
}
}
本文章為原創(chuàng)作品,轉(zhuǎn)載請(qǐng)注明出處。