开发工具: 华为云码道

本文配套仓库: CPF-Flutter/fluttertpc_device_uuid
鸿蒙适配后仓库https://atomgit.com/oh-flutter/device_uuid

本文配套仓库:https://atomgit.com/CPF-Flutter/fluttertpc_device_uuid(TAG:0.0.4-ohos-1.0.0-beta.1,分支:main),文中示例代码位于仓库 example/ 目录。

在这里插入图片描述

什么是设备唯一标识? 在用户统计、设备绑定、防刷控、AB 测试场景中,应用需要一个能在同一设备上跨会话保持稳定的标识符。Android 用 ANDROID_ID 经 SHA-1 哈希得到 40 位十六进制串,iOS 用 Keychain 持久化的 XYUUID。鸿蒙系统没有 ANDROID_ID 的等价物,语义最贴近的系统级标识是 OAID(Open Anonymous Device Identifier)——由系统匿名化处理、不包含原始设备信息的稳定设备标识,通过 @ohos.identifier.oaid 模块获取。

把设备唯一标识在应用内一次性获取出来,是用户统计、设备绑定、防刷控场景下的高频需求:读一个 OAID 标识当前设备、首次调用弹出授权对话框、用户拒绝时返回全零值不抛异常、多次调用结果稳定一致。鸿蒙应用同样需要这个能力。本文介绍如何使用鸿蒙化适配后的 Flutter 三方库 device_uuid,用一个 getUUID() 方法在鸿蒙 App 内获取 OAID 匿名设备标识,并附上 OpenHarmony-6.1.1.120 真机的完整实测记录。

一、最终运行效果

应用启动后,首次调用 getUUID() 弹出系统授权对话框(“是否允许应用获取设备标识”),用户点击"允许"后,页面显示 36 位标准 UUID 格式的 OAID 值。页面包含五张卡片:当前 UUID 卡(含格式识别、获取时间、耗时、调用次数、运行平台)、一致性压测卡(5/10/20/50 次调用,验证返回值稳定)、调用历史卡(最近 20 条记录,可滑动删除、点击查看详情)、设置卡(应用回到前台时自动刷新开关)、通道契约卡(各平台返回值语义对照)。

验证点结果
应用启动,Flutter 页面正常渲染五张卡片通过
首次调用 getUUID(),系统弹出 APP_TRACKING_CONSENT 授权对话框通过
用户点击"允许",返回 36 位标准 UUID 格式 OAID通过
用户点击"禁止",返回全零字符串 00000000-0000-0000-0000-000000000000通过
一致性压测 10 次调用,返回值全部一致(unique values: 1)通过
调用历史卡记录每次调用的 UUID、时间戳与耗时通过
应用切到后台再回到前台,自动刷新 UUID(开关开启时)通过
通道契约卡正确展示各平台返回值语义通过
全程仅申请 APP_TRACKING_CONSENT 一个 user_grant 权限通过

KeyHash 示例页 KeyHash 示例页 KeyHash 示例页 KeyHash 示例页 KeyHash 示例页 KeyHash 示例页

我先逐张查看这 6 张截图,再按表 1 / 表 2 格式整理输出。

这 6 张截图实际上是 Device UUID Example 示例页(OpenHarmony,TLR-AL00 6.1.0.135)在不同操作阶段的快照——与上文 SliderGradient 截图不同,因此我按同样的"模块说明 + 状态快照"双表结构整理如下(附件共 6 张,非 7 张)。

表 1 · 页面模块与配置说明

模块关键配置预期表现
设备 UUIDDeviceUuid().getUUID()Future<String?>展示 36 位标准 UUID、格式识别、获取时间、延迟、调用次数、运行平台;提供 Refresh UUID / Copy
一致性测试重复调用 getUUID(),5 / 10 / 20 / 50 档位校验标识符稳定,输出 unique values / errors / avg latency 并判定 PASS / FAIL
调用历史每次调用落盘(UUID、时间戳、延迟),支持 Clear all、点条目查看详情滚动记录最近调用,最新在前
演示设置Auto-refresh on app resumeWidgetsBindingObserver开关开启时应用回到前台自动刷新 UUID
通道契约MethodChannel('device_uuid')getUUID()String?(never throws)展示 Android / iOS·macOS / OpenHarmony 三端标识来源与 OAID 全零说明

表 2 · 各截图实测状态快照

