返回

Flutter设备ID变动问题排查与解决方案(含持久化)

Android

Flutter 应用获取设备唯一标识符,ID 变动问题排查与解决方案

开发 Flutter 应用时,获取设备唯一标识符是一个常见的需求,比如用于用户识别、设备绑定等场景。 但使用 device_info_plus 插件获取的设备 ID,在某些设备上会发生改变,这让人头疼。这篇文章,咱们来好好聊聊这个问题,并提供一些靠谱的解决方法。

一、 问题:设备 ID 为什么会变?

device_info_plus 插件在 Android 和 iOS 平台上获取设备 ID 的方式不同,导致 ID 变化的根本原因也不同。

1.1 Android 平台

你提供的代码里,在安卓平台使用的是 androidInfo.id 。 这个值实际上对应的是 ANDROID_ID

ANDROID_ID 有以下特点:

  • 在设备首次启动并进行设置后生成。
  • 通常情况下,在设备生命周期内保持不变。

但是,ANDROID_ID 保证绝对唯一,且在以下情况会发生改变:

  • 恢复出厂设置: 设备恢复出厂设置后,ANDROID_ID 会被重置。
  • Root 权限修改: 具有 Root 权限的应用可以修改 ANDROID_ID
  • 系统 Bug 或厂商定制: 某些设备或 ROM 可能存在 Bug,导致 ANDROID_ID 不稳定。
  • Android 8.0 (API 级别 26) 及以上: ANDROID_ID 的作用域是每个应用和每个用户的。 相同设备上的不同应用获取的 ANDROID_ID 会不同; 对于多用户系统,每个用户的同一个应用获取的也不一样。

1.2 iOS 平台

iOS 平台上,代码里使用 iosInfo.identifierForVendor。 这个值(identifierForVendor)有如下特点:

  • 同一厂商的所有 App 获取到的 identifierForVendor 相同。
  • 如果用户卸载了厂商的所有 App,然后再重新安装,identifierForVendor 会改变。
  • 删除应用再重新安装这个值可能就会变(如果设备上还有同个开发者的app则不会变)。

正因为以上这些原因,在一些设备或者操作之后会导致 ID 发生改变,因此我们需要进一步处理。

二、 解决设备 ID 变化问题

既然直接使用 androidInfo.idiosInfo.identifierForVendor 存在问题,那我们还有其他选择。 下面提供几个可选方案。

2.1 使用 android_id 插件(仅 Android)

如果只需要在 Android 平台获取相对稳定的 ID,可以考虑使用 android_id 插件。它直接提供了获取 ANDROID_ID 的方法。

原理:

android_id 插件直接通过 Flutter 的 Platform Channel 调用 Android 原生的 Settings.Secure.getString 方法来获取 ANDROID_ID

代码示例:

import 'package:android_id/android_id.dart';

Future<String?> getAndroidId() async {
  const _androidId = AndroidId();
  final String? androidId = await _androidId.getId();
  return androidId;
}

使用:

String? deviceId = await getAndroidId();
print(deviceId);

注意点:

  • 这个方法获取到的还是ANDROID_ID,还是有上面提到的那些限制, 还是会在恢复出厂设置后变更等情况。

2.2 使用 flutter_udid 插件

flutter_udid 这个插件尝试提供一个更加持久化的设备唯一ID。

原理:

  • Android 平台:
    • 它首先尝试获取 ANDROID_ID
    • 如果获取到的ANDROID_ID为null,或者为一些已知的无效值(例如 "9774d56d682e549c"),它会生成一个随机的 UUID,并存储到应用的私有存储空间。
    • 后续获取时,如果能从私有存储空间读取到之前保存的 UUID,则直接返回;否则,返回获取到的ANDROID_ID或新生成的 UUID。
  • iOS 平台:
    • 它会先尝试从 Keychain 获取之前存储的 UUID。
    • 如果 Keychain 中没有,则生成一个新的 UUID,并保存到 Keychain 中。
    • flutter_udid使用 Keychain 来实现数据持久化,可以做到应用卸载重装也可以获得原来的值(只要不手动清理 Keychain,这个 ID 基本不变).

代码示例:

import 'package:flutter_udid/flutter_udid.dart';

Future<String> getUdid() async {
  String udid = await FlutterUdid.udid;
  return udid;
}

使用

  String deviceUdid= await getUdid();
  print(deviceUdid);

进阶-自定义gettersetter

