谈谈 Android 中的各种设备标识符

在上一篇谷歌博客 (识别应用安装) 中,谷歌介绍了Android 中一些常用的标识符,并提出了合理识别应用每次安装的办法。指出通过获取设备可靠,唯一,稳定标识符来追踪设备可能产生的错误,并简单介绍了 Android 中一些设备标识符可能存在的问题。今天,我会介绍一下 Android 中的一些标识符以及如何获取它们,以及获取这些标识符过程中可能存在的坑。

标识符(identifier)

设备ID(DeviceId)

  • 获取办法
android.telephony.TelephonyManager tm = (android.telephony.TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE);
String deviceId = tm.getDeviceId();
  • 当设备为手机时,返回设备的唯一ID。手机制式为 GSM 时,返回手机的 IMEI 。手机制式为 CDMA 时,返回手机的 MEID 或 ESN 。
  • 非电话设备或者 Device ID 不可用时,返回 null .
  • 属于比较稳定的设备标识符。
  • 需要 READ_PHONE_STATE 权限。 (Android 6.0 以上需要用户手动赋予该权限)。
  • 某些设备上该方法存在 Bug ,返回的结果可能是一串0或者一串*号。

Sim 序列号(Sim Serial Number)

  • 获取办法:
android.telephony.TelephonyManager tm = (android.telephony.TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE);
String simSerialNum = tm.getSimSerialNumber();
  • 不同 Sim 卡的序列号不同.
  • Sim 卡序列号,当手机上装有 Sim 卡并且可用时,返回该值。手机未装 Sim 卡或者不可用时,返回 null.
  • 需要 READ_PHONE_STATE 权限。 (Android 6.0 以上需要用户手动赋予该权限)

Mac 地址(Mac Address)

  • 获取办法:
android.net.wifi.WifiManager wifi = (android.net.wifi.WifiManager) context.getSystemService(Context.WIFI_SERVICE);
String macAddress = wifi.getConnectionInfo().getMacAddress();
  • 没有 WiFi 硬件或者 WiFi 不可用的设备可能返回 null 或空,注意判空.
  • 比较稳定的硬件标识符。
  • 需要 ACCESS_WIFI_STATE 权限。
  • Android 6.0开始,谷歌为保护用户数据,用此方法获取到的 Wi-Fi mac 地址都为02:00:00:00:00:00更多信息查看此处
  • 如果 app 在装有谷歌框架的设备中读取了mac地址,会被谷歌检测为有害应用提示用户卸载。这也是为什么像友盟、TalkingData 等数据统计 sdk 提供商专门针对 Google Play 提供特供版的 sdk.
读取 mac 地址导致 app 被谷歌框架判定为有害应用

设备序列号(Serial Number, SN)

  • 获取办法:
String serialNum = android.os.Build.SERIAL;
  • 比较稳定的设备硬件标识符,在上一篇文章中谷歌也未提到有啥缺点。

ANDROID_ID

  • 获取办法:
String androidId = android.provider.Settings.Secure.getString(context.getContentResolver(), android.provider.Settings.Secure.ANDROID_ID);
  • 在设备第一次启动的时候生成并保存,并且可能会在恢复出厂设置后重置该值。理论上是大部分是重置的。(API 中原话是:The value may change if a factory reset is performed on the device.)
  • 在 Android 2.2 中不可靠.
  • 部分设备由于制造商错误实现,导致会返回相同的 Android_ID.
  • 在 Android 4.2 及以上, 设备启用多用户功能后,每个用户的 Android_ID 不相同.

制造商 (Manufacturer)

  • 获取办法:
String manufacturer = android.os.Build.MANUFACTURER;

型号(Model)

  • 获取办法:
String model = android.os.Build.MODEL;

品牌(Brand)

  • 获取办法:
String brand = android.os.Build.BRAND;

设备名 (Device)

  • 获取办法:
String device = android.os.Build.DEVICE;

以下是我的一台 Nexus 4 所获取的全部值:
以下的值仅作为举例,并非真实

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

如何合理使用标识符跟踪设备