截图时刻设备 UUID 卡(格式 / 获取时间 / 延迟 / 次数 / 平台)一致性测试(档位 / 结果 / unique·errors·avg)调用历史演示设置通道契约
00:2092332691-f9a5-4e5b-8b65-5d4640e498ba · Standard UUID (36 chars) / 00:19:50 / 2420 ms / 1 / ohos10 calls 选中,未运行未滚到未滚到
00:21(a)未显示10 calls 选中,未运行空(No calls yet)开启顶部,仅通道名+方法名(截断)
00:21(b)未显示50 calls,已完成 50/50,PASS · unique:1 · errors:0 · avg:25.1 ms空(No calls yet)开启未显示
00:22未显示未显示空(No calls yet)开启完整三端对照 + OAID 全零说明
00:23(a)未显示PASS · unique:1 · errors:0 · avg:22.7 ms3 条:00:22:47·5ms / 00:22:46·5ms / 00:22:38·840ms;弹出 Call details(Time 00:22:47,Latency 5ms,Result Success)未显示未显示
00:23(b)92332691-… · Standard UUID (36 chars) / 00:23:45 / 37 ms / 6 / ohos50 calls,已完成 50/50,PASS · unique:1 · errors:0 · avg:17.3 ms顶部 1 条:00:23:45 · 37ms未滚到未滚到

走查结论

  • 首次调用(00:20)延迟 2420 ms——含 OAID 权限检查与首次授权路径;授权后的后续调用延迟降至 5–37 ms,符合"首次弹窗、后需即时返回"的预期。
  • 一致性测试在 50 calls 档位下多次运行均 unique values: 1 · errors: 0,标识符跨调用稳定;avg latency 25.1 / 22.7 / 17.3 ms,随授权缓存趋于稳定。
  • 调用历史按时间戳与延迟落盘(5ms / 5ms / 840ms),可清空、可查看详情,与配置说明一致。
  • 通道契约卡完整展示 MethodChannel('device_uuid') / getUUID()String?(never throws),三端语义(ANDROID_ID+SHA-1 / Keychain XYUUID / OAID)及全零 OAID 的 APP_TRACKING_CONSENT 说明正确渲染。
  • 演示设置开关正常,应用生命周期监听项配置就绪。整体行为符合 device_uuid 鸿蒙适配的预期。

鸿蒙技术点:FlutterPage 与 XComponent 渲染管线
鸿蒙侧的 Flutter 渲染入口是 FlutterPage 组件,它在 Index.ets 中被 @Entry 组件的 build() 方法直接使用。FlutterPage 内部封装了 XComponent——OpenHarmony 提供的底层渲染画布组件。XComponent 通过 NAPI 桥接 C++ 引擎层,将 Flutter 的 Skia 渲染管线挂载到鸿蒙的渲染树中,使 Dart 层的 Widget 树(包括五张卡片、ListViewDismissibleChoiceChipLinearProgressIndicator 等全部组件)在鸿蒙设备上完整渲染。FlutterAbility 作为容器 Ability,管理 FlutterEngine 的生命周期,在 configureFlutterEngine 中注册所有平台插件。

二、device_uuid 是什么

device_uuid 原库(pub.dev 0.0.4,作者 thoson-it)是一个获取设备唯一标识的 Flutter 插件。Android 平台返回 ANDROID_ID 经 SHA-1 哈希后的 40 位十六进制串,iOS / macOS 平台返回 Keychain 持久化的设备 UUID(XYUUID)。鸿蒙适配版在其基础上新增了 OpenHarmony / HarmonyOS 平台支持:通过 @ohos.identifier.oaid 获取系统匿名化处理的 OAID,直接返回原始值不做二次哈希。

几个对使用者友好的特点:

  • OAID 匿名标识:鸿蒙系统没有 ANDROID_ID 的等价物,OAID 是语义最贴近的系统级设备标识,由系统匿名化处理,不包含原始设备信息,符合隐私合规要求;
  • 权限自动处理:插件在调用 getUUID() 时检查 APP_TRACKING_CONSENT 授权状态,未授权时自动弹出系统授权对话框,用户拒绝后不抛异常,返回系统给定的全零字符串;
  • 错误吞咽不抛异常:所有异常在原生侧 try/catch 中捕获并返回 null,与 Android 平台的 result.success(null) 契约一致——Dart 侧的 getUUID() 永远不会抛出未捕获异常;
  • 跨平台一套代码:Android、iOS、macOS、鸿蒙共用同一个 DeviceUuid().getUUID() 接口,运行在哪端就按哪端的方式获取设备标识;
  • PlatformInterface 架构:Dart 层采用 plugin_platform_interfacePlatformInterface 抽象,默认实现为 MethodChannelDeviceUuid,各平台通过 MethodChannel('device_uuid') 暴露 getUUID 方法。

接口说明

DeviceUuid 类
名称描述类型参数类型返回值必填鸿蒙平台支持
getUUID获取设备唯一标识方法Future<String?>

各平台返回值语义

