返回

Flutter Android设备唯一标识符获取方案与避坑指南

Android

Flutter 获取 Android 设备唯一标识符:踩坑与解决方案

最近用 Flutter 开发应用,需要获取 Android 设备的唯一标识符。 我一开始用了 DeviceInfoPlugin,发现 androidInfo.id 获取的并不是期望的那个设备 ID (类似 IMEI 的东西),而是其他信息。 这坑踩得猝不及防, 经过一番折腾,整理出一些靠谱的解决方案,分享给大家。

一、问题:DeviceInfoPluginandroidInfo.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; //用户拒绝了权限或者获取失败.
    }

}
使用方法通道调用原生代码(进阶):
  1. 在 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;
      }
    }
    
  2. 在 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_idflutter_udid;如果集成了 Firebase, Firebase Installation ID 也是个不错的选择;如果对安全性、持久性要求非常高,推荐使用 KeyChain/KeyStore 存储自己生成的 UUID;IMEI 尽量不要用了(各种限制)。