Android 如何获取有效的DeviceId

简介: 从 Android 10 开始,应用必须具有 READ_PRIVILEGED_PHONE_STATE 特许权限才能访问设备的不可重置标识符(包含 IMEI 和序列号)。而这个权限是系统权限,也就是说一般应用将无法再获取IMEI 和序列号

Android 10上的DeviceId


从 Android 10 开始,应用必须具有 READ_PRIVILEGED_PHONE_STATE 特许权限才能访问设备的不可重置标识符(包含 IMEI 和序列号)。

而这个权限是系统权限,也就是说一般应用将无法再获取IMEI 和序列号

受影响的方法包括:

Build

  • getSerial()

TelephonyManager

  • getImei()
  • getDeviceId()
  • getMeid()
  • getSimSerialNumber()
  • getSubscriberId()


如果您的应用没有该权限,但您仍尝试查询不可重置标识符的相关信息,则平台的响应会因目标 SDK 版本而异:


  • 如果应用以 Android 10 或更高版本为目标平台,则会发生 SecurityException。
  • 如果应用以 Android 9(API 级别 28)或更低版本为目标平台,则相应方法会返回 null 或占位符数据(如果应用具有 READ_PHONE_STATE 权限)。否则,会发生 SecurityException。


google也给出了一个解决方案

许多使用场景都不需要不可重置的设备标识符。例如,如果您的应用将不可重置的设备标识符用于广告跟踪或用户分析目的,请为这些特定使用场景使用 Android 广告 ID。要了解详情,请参阅唯一标识符的最佳做法。

这里大部分方案对国内无效,比如广告ID,需要google play的服务,但是国内的手机上都阉割掉了。所以我们只能参考一些可用的方案。


解读官方唯一标识符建议


这部分我们一天天来看官方唯一标识的建议


使用广告 ID


国内就不要考虑了,需要依赖google play服务


使用实例 ID 和 GUID


只对单一应用有效,卸载了就变了,不可取。


不要使用 MAC 地址


MAC 地址具有全局唯一性,无法由用户重置,在恢复出厂设置后也不会变化。因此,一般不建议使用 MAC 地址进行任何形式的用户标识。运行 Android 10(API 级别 29)和更高版本的设备会报告不是设备所有者应用的所有应用的随机化 MAC 地址。

在 Android 6.0(API 级别 23)到 Android 9(API 级别 28)中,无法通过第三方 API 使用 Wi-Fi 和蓝牙等本地设备 Mac 地址。WifiInfo.getMacAddress() 方法和 BluetoothAdapter.getDefaultAdapter().getAddress() 方法都返回 02:00:00:00:00:00。

此外,在 Android 6.0 到 Android 9 版本中,您还必须拥有下列权限,才能访问通过蓝牙和 Wi-Fi 扫描获得的附近外部设备的 MAC 地址:

方法/属性 所需权限
WifiManager.getScanResults() ACCESS_FINE_LOCATION 或 ACCESS_COARSE_LOCATION
BluetoothDevice.ACTION_FOUND ACCESS_FINE_LOCATION 或 ACCESS_COARSE_LOCATION
BluetoothLeScanner.startScan(ScanCallback) ACCESS_FINE_LOCATION 或 ACCESS_COARSE_LOCATION

所以,mac是仅次于DeviceId的靠谱的标识,不过android 6.0之后获取不到了。不过有其他方法完善,见后面。


标识符特性


一堆废话


常见用例和适用的标识符


也是一堆废话,要么就是国内无法使用,不过提到了SSAID。

SSAID,即ANDROID_ID(Settings.Secure.ANDROID_ID),在8.0系统迎来改变,具体如下:

对于在 OTA 之前安装到某个版本 Android 8.0(API 级别 26)的应用,除非在 OTA 后卸载并重新安装,否则 ANDROID_ID 的值将保持不变。要在 OTA 后在卸载期间保留值,开发者可以使用密钥/值备份关联旧值和新值。

对于安装在运行 Android 8.0 的设备上的应用,ANDROID_ID 的值现在将根据应用签署密钥和用户确定作用域。应用签署密钥、用户和设备的每个组合都具有唯一的 ANDROID_ID 值。因此,在相同设备上运行但具有不同签署密钥的应用将不会再看到相同的 Android ID(即使对于同一用户来说,也是如此)。

只要签署密钥相同(并且应用未在 OTA 之前安装到某个版本的 O),ANDROID_ID 的值在软件包卸载或重新安装时就不会发生变化。

