Flutter设备ID变动问题排查与解决方案(含持久化)
2025-03-16 15:20:23
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.id
和 iosInfo.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);
进阶-自定义getter
和setter
如果默认存储方式不满足你的需求, flutter_udid
也支持自定义数据存储方法,通过自定义getter
和setter
,可以使用其他方式来实现设备 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 (更安全)。
- Android: 可以使用
代码示例(使用 shared_preferences
和 uuid
):
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 等。没有绝对的最佳方案,只有最适合你的方案。根据具体情况选择就好。 |