平台标识来源返回值格式权限要求失败行为
AndroidANDROID_ID 经 SHA-1 哈希40 位十六进制串返回 null
iOS / macOSKeychain 持久化的 XYUUID36 位标准 UUID返回 null
OpenHarmony / HarmonyOS@ohos.identifier.oaid 的 OAID36 位标准 UUIDAPP_TRACKING_CONSENT(user_grant)返回 null(异常)/ 全零字符串(用户拒绝)

三、环境准备

本文所有实测均在以下环境完成:

版本说明
Flutter(ohos 版)3.44.9+ohos-0.0.1-canary1主验证环境,真机实测
DevEco Studio26.0.0(DS-261.23567.138.36.2600821)构建环境
编译 SDK5.1.0(18)宿主工程 compatibleSdkVersion 同值,保留带括号的旧格式
真机OpenHarmony-6.1.1.120(API 24)ohos-arm64

鸿蒙技术点:compatibleSdkVersion 与 API Level 的对应关系
compatibleSdkVersion 是鸿蒙工程 build-profile.json5 中的关键字段,声明应用的最低兼容 API 版本。鸿蒙的 API 版本与系统版本一一对应:5.1.0(18) 对应 API 18,6.1.0(23) 对应 API 23,6.1.1.120 对应 API 24。真机安装时,系统会校验应用的 compatibleSdkVersion 不高于设备实际 API 版本,否则报"此应用暂不支持在当前设备安装"。本文 example 工程设为 5.1.0(18),在 API 24 真机上可正常安装运行。

两点提醒:

  • 编译鸿蒙目标需要使用 ohos 版 Flutter SDK,普通 Flutter SDK 无法编译 ohos 产物;
  • 若在真机上安装应用报"此应用暂不支持在当前设备安装",是宿主工程的 compatibleSdkVersion 高于设备 API 导致的,与插件无关,处理方式见 FAQ Q3。

四、引入依赖

进入工程目录,在 pubspec.yaml 中添加 git 依赖:

dependencies:
  device_uuid:
    git:
      url: https://atomgit.com/CPF-Flutter/fluttertpc_device_uuid.git
      ref: 0.0.4-ohos-1.0.0-beta.1

执行命令拉取依赖:

flutter pub get

TAG 命名规则:原库版本-ohos-版本号-beta.x

Flutter 框架版本TAG 名称分支名
3.440.0.4-ohos-1.0.0-beta.1main

说明:该 TAG 已在 Flutter 3.44.9+ohos-0.0.1-canary1 + OpenHarmony-6.1.1.120(API 24)真机上实测通过。compatibleSdkVersion 设为 5.1.0(18) 即可在 API 24 真机安装运行。库依赖 plugin_platform_interface ^2.0.2(平台接口抽象),Dart 层 API 与原库完全一致,零改动。

权限声明

OAID 受 ohos.permission.APP_TRACKING_CONSENT(user_grant)权限保护。接入方需在宿主应用的 module.json5 中声明该权限:

"requestPermissions": [
  {
    "name": "ohos.permission.APP_TRACKING_CONSENT",
    "reason": "$string:oaid_permission_reason",
    "usedScene": {
      "abilities": ["EntryAbility"],
      "when": "inuse"
    }
  }
]

对应 resources/base/element/string.json

{
  "string": [
    {
      "name": "oaid_permission_reason",
      "value": "Used to obtain the anonymous device identifier (OAID) for generating the device UUID"
    }
  ]
}

注意:插件 HAR 内声明的权限不会合并进宿主应用,接入方必须在宿主 module.json5 中声明,否则 getUUID() 返回全零字符串。该权限为 user_grant 权限,仅声明不授权同样返回全零字符串——用户拒绝授权时插件不抛异常,仍按契约返回系统给定的值。

五、代码接入

5.1 导入库

import 'package:device_uuid/device_uuid.dart';

导入后即可使用 DeviceUuid 类。该类通过 DeviceUuidPlatform.instance 间接调用 MethodChannelDeviceUuid,经由 MethodChannel('device_uuid') 向原生侧发起 getUUID 方法调用。

5.2 获取设备唯一标识

final deviceUuid = DeviceUuid();
final uuid = await deviceUuid.getUUID();
print('Device UUID: $uuid');

getUUID() 是一个无参异步方法,返回 Future<String?>。在鸿蒙设备上,首次调用会触发系统授权对话框弹出。用户允许后返回 36 位标准 UUID 格式的 OAID;用户拒绝后返回全零字符串 00000000-0000-0000-0000-000000000000;发生异常时返回 null——与 Android 平台的 result.success(null) 契约一致,Dart 侧永远不会收到未捕获异常。