即使系统更新导致软件包签署密钥发生变化,ANDROID_ID 的值也不会变化。

可以看到8.0之后ANDROID_ID是与应用签名关联的,同签名的应用共用相同的ANDROID_ID,而且卸载重装不会变化。

而8.0之前,ANDROID_ID是与设备关联的,当设备首次启动时,系统会随机生成一个64位的数字,并以16进制字符串的形式保存到手机系统中,当手机恢复出厂设置后,Android ID会被重置,这是Android ID与Device ID的主要区别。当然还有其他bug,比如有些厂家获取为null之类的。

所以,ANDROID_ID是可以考虑的选择之一,后面细说。


解决方案


想要一个行为获取稳定的DeviceId是不可能的,我们需要多个行为结合处理。


DeviceId


首先就是传统的DeviceId,在Android 10一下还是很稳定的。


ANDROID_ID


在Android 8.0之后,就可以考虑用ANDROID_ID来代替DeviceId了。


Settings.System.getString(BaseApp.getAppContext().getContentResolver(), Settings.Secure.ANDROID_ID);
复制代码


这样可以做一个版本判断,低于10.0(或8.0)获取DeviceId,否则获取ANDROID_ID


Mac地址


如果上面两步获取的还是null,那么可以使用mac地址,但是mac由于6.0之后无法通过WifiInfo.getMacAddress()获取了,所以我们需要处理一下,代码如下:


public static String getMac(Context context) {
    String mac = "";
    if (context == null) {
        return mac;
    }
    if (Build.VERSION.SDK_INT < 23) {
        mac = getMacBySystemInterface(context);
    } else {
        mac = getMacByJavaAPI();
        if (TextUtils.isEmpty(mac)){
            mac = getMacBySystemInterface(context);
        }
    }
    return mac;
}
@TargetApi(9)
private static String getMacByJavaAPI() {
    try {
        Enumeration<NetworkInterface> interfaces = NetworkInterface.getNetworkInterfaces();
        while (interfaces.hasMoreElements()) {
            NetworkInterface netInterface = interfaces.nextElement();
            if ("wlan0".equals(netInterface.getName()) || "eth0".equals(netInterface.getName())) {
                byte[] addr = netInterface.getHardwareAddress();
                if (addr == null || addr.length == 0) {
                    return null;
                }
                StringBuilder buf = new StringBuilder();
                for (byte b : addr) {
                    buf.append(String.format("%02X:", b));
                }
                if (buf.length() > 0) {
                    buf.deleteCharAt(buf.length() - 1);
                }
                return buf.toString().toLowerCase(Locale.getDefault());
            }
        }
    } catch (Throwable e) {
    }
    return null;
}
private static String getMacBySystemInterface(Context context) {
    if (context == null) {
        return "";
    }
    try {
        WifiManager wifi = (WifiManager) context.getSystemService(Context.WIFI_SERVICE);
        if (checkPermission(context, Manifest.permission.ACCESS_WIFI_STATE)) {
            WifiInfo info = wifi.getConnectionInfo();
            return info.getMacAddress();
        } else {
            return "";
        }
    } catch (Throwable e) {
        return "";
    }
}
复制代码

可以看到6.0即23以下直接获取,否则先通过NetworkInterface获取,获取不到再通过原方法获取。 目前来看这一步还是能稳定获取的。


UUID


兜底行为。因为需要我们手动生成,且每次生成的都不一样。


UUID.randomUUID().toString()
复制代码

所以必须生成一次保存起来。这样就有一个问题,如果保存到应用内部存储,卸载后重装一定要重新生成,这样就无法判断是同一设备了。

所以最好将其保存到外部存储,保证卸载重装后还能读取到上次的值。

这样一般情况下是最稳定的,除非手动删除该文件。

所以最好的方案,就是将上面四个方案融合在一起,一个个兜底。目前来看,各手机厂商的指导方案也就这几个方案。


补充


除了上面的方案,还有移动安全联盟(信通院牵头)提供的sdk,可以获取几种设备标识符,大部分国内厂商都支持。

不过需要申请使用,还没测试过。


