談?wù)?Android 中的各種設(shè)備標(biāo)識(shí)符

在上一篇谷歌博客 (識(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.
讀取 mac 地址導(dǎo)致 app 被谷歌框架判定為有害應(yīng)用

設(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 may change 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è)備的具體需求。目前看來,需求無非兩種:

  1. 跟蹤用戶設(shè)備使用周期層次上的設(shè)備。
    意思是將每次用戶的擦除設(shè)備、恢復(fù)出廠設(shè)置動(dòng)作后將設(shè)備視為一臺(tái)新的設(shè)備。
  2. 跟蹤硬件層次上的設(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)注明出處。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容