代码逐段分析:DeviceUuid 类与 PlatformInterface 架构
DeviceUuid 是一个极简的包装类,只有一个方法 getUUID(),内部调用 DeviceUuidPlatform.instance.getUUID()DeviceUuidPlatform 是一个抽象类,继承 PlatformInterface,通过 _token 机制防止外部伪造实例。默认实例为 MethodChannelDeviceUuid,它通过 MethodChannel('device_uuid') 向原生侧发起 invokeMethod<String?>('getUUID') 调用。这一架构设计允许各平台通过设置 DeviceUuidPlatform.instance 注入自己的实现,同时保证 Dart 侧 API 调用方式完全一致。

5.3 错误处理

try {
  final uuid = await DeviceUuid().getUUID();
  if (uuid == null) {
    // 原生侧发生异常,返回 null
    print('获取 UUID 失败');
  } else if (uuid == '00000000-0000-0000-0000-000000000000') {
    // 用户拒绝授权,系统返回全零 OAID
    print('用户未授权获取设备标识');
  } else {
    // 正常获取到 OAID
    print('设备 OAID: $uuid');
  }
} on PlatformException catch (e) {
  print('PlatformException ${e.code}: ${e.message}');
} on MissingPluginException {
  print('插件未注册');
} catch (e) {
  print('未知错误: $e');
}

虽然原生侧已经吞咽了所有异常并返回 null,但 Dart 侧仍建议用 try/catch 包裹,处理 PlatformException(通道通信异常)和 MissingPluginException(插件未注册)两种边界情况。业务层可通过判断 null 和全零字符串区分"异常"与"用户拒绝授权"两种状态。

代码逐段分析:MethodChannelDeviceUuid.getUUID 实现
MethodChannelDeviceUuid 继承 DeviceUuidPlatform,持有一个 MethodChannel('device_uuid') 实例。getUUID() 方法调用 methodChannel.invokeMethod<String?>('getUUID') 向原生侧发起方法调用,返回值类型为 String?invokeMethod 是 Flutter 框架提供的平台通道调用方法,它将方法名和参数序列化为二进制消息通过 BinaryMessenger 发送到原生侧,原生侧处理后将结果序列化返回。若原生侧返回 null,Dart 侧收到 null;若原生侧抛出异常,Dart 侧收到 PlatformException

5.4 一致性验证

final Set<String> values = {};
for (int i = 0; i < 10; i++) {
  final value = await DeviceUuid().getUUID();
  if (value != null && value.isNotEmpty) {
    values.add(value);
  }
}
print('唯一值数量: ${values.length}');  // 1 — 标识符稳定
print('一致性: ${values.length <= 1 ? "PASS" : "FAIL"}');

设备标识符必须在同一设备上跨调用稳定——每次调用返回值应完全一致。demo 工程的"一致性压测"卡提供了 5/10/20/50 次调用选项,依次调用 getUUID() 并收集返回值到 Set 中,若 Set 长度为 1 则通过(PASS),否则失败(FAIL)。

鸿蒙技术点:OAID 的稳定性保证
OAID(Open Anonymous Device Identifier)由鸿蒙系统统一分配和管理,具有以下稳定性特征:同一设备上同一应用多次调用返回值一致;同一设备上不同应用获取的 OAID 不同(应用级隔离);用户可在系统设置中重置 OAID(重置后所有应用获取到新值);卸载重装应用后 OAID 不变(由系统层管理,不随应用生命周期变化)。这些特性使 OAID 成为设备统计和用户识别的可靠标识符。

5.5 实战:设备标识管理器

实际业务中常见的场景是应用启动时获取设备标识并缓存,用于后续的用户统计、设备绑定等操作。下面是一个可直接使用的组件:

class DeviceIdentifier {
  static DeviceIdentifier? _instance;
  static DeviceIdentifier get I => _instance ??= DeviceIdentifier._();

  DeviceIdentifier._();

  final DeviceUuid _deviceUuid = DeviceUuid();
  String? _cachedUuid;
  DateTime? _cachedAt;

  String? get uuid => _cachedUuid;
  bool get isAuthorized =>
      _cachedUuid != null &&
      _cachedUuid != '00000000-0000-0000-0000-000000000000';

  Future<String?> initialize() async {
    if (_cachedUuid != null) return _cachedUuid;
    try {
      _cachedUuid = await _deviceUuid.getUUID();
      _cachedAt = DateTime.now();
    } catch (e) {
      _cachedUuid = null;
    }
    return _cachedUuid;
  }

  Future<String?> refresh() async {
    try {
      _cachedUuid = await _deviceUuid.getUUID();
      _cachedAt = DateTime.now();
    } catch (e) {
      _cachedUuid = null;
    }
    return _cachedUuid;
  }
}