目录
相关文章
|
Android开发
Android 获取手机 Imei 和 DeviceId
从 Android 10 开始,应用必须具有 READ_PRIVILEGED_PHONE_STATE 特许权限才能访问设备的不可重置标识符(包含 IMEI 和序列号)。
1690 0
|
2天前
|
搜索推荐 Android开发 开发者
探索安卓开发中的自定义视图:打造个性化UI组件
【10月更文挑战第39天】在安卓开发的世界中,自定义视图是实现独特界面设计的关键。本文将引导你理解自定义视图的概念、创建流程,以及如何通过它们增强应用的用户体验。我们将从基础出发,逐步深入,最终让你能够自信地设计和实现专属的UI组件。
|
3天前
|
Android开发 Swift iOS开发
探索安卓与iOS开发的差异和挑战
【10月更文挑战第37天】在移动应用开发的广阔舞台上,安卓和iOS这两大操作系统扮演着主角。它们各自拥有独特的特性、优势以及面临的开发挑战。本文将深入探讨这两个平台在开发过程中的主要差异,从编程语言到用户界面设计,再到市场分布的不同影响,旨在为开发者提供一个全面的视角,帮助他们更好地理解并应对在不同平台上进行应用开发时可能遇到的难题和机遇。
|
5天前
|
XML 存储 Java
探索安卓开发之旅:从新手到专家
【10月更文挑战第35天】在数字化时代,安卓应用的开发成为了一个热门话题。本文旨在通过浅显易懂的语言,带领初学者了解安卓开发的基础知识,同时为有一定经验的开发者提供进阶技巧。我们将一起探讨如何从零开始构建第一个安卓应用,并逐步深入到性能优化和高级功能的实现。无论你是编程新手还是希望提升技能的开发者,这篇文章都将为你提供有价值的指导和灵感。
|
3天前
|
存储 API 开发工具
探索安卓开发:从基础到进阶
【10月更文挑战第37天】在这篇文章中,我们将一起探索安卓开发的奥秘。无论你是初学者还是有经验的开发者,这篇文章都将为你提供有价值的信息和建议。我们将从安卓开发的基础开始,逐步深入到更复杂的主题,如自定义组件、性能优化等。最后,我们将通过一个代码示例来展示如何实现一个简单的安卓应用。让我们一起开始吧!
|
4天前
|
存储 XML JSON
探索安卓开发:从新手到专家的旅程
【10月更文挑战第36天】在这篇文章中,我们将一起踏上一段激动人心的旅程,从零基础开始,逐步深入安卓开发的奥秘。无论你是编程新手,还是希望扩展技能的老手,这里都有适合你的知识宝藏等待发掘。通过实际的代码示例和深入浅出的解释,我们将解锁安卓开发的关键技能,让你能够构建自己的应用程序,甚至贡献于开源社区。准备好了吗?让我们开始吧!
15 2
|
5天前
|
Android开发
布谷语音软件开发:android端语音软件搭建开发教程
语音软件搭建android端语音软件开发教程!
|
13天前
|
编解码 Java Android开发
通义灵码:在安卓开发中提升工作效率的真实应用案例
本文介绍了通义灵码在安卓开发中的应用。作为一名97年的聋人开发者,我在2024年Google Gemma竞赛中获得了冠军,拿下了很多项目竞赛奖励,通义灵码成为我的得力助手。文章详细展示了如何安装通义灵码插件,并通过多个实例说明其在适配国际语言、多种分辨率、业务逻辑开发和编程语言转换等方面的应用,显著提高了开发效率和准确性。
|
12天前
|
Android开发 开发者 UED
安卓开发中自定义View的实现与性能优化
【10月更文挑战第28天】在安卓开发领域,自定义View是提升应用界面独特性和用户体验的重要手段。本文将深入探讨如何高效地创建和管理自定义View,以及如何通过代码和性能调优来确保流畅的交互体验。我们将一起学习自定义View的生命周期、绘图基础和事件处理,进而探索内存和布局优化技巧,最终实现既美观又高效的安卓界面。
25 5
|
10天前
|
JSON Java Android开发
探索安卓开发之旅:打造你的第一个天气应用
【10月更文挑战第30天】在这个数字时代,掌握移动应用开发技能无疑是进入IT行业的敲门砖。本文将引导你开启安卓开发的奇妙之旅,通过构建一个简易的天气应用来实践你的编程技能。无论你是初学者还是有一定经验的开发者,这篇文章都将成为你宝贵的学习资源。我们将一步步地深入到安卓开发的世界中,从搭建开发环境到实现核心功能,每个环节都充满了发现和创造的乐趣。让我们开始吧,一起在代码的海洋中航行!