如果默认存储方式不满足你的需求, flutter_udid也支持自定义数据存储方法,通过自定义gettersetter,可以使用其他方式来实现设备 ID 的持久化。

import 'package:flutter_udid/flutter_udid.dart';
import 'package:shared_preferences/shared_preferences.dart';

Future<String> getCustomUdid() async {

  String udid = await FlutterUdid.consistentUdid(
    getter: () async {
      // 自定义的 getter, 使用 SharedPreferences 读取
        final prefs = await SharedPreferences.getInstance();
        return prefs.getString('my_custom_udid');

    },
    setter: (String udid) async {
      // 自定义的 setter, 使用 SharedPreferences 保存
        final prefs = await SharedPreferences.getInstance();
        await prefs.setString('my_custom_udid', udid);
    },
  );
  return udid;
}

注意:

  • iOS平台如果清空KeyChain, 数据会丢失.

2.3 使用 Firebase Instance ID (FID)

如果你的应用集成了 Firebase,可以考虑使用 Firebase Instance ID (FID)。

原理:

  • FID 由 Firebase 服务生成,并与你的 Firebase 项目关联。
  • 每个应用实例都有一个唯一的 FID。
  • FID 具有较好的持久性,但用户清除应用数据或长时间未使用应用可能导致 FID 改变。

代码示例:(需先配置 Firebase)

import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_installations/firebase_installations.dart';

Future<String?> getFirebaseInstallationId() async {
  try {
    // 确保 Firebase 已经初始化
   if (Firebase.apps.isEmpty) {
       await Firebase.initializeApp(); //根据你的项目配置初始化Firebase
    }

    String fid = await FirebaseInstallations.instance.getId();
    return fid;
  } catch (e) {
    print("获取 Firebase Installation ID 失败: $e");
    return null;
  }
}

使用:

  String? fid = await getFirebaseInstallationId();
    if(fid!=null){
       print(fid);
    }

安全性建议:

  • FID 与 Firebase 项目绑定,应妥善保管 Firebase 配置文件,防止泄露。

2.4 自建方案:生成 UUID 并持久化存储

终极方案:自己生成一个 UUID,并想办法持久化存储起来。

原理:

  • 使用 uuid 包生成一个随机的 UUID。
  • 选择一个合适的存储方案:
    • Android: 可以使用 shared_preferences、文件存储(应用私有目录)、或者 KeyStore (更安全,但更复杂)。
    • iOS: 可以使用 shared_preferences、文件存储(应用沙盒目录)、或者 Keychain (更安全)。

代码示例(使用 shared_preferencesuuid):

import 'package:uuid/uuid.dart';
import 'package:shared_preferences/shared_preferences.dart';

Future<String> getOrCreateDeviceId() async {
  final prefs = await SharedPreferences.getInstance();
  String? deviceId = prefs.getString('device_id');

  if (deviceId == null) {
    deviceId = const Uuid().v4(); // 生成 UUID v4
    await prefs.setString('device_id', deviceId);
  }

  return deviceId;
}

安全性建议:

  • 如果对安全性要求较高,建议使用 KeyStore (Android) 或 Keychain (iOS) 存储 UUID。

三、方案对比与选择

方案 优点 缺点 适用场景
android_id 插件 简单易用,直接获取 ANDROID_ID ANDROID_ID 存在不唯一、可被修改、8.0及以上系统ID有变化的问题。 仅需 Android 平台,对 ID 唯一性要求不高的场景。
flutter_udid 插件 跨平台,尝试提供更持久的 ID,也提供自定义的存储方式。 依赖于内部实现的持久化策略。iOS 清理 KeyChain 会丢失. 需要跨平台支持,对 ID 持久性有较高要求的场景。
Firebase Instance ID 由 Firebase 服务生成,与 Firebase 项目关联,较稳定。 依赖 Firebase 服务, 用户清除数据会导致ID改变 已集成 Firebase 的应用。
自建方案:UUID + 持久化 灵活性最高,可根据需求选择存储方案。 需要自己实现持久化逻辑,代码相对复杂。 对 ID 持久性、安全性有特殊要求的场景,或需自定义存储逻辑的场景。
选择哪种方案取决于你的具体需求,例如是否需要跨平台、对 ID 持久性和安全性的要求、是否已集成 Firebase 等。没有绝对的最佳方案,只有最适合你的方案。根据具体情况选择就好。