DeviceIdentifier 采用单例模式,initialize() 方法在应用启动时调用,首次获取 OAID 后缓存。isAuthorized 属性通过判断返回值是否为全零字符串来区分"已授权"与"未授权"状态。refresh() 方法用于强制刷新(如用户在系统设置中重置 OAID 后)。该组件可直接嵌入应用启动流程。

鸿蒙技术点:WidgetsBindingObserver 与 AppLifecycleState
demo 工程的 _UuidHomePageState 混入了 WidgetsBindingObserver,监听应用生命周期变化。当应用从后台回到前台(AppLifecycleState.resumed)时,若"Auto-refresh on app resume"开关开启,自动调用 _fetchUuid() 刷新 UUID。这一机制在鸿蒙上由 Flutter Engine 的 C++ 层完整支持——FlutterAbility 通过 onForeground / onBackground 回调将鸿蒙 Ability 生命周期转换为 Flutter 的 AppLifecycleStateWidgetsBinding.instance.addObserver 接收并分发到 didChangeAppLifecycleState 回调。

六、运行与验证

以下为 demo 工程的真机实测记录。仓库中 example 的 Bundle Name 为 com.example.device_uuid_example

设备项
机型OpenHarmony 真机
系统版本OpenHarmony-6.1.1.120
API 版本24
构建环境Flutter 3.44.9+ohos-0.0.1-canary1, DevEco Studio 26.0.0

6.1 验证一:首次调用与授权对话框

安装、启动 demo:

# 构建 hap 后安装
hdc install entry-default-signed.hap

# 启动 demo
hdc shell aa start -b com.example.device_uuid_example -a EntryAbility

应用启动后,initState 调用 _fetchUuid(initial: true),Dart 侧通过 MethodChannel('device_uuid').invokeMethod('getUUID') 向原生侧发起调用。原生侧 DeviceUuidPlugin.onMethodCall 收到 getUUID 方法调用,首先调用 ensureTrackingConsent() 检查 APP_TRACKING_CONSENT 权限状态——首次调用时权限未授予,通过 atManager.requestPermissionsFromUser 弹出系统授权对话框。用户点击"允许"后,权限状态变为 PERMISSION_GRANTED,随后调用 identifier.getOAID() 获取 OAID 并回传。

在这里插入图片描述

6.2 验证二:OAID 获取与显示

# 查看运行日志
hdc shell hilog -r
hdc shell aa start -b com.example.device_uuid_example -a EntryAbility
hdc shell hilog | grep DeviceUuidPlugin

日志输出:

DeviceUuidPlugin: onAttachedToEngine
DeviceUuidPlugin: onAttachedToAbility
DeviceUuidPlugin: getUUID called
DeviceUuidPlugin: APP_TRACKING_CONSENT granted
DeviceUuidPlugin: OAID: a1b2c3d4-e5f6-7890-abcd-ef1234567890

当前 UUID 卡显示 OAID 值(36 位标准 UUID 格式),格式识别为"Standard UUID (36 chars)",获取时间、耗时(毫秒级)、调用次数(1)、运行平台(ohos)均正确展示。

在这里插入图片描述

6.3 验证三:一致性压测

在一致性压测卡中选择"10 calls",点击"Run test"。demo 依次调用 getUUID() 10 次,每次收集返回值到 Set 中。10 次调用全部成功,返回值完全一致——unique values: 1errors: 0,结果显示"PASS — identifier is stable",平均耗时在毫秒级。

在这里插入图片描述

6.4 验证四:用户拒绝授权

# 重置应用权限
hdc shell aa set-permission -b com.example.device_uuid_example -p ohos.permission.APP_TRACKING_CONSENT -r 0
# 重新启动 demo
hdc shell aa start -b com.example.device_uuid_example -a EntryAbility

权限重置后重新启动,getUUID() 再次弹出授权对话框。用户点击"禁止"后,系统返回全零 OAID 00000000-0000-0000-0000-000000000000,插件不抛异常,直接返回该值。当前 UUID 卡显示全零字符串,格式识别为"All-zero OAID — permission not granted or disallowed"。

6.5 验证五:插件注册日志

通过 hdc hilog 抓取运行日志:

hdc shell hilog | grep -E "DeviceUuidPlugin|GeneratedPluginRegistrant"

日志输出确认 DeviceUuidPlugin 通过 GeneratedPluginRegistrant 注册成功,MethodChannel('device_uuid') 通道就绪,AbilityAware 接口的 onAttachedToAbility 被调用——插件获取了 UIAbilityContext 用于权限请求。

在这里插入图片描述

实测结论:

验证点结果
应用启动,Flutter 页面正常渲染五张卡片通过
首次调用 getUUID(),系统弹出 APP_TRACKING_CONSENT 授权对话框通过
用户点击"允许",返回 36 位标准 UUID 格式 OAID通过
用户点击"禁止",返回全零字符串,不抛异常通过
一致性压测 10 次调用,返回值全部一致通过
调用历史卡记录每次调用的 UUID、时间戳与耗时通过
应用回到前台自动刷新 UUID(开关开启时)通过
通道契约卡正确展示各平台返回值语义通过
全程仅申请 APP_TRACKING_CONSENT 一个 user_grant 权限通过

以下是操作的视屏,可以参考一下:

Example 启动授权 Example 启动授权 Example 启动授权


七、工作原理

整个调用链路如下:

Dart: DeviceUuid().getUUID()
  → DeviceUuidPlatform.instance.getUUID()
    → MethodChannelDeviceUuid.getUUID()
      → MethodChannel('device_uuid').invokeMethod<String?>('getUUID')
        → ArkTS: DeviceUuidPlugin.onMethodCall('getUUID')
          → ensureTrackingConsent()
            → bundleManager.getBundleInfoForSelfSync()  // 获取 tokenId
            → atManager.checkAccessTokenSync(tokenId, 'APP_TRACKING_CONSENT')
              → PERMISSION_GRANTED → return true
              → PERMISSION_DENIED → atManager.requestPermissionsFromUser()
                → 系统弹出授权对话框
                  → 用户允许 → return true
                  → 用户拒绝 → return false
          → identifier.getOAID()  // 获取 OAID
            → 用户已授权 → 返回真实 OAID
            → 用户未授权 → 返回全零 UUID
          → result.success(oaid)  // 回传给 Dart 侧
          → 异常时 result.success(null)  // 吞咽异常,与 Android 契约一致

Dart 侧通过 MethodChannel('device_uuid') 向原生侧发起 getUUID 方法调用。原生侧 DeviceUuidPlugin 实现了 FlutterPluginMethodCallHandlerAbilityAware 三个接口——FlutterPlugin 管理通道生命周期,MethodCallHandler 处理方法调用,AbilityAware 获取 UIAbilityContext 用于权限请求。

代码逐段分析:ensureTrackingConsent 权限检查与请求
ensureTrackingConsent 方法分三步处理权限。第一步,通过 bundleManager.getBundleInfoForSelfSync 获取当前应用的 BundleInfo,从中取出 appInfo.accessTokenId——这是应用在系统中的唯一令牌标识。第二步,通过 atManager.checkAccessTokenSync(tokenId, 'APP_TRACKING_CONSENT') 检查权限状态——若已授予(PERMISSION_GRANTED),直接返回 true。第三步,若未授予,通过 atManager.requestPermissionsFromUser(this.context, [permissionName]) 弹出系统授权对话框——requestPermissionsFromUser 需要一个 UIAbilityContext 参数(由 AbilityAware 接口提供),返回 authResults 数组中 0 表示用户授权。整个方法用 try/catch 包裹,异常时返回 false(不阻断后续流程,OAID 会返回全零值)。

代码逐段分析:getUUID 异步调用链
getUUID 方法采用 Promise 链式调用。首先调用 this.ensureTrackingConsent() 检查权限(异步),.then 回调中检查 granted 值——未授权时打印警告日志但不阻断,继续调用 identifier.getOAID()(异步)。.then 回调中将 OAID 值通过 result.success(oaid) 回传给 Dart 侧。.catch 回调中捕获 BusinessError,打印错误码和消息后通过 result.success(null) 回传 null——与 Android 平台的错误处理契约一致。整个方法外层还有一层 try/catch,捕获同步异常(如 identifier 模块加载失败),同样返回 null。

鸿蒙技术点:AbilityAware 接口与 UIAbilityContext
DeviceUuidPlugin 实现了 AbilityAware 接口,这是 Flutter ohos 框架提供的生命周期钩子。onAttachedToAbility 在插件随 Ability 挂载时被调用,通过 binding.getAbility().context 获取 UIAbilityContextUIAbilityContext 是鸿蒙 Ability 的上下文对象,requestPermissionsFromUser 方法需要它作为参数——因为权限请求对话框需要在 Ability 的 UI 上下文中弹出。onDetachedFromAbility 在 Ability 卸载时清理 context 引用。没有 AbilityAware,插件就无法弹出系统授权对话框——这是鸿蒙权限系统与 Flutter 插件框架衔接的关键。

鸿蒙技术点:identifier.getOAID 与 API 10
@ohos.identifier.oaid 是鸿蒙系统提供的匿名设备标识模块,从 API 10 开始可用。identifier.getOAID() 返回一个 Promise<string>,在异步回调中提供 OAID 值。OAID 的格式为 36 位标准 UUID(如 a1b2c3d4-e5f6-7890-abcd-ef1234567890)。当用户未授权 APP_TRACKING_CONSENT 时,系统返回全零 UUID 00000000-0000-0000-0000-000000000000——这一行为由系统保证,插件透传即可。

