Flutter Android设备唯一标识符获取方案与避坑指南
2025-03-18 03:54:26
Flutter 获取 Android 设备唯一标识符:踩坑与解决方案
最近用 Flutter 开发应用,需要获取 Android 设备的唯一标识符。 我一开始用了 DeviceInfoPlugin,发现 androidInfo.id
获取的并不是期望的那个设备 ID (类似 IMEI 的东西),而是其他信息。 这坑踩得猝不及防, 经过一番折腾,整理出一些靠谱的解决方案,分享给大家。
一、问题:DeviceInfoPlugin
的 androidInfo.id
是啥?
DeviceInfoPlugin
插件提供的 androidInfo.id
实际上是 ANDROID_ID
,这是一个 64 位的数字(以十六进制字符串表示)。
ANDROID_ID
的特点:
- 在设备首次启动并进行设置后生成。
- 通常情况下,对于同一个设备来说,
ANDROID_ID
是保持不变的。 - 重要问题: 在设备恢复出厂设置后,
ANDROID_ID
会被重置! - 另一个问题: 某些设备或定制 ROM 可能存在 bug,导致
ANDROID_ID
不可靠,甚至不同应用获取到的值不同!
所以,直接用 androidInfo.id
作为设备唯一标识符,可能会出现问题!尤其在用户恢复出厂设置后, 设备标识符会变, 这对于需要持久、稳定标识的场景 (比如设备绑定、防作弊等) 来说是不可接受的。
二、靠谱的解决方案
下面提供几种获取 Android 设备唯一标识符的更可靠的方案,并根据具体场景推荐使用。
1. android_id
(ANDROID_ID)
原理和作用:
这是 Android 系统提供的一个标识符,相对容易获取,但有上面提到的缺点 (恢复出厂设置后改变,部分设备可能不可靠)。
代码示例:
import 'package:device_info_plus/device_info_plus.dart';
import 'package:flutter/foundation.dart';
Future<String?> getAndroidId() async {
if (kIsWeb) return null; // 网页端无法获取
try {
DeviceInfoPlugin deviceInfo = DeviceInfoPlugin();
AndroidDeviceInfo androidInfo = await deviceInfo.androidInfo;
return androidInfo.id; //这就是 ANDROID_ID
} catch (e) {
debugPrint("获取 ANDROID_ID 失败: $e");
return null;
}
}
//使用
// String? deviceId = await getAndroidId();
适用场景:
- 对设备标识符的持久性、唯一性要求不高的场景。
- 作为其他方案的补充,可以先尝试获取
android_id
,失败再用其他方案。
安全建议:
- 不要单独依赖
android_id
进行关键业务逻辑。 - 如果需要更高安全性,考虑结合其他标识符或使用服务器生成的唯一标识符。
2. 使用 flutter_udid
插件
原理和作用:
flutter_udid
插件通过原生代码获取更底层的设备信息, 经过处理后生成一个相对独特的标识符, 相对于android_id
更加稳定可靠. 这个库的目标是在平台限制允许的情况下生成持久的唯一标识符。
它首先尝试获取各种硬件标识符(如序列号等), 然后使用这些信息以及 android_id
生成一个哈希值。
代码示例:
import 'package:flutter_udid/flutter_udid.dart';
Future<String> getUdid() async {
String udid;
try {
udid = await FlutterUdid.consistentUdid;
print('flutter_udid 产生的ID是: $udid');
} catch (e) {
udid = 'Failed to get UDID';
print('无法获得UDID $e');
}
return udid;
}
//使用:
// String myUdid = await getUdid();
适用场景:
- 需要比较稳定的设备标识符,且对轻微的误差容忍度较高。
- 应用需要长期跟踪同一台设备。
安全建议:
- 了解这个库的生成机制,清楚可能出现的重复情况(虽然概率很低)。
- 不要过分依赖它作为绝对的唯一标识。
3. IMEI/MEID (仅限有电话功能的设备)
原理和作用:
IMEI(国际移动设备识别码)和 MEID(移动设备识别码)是分配给具有电话功能的移动设备的唯一识别码。它们通常用于跟踪被盗设备和管理网络访问。 获取 IMEI/MEID 需要相应的权限。
重要提示:
- 从 Android 10(API 级别 29)开始,应用获取 IMEI/MEID 受到更严格的限制,只有具有特殊权限的应用才能访问。
- 用户可能因为隐私顾虑而拒绝授予权限。
- 一些没有电话功能的设备(比如平板)没有 IMEI/MEID。
代码示例 (仅供参考,高版本 Android 可能需要特殊权限):
import 'package:permission_handler/permission_handler.dart';
Future<String?> getImei() async {
var status = await Permission.phone.request();
if (status.isGranted) {
// DeviceInfoPlus 插件本身无法获取 IMEI,这里仅为逻辑演示
// 你可能需要借助原生代码或者专门处理 IMEI 的插件
// 例如 使用 MethodChannel 调用原生代码。
print('已经获得了权限, 可以获取IMEI');
// ... 这里补充获取 IMEI 的代码 ...
return "IMEI-Placeholder"; // 用实际 IMEI 替换
}
else
{
print('无法获得phone权限');
return null; //用户拒绝了权限或者获取失败.
}
}
使用方法通道调用原生代码(进阶):
-
在 Flutter 中定义 MethodChannel:
static const platform = MethodChannel('com.example.myapp/imei'); // 频道名 Future<String?> getImeiViaMethodChannel() async { try { final String? imei = await platform.invokeMethod('getImei'); return imei; } on PlatformException catch (e) { print("无法获取IMEI: ${e.message}"); return null; } }
-
在 Android 原生代码(Kotlin)中实现:
// MainActivity.kt import io.flutter.embedding.android.FlutterActivity import io.flutter.embedding.engine.FlutterEngine import io.flutter.plugin.common.MethodChannel import android.content.Context import android.telephony.TelephonyManager class MainActivity: FlutterActivity() { private val CHANNEL = "com.example.myapp/imei" // 与 Flutter 中的频道名一致 override fun configureFlutterEngine(flutterEngine: FlutterEngine) { super.configureFlutterEngine(flutterEngine) MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL).setMethodCallHandler { call, result -> if (call.method == "getImei") { val imei = getDeviceIMEI() if (imei != null) { result.success(imei) } else { result.error("UNAVAILABLE", "IMEI not available", null) } } else { result.notImplemented() } } } private fun getDeviceIMEI(): String? { //在较新版本Android上运行时, 这里要进行运行时权限检查. if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M && checkSelfPermission(android.Manifest.permission.READ_PHONE_STATE) != android.content.pm.PackageManager.PERMISSION_GRANTED) { return null //没有权限. } val telephonyManager = getSystemService(Context.TELEPHONY_SERVICE) as TelephonyManager return try { if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) { telephonyManager.imei } else{ @Suppress("DEPRECATION") telephonyManager.deviceId } } catch (e: SecurityException) { //如果出现SecurityException 依然是没有获得权限 null } } }
适用场景:
- 极少数对唯一性要求极高,且用户允许获取电话权限的情况。
安全建议:
- 在请求权限时,清晰地向用户解释为什么需要这个权限。
- 尊重用户的隐私选择,如果用户拒绝权限,不要强行获取。
4. Firebase Installation ID
原理与作用:
Firebase 安装 ID(FID)是 Firebase 提供的、用于标识应用安装实例的唯一标识符。它在应用首次启动时生成, 并与 Firebase 项目关联。
优点:
* 相对稳定, 通常只在卸载重装, 清除应用数据, 或者一些特殊情况下(如 Firebase 内部刷新) 才会改变.
* 对开发者比较友好, 不需要特殊权限。
代码示例:
import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_installations/firebase_installations.dart';
//首先, 确保在 main() 方法里 初始化 Firebase
// WidgetsFlutterBinding.ensureInitialized();
// await Firebase.initializeApp();
Future<String> getFirebaseInstallationId() async {
try {
String fid = await FirebaseInstallations.instance.getId();
print("Firebase Installation ID: $fid");
return fid;
} catch (e) {
print("Error getting Firebase Installation ID: $e");
return "Error: $e";
}
}
//使用:
// String fid = await getFirebaseInstallationId();
适用场景:
- 使用 Firebase 服务的应用(如 Firebase Cloud Messaging、Firebase Analytics 等)。
- 需要一个跨设备恢复的应用安装级别标识符。
安全建议:
- 不要将 FID 当做用户ID 来用, 因为在不同设备上,同一用户的同一应用会有不同的 FID。
5. 自己生成并存储
原理与作用:
在应用第一次启动时,使用 UUID(通用唯一识别码)生成一个随机的标识符,并将其存储到设备的本地存储中(如 SharedPreferences)。
之后每次启动都读取这个已存储的值. 保证多次启动使用相同的标识.
代码示例:
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
deviceId = const Uuid().v4();
await prefs.setString('device_id', deviceId);
print('首次生成ID: $deviceId');
}
else{
print('已存在的ID是: $deviceId');
}
return deviceId;
}
//使用:
// String myDeviceId = await getOrCreateDeviceId();
适用场景:
- 需要自己完全控制标识符的生成和存储逻辑。
- 需要一个非常简单的解决方案,不需要复杂的原生交互。
安全建议:
- 注意,如果用户清除了应用数据,存储在
SharedPreferences
中的标识符也会丢失, 此时会生成新的. 如果希望防止清除应用数据后变化, 可以考虑存在 KeyChain/KeyStore。
6. 使用 Keychain/KeyStore (进阶)
原理与作用
将生成的 UUID 存储到 Android 的 KeyStore 或 iOS 的 Keychain 中。这是一种更安全的存储方式,即使用户清除应用数据,这个标识符也能保存。
代码示例(使用flutter_secure_storage
插件):
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:uuid/uuid.dart';
Future<String> getOrCreateDeviceIdSecurely() async {
const storage = FlutterSecureStorage();
String? deviceId = await storage.read(key: 'device_id');
if (deviceId == null) {
deviceId = const Uuid().v4();
await storage.write(key: 'device_id', value: deviceId);
print("Securely stored new device ID: $deviceId");
}
else{
print('从 KeyChain/KeyStore 恢复的ID是: $deviceId');
}
return deviceId;
}
//使用:
// String secureId = await getOrCreateDeviceIdSecurely();
#####适用场景:
- 需要高度安全和持久性,即使用户清除应用数据也不会改变的唯一标识.
#####安全建议:
- KeyChain/KeyStore 的具体实现依赖于平台和设备, 可能在某些设备上行为不一致, 注意兼容性测试.
三、总结
获取 Android 设备的唯一标识符, 没有一招鲜吃遍天的方法。 选择哪种方案,需要根据实际需求来决定, 充分考虑各种方案的优缺点和适用场景, 并结合一些安全建议, 选择最适合的方案。
一般来说, 对于不太重要的场景, 可以使用 android_id
或 flutter_udid
;如果集成了 Firebase, Firebase Installation ID
也是个不错的选择;如果对安全性、持久性要求非常高,推荐使用 KeyChain/KeyStore 存储自己生成的 UUID;IMEI 尽量不要用了(各种限制)。