介绍完了一些常见的、可能的作为标识符的值,现在来谈谈如何合理地使用这些标识符跟踪设备。

首先,我们先要弄清自己跟踪设备的具体需求。目前看来,需求无非两种:

  1. 跟踪用户设备使用周期层次上的设备。
    意思是将每次用户的擦除设备、恢复出厂设置动作后将设备视为一台新的设备。
  2. 跟踪硬件层次上的设备。
    意思是无论设备擦除数据或者恢复出厂设置后都需要将该设备视为同一台设备。

跟踪用户使用层次上的设备

方案 1:
这个层次上的设备跟踪,我比较推荐使用谷歌官方推荐的办法来跟踪, App 首次启动时生成一个 Random UUID 并保存在本地存储,以后每次启动时检查该 UUID 文件。具体可以查看我的上一篇翻译文章),其中有具体的代码实现。

方案 2:
如果你不喜欢谷歌推荐的这种方式,或者觉得这种方式涉及到文件读写太过繁琐等。我们也可以通过以上介绍的这些标识符来跟踪设备。因为需要将设备擦除数据或恢复出厂设置后将其视为一台新的设备,所以需要使用一些与当前用户设备使用周期有关的值。

理论上,Android_ID 这一个值就已经足够我们实现这样的需求,不过正是因为 Android_ID 存在缺陷,所以我们无法直接拿来识别设备。这里我们使用多个值拼凑来规避这些缺点。
与用户设备使用周期有关的标识符我推荐使用Android_ID和Sim Serial Number。另外可以加上Device_ID,通过 UUID 或者 MD5 等来计算生成设备的标识符。

以下是一个简单的实现,参考了 Stack Overflow 上的这个问题下面的回答

  • UUID 实现:
UUID deviceUuid = new UUID(androidId.hashCode(), ((long)deviceId.hashCode() << 32) | simSerialNum.hashCode());
String deviceId = deviceUuid.toString();

结果类似:00000000-54b3-e7c7-0000-000046bffd97

  • 或者你也可以使用 MD5 实现(MD5 算法见下文):
String md5ID = md5(androidId + deviceId + simSerialNum);

结果类似:f87b20b3c359c4af608b3eb26b26a1b8

跟踪硬件层次上的设备

跟踪硬件层次上的设备建议使用硬件的标识符,比如设备ID(DeviceId)、Mac 地址、设备序列号(SN)或者设备的品牌,型号名等,这些值在用户擦除数据或者恢复出厂设置后也不会改变。同样的,为了提升稳定性及排除单一标识符所存在的缺陷,我们使用多个标识符拼接,然后通过 UUID 或者 MD5 算法计算得出我们需要的设备标识符。

以下是一个简单的实现,使用了设备序列号(SN)、设备ID(DeviceId)和 Mac 地址。

拼接后的字符串类似于:01b4549262d6a4a235513602180805610:68:3f:81:ed:ff

同时为了不暴露用户的设备具体信息,这里我们同样采用 MD5 对拼接后的字符串进行Hash操作:

String md5ID = md5("01b4549262d6a4a235513602180805610:68:3f:81:ed:ff");

拼凑的标识符选择,拼接的顺序,MD5或者UUID的选择并无绝对,重要的是思想。

其他

  • 以上这些值在使用前都建议判空。
  • 因为硬件缺失或者不可用,获取标识符过程中也可能返回 null 对象。为了避免 NullPointerException,建议获取标识符操作全部在 try...catch 中操作。
  • 安卓设备的用户不乏极客,修改 Android_ID 或者 Build 文件对他们来说并非难题。所以一定程度上说,没有绝对准确的跟踪设备的标识符。

*以下是一个Demo,项目建立后添加权限后即可使用

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权限
        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权限
        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();
    }
 
    //检查权限,READ_PHONE_STATE在API>=23需要用户手动赋予权限
    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;
    }
}

本文章为原创作品,转载请注明出处。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,847评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,208评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,587评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,942评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,332评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,587评论 1 218
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,853评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,568评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,273评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,542评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,033评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,373评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,031评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,073评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,830评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,628评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,537评论 2 269

推荐阅读更多精彩内容