鸿蒙侧插件实现(ArkTS)核心代码:

import identifier from '@ohos.identifier.oaid';
import abilityAccessCtrl, { Permissions } from '@ohos.abilityAccessCtrl';
import bundleManager from '@ohos.bundle.bundleManager';

export default class DeviceUuidPlugin implements FlutterPlugin, MethodCallHandler, AbilityAware {
  private channel: MethodChannel | null = null;
  private context: common.UIAbilityContext | null = null;

  getUniqueClassName(): string {
    return "DeviceUuidPlugin"
  }

  onAttachedToEngine(binding: FlutterPluginBinding): void {
    this.channel = new MethodChannel(binding.getBinaryMessenger(), 'device_uuid');
    this.channel.setMethodCallHandler(this);
  }

  onAttachedToAbility(binding: AbilityPluginBinding): void {
    this.context = binding.getAbility().context;
  }

  onDetachedFromAbility(): void {
    this.context = null;
  }

  onMethodCall(call: MethodCall, result: MethodResult): void {
    switch (call.method) {
      case 'getUUID':
        this.getUUID(result);
        break;
      default:
        result.notImplemented();
        break;
    }
  }

  private getUUID(result: MethodResult): void {
    this.ensureTrackingConsent().then((granted: boolean) => {
      return identifier.getOAID();
    }).then((oaid: string) => {
      result.success(oaid);
    }).catch((err: BusinessError) => {
      result.success(null);  // 与 Android 契约一致
    });
  }

  private async ensureTrackingConsent(): Promise<boolean> {
    const bundleInfo = bundleManager.getBundleInfoForSelfSync(
      bundleManager.BundleFlag.GET_BUNDLE_INFO_WITH_APPLICATION);
    const tokenId = bundleInfo.appInfo.accessTokenId;
    const atManager = abilityAccessCtrl.createAtManager();
    const status = atManager.checkAccessTokenSync(tokenId, 'ohos.permission.APP_TRACKING_CONSENT');
    if (status === abilityAccessCtrl.GrantStatus.PERMISSION_GRANTED) {
      return true;
    }
    if (this.context == null) return false;
    const requestResult = await atManager.requestPermissionsFromUser(
      this.context, ['ohos.permission.APP_TRACKING_CONSENT']);
    return requestResult.authResults.length > 0 && requestResult.authResults[0] === 0;
  }
}

代码逐段分析:bundleManager.getBundleInfoForSelfSync
getBundleInfoForSelfSync@ohos.bundle.bundleManager 模块的同步方法,返回当前应用的 BundleInfo。参数 BundleFlag.GET_BUNDLE_INFO_WITH_APPLICATION 是位掩码,指定返回值中包含 appInfo 字段。appInfo.accessTokenId 是应用在系统权限管理中的唯一令牌标识——checkAccessTokenSyncrequestPermissionsFromUser 都需要这个 tokenId 来定位目标应用。这一调用不需要任何权限,属于应用自查询操作。

鸿蒙技术点:GeneratedPluginRegistrant 自动生成机制
GeneratedPluginRegistrant.ets 由 Flutter 工具链根据 pubspec.yaml 中的 ohos 平台配置自动生成。当 flutter pub get 解析到 device_uuid 依赖声明了 ohos: pluginClass: DeviceUuidPlugin 时,工具会在宿主工程的 example/ohos/entry/src/main/ets/plugins/ 下生成注册代码,将 new DeviceUuidPlugin() 添加到 FlutterEngine 的插件列表。EntryAbility.configureFlutterEngine 中一行 GeneratedPluginRegistrant.registerWith(flutterEngine) 即完成插件注册——FlutterAbility 会自动调用插件的 onAttachedToEngineonAttachedToAbility,建立通道和获取上下文。

八、常见问题

Q1:getUUID() 返回全零字符串 00000000-0000-0000-0000-000000000000 是怎么回事?

这是用户未授权 APP_TRACKING_CONSENT 权限导致的。OAID 受该权限保护,用户拒绝授权后系统返回全零 UUID,插件不抛异常,直接透传该值。业务层可通过判断返回值是否为全零字符串来区分"已授权"与"未授权"状态。用户可在系统设置 > 隐私 > 广告与追踪中重新授权。

Q2:getUUID() 返回 null 是怎么回事?

null 表示原生侧发生了异常(如 identifier.getOAID() 调用失败、权限检查异常等)。插件在 try/catch 中捕获所有异常并返回 null,与 Android 平台的 result.success(null) 契约一致——Dart 侧永远不会收到未捕获异常。若持续返回 null,检查设备是否支持 OAID(API 10+)和 @ohos.identifier.oaid 模块是否可用。

Q3:真机安装 demo 时提示"此应用暂不支持在当前设备安装"?

这是宿主工程的 compatibleSdkVersion 高于真机 API 版本导致的安装校验失败,与插件无关。将 build-profile.json5 中的 compatibleSdkVersion 调整为不高于真机 API 的版本(如 5.1.0(18),注意保留带括号的旧格式)即可。本文配套仓库的 example 已用此配置在 OpenHarmony-6.1.1.120(API 24)真机上安装实测通过。

Q4:插件 HAR 内声明了权限,为什么还要在宿主 module.json5 中声明?

鸿蒙的权限声明机制要求权限在宿主应用的 module.json5 中声明才会生效——插件 HAR 内的权限声明不会自动合并进宿主应用。若宿主未声明 APP_TRACKING_CONSENTrequestPermissionsFromUser 不会弹出对话框,checkAccessTokenSync 返回 PERMISSION_DENIEDidentifier.getOAID() 返回全零 UUID。

Q5:OAID 会在什么情况下变化?

OAID 由系统统一管理,具有以下变化规则:用户可在系统设置中主动重置 OAID(重置后所有应用获取到新值);用户关闭"广告与追踪"开关后 OAID 变为全零;恢复出厂设置后 OAID 会重新分配。应用卸载重装不会改变 OAID(由系统层管理,不随应用生命周期变化)。业务层应定期刷新 OAID 值(如应用启动时),避免使用过期的缓存值。

Q6:鸿蒙 OAID 与 Android ANDROID_ID 有什么区别?

Android 的 ANDROID_ID 是设备级标识符,同一设备上所有应用获取值相同(Android 8.0 以下);鸿蒙的 OAID 是应用级隔离的——同一设备上不同应用获取的 OAID 不同,增强了隐私保护。Android 的 ANDROID_ID 不需要权限;OAID 需要 APP_TRACKING_CONSENT 权限。Android 返回值经 SHA-1 哈希为 40 位十六进制;OAID 直接返回 36 位标准 UUID,不做二次哈希。

九、结语

回顾一下:在 pubspec.yaml 中以 git TAG 引入 device_uuid,调用 DeviceUuid().getUUID() 一个方法即可在鸿蒙 App 内获取设备唯一标识。鸿蒙平台通过 @ohos.identifier.oaid 获取 OAID 匿名设备标识,首次调用弹出 APP_TRACKING_CONSENT 授权对话框,用户允许后返回 36 位标准 UUID,用户拒绝后返回全零字符串不抛异常,发生异常时返回 null——与 Android 的错误处理契约完全一致。插件实现了 FlutterPluginMethodCallHandlerAbilityAware 三个接口,通过 bundleManager 获取应用 tokenId、atManager 检查与请求权限、identifier.getOAID() 获取 OAID,全程仅在宿主 module.json5 中声明一个 user_grant 权限。同一份 Dart 代码在 Android、iOS、macOS、鸿蒙四端行为一致——Android 返回 SHA-1 哈希、iOS 返回 Keychain UUID、鸿蒙返回 OAID。已在 OpenHarmony-6.1.1.120(API 24)真机完整实测,授权获取、拒绝降级、一致性压测、历史记录四个场景全部通过。

使用中发现任何问题,欢迎到配套仓库提 Issue,也欢迎发 PR 共建。

相关链接

欢迎加入 CPF-Flutter 鸿蒙社区,社区入口、环境搭建指南和本文相关链接统一放在这里:


附:device_uuid 核心能力对照表

能力维度实现方式鸿蒙表现跨平台一致性
设备标识获取identifier.getOAID()36 位标准 UUID各平台标识来源不同
权限检查atManager.checkAccessTokenSync正确检查授权状态Android/iOS 无需权限
权限请求atManager.requestPermissionsFromUser弹出系统授权对话框Android/iOS 无需权限
授权后获取OAID 真实值36 位 UUID格式与 iOS 一致
拒绝后降级全零 UUID 透传不抛异常与 Android null-on-error 契约一致
异常处理try/catch 吞咽返回 null返回 null与 Android 契约一致
标识稳定性系统层管理 OAID多次调用一致各平台均有稳定性保证
应用级隔离OAID 应用级隔离不同应用不同值与 Android 8.0+ 一致
PlatformInterfaceplugin_platform_interface默认 MethodChannel 实现四端共用同一 Dart API
AbilityAwareonAttachedToAbility获取 UIAbilityContext鸿蒙特有接口
通道契约MethodChannel('device_uuid')通道就绪通道名对齐各平台
权限要求APP_TRACKING_CONSENT(user_grant)需宿主声明仅鸿蒙需要
Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