实战HarmonyOS鸿蒙App 设备唯一标识一键获取能力,用 OAID 匿名标识设备、用户授权弹窗、全零值降级不抛异常,一个方法即取即用 —— device_uuid 鸿蒙使用指南
开发工具: 华为云码道
本文配套仓库: 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 权限 | 通过 |
我先逐张查看这 6 张截图,再按表 1 / 表 2 格式整理输出。
这 6 张截图实际上是 Device UUID Example 示例页(OpenHarmony,TLR-AL00 6.1.0.135)在不同操作阶段的快照——与上文 SliderGradient 截图不同,因此我按同样的"模块说明 + 状态快照"双表结构整理如下(附件共 6 张,非 7 张)。
表 1 · 页面模块与配置说明
| 模块 | 关键配置 | 预期表现 |
|---|---|---|
| 设备 UUID | DeviceUuid().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 resume(WidgetsBindingObserver) | 开关开启时应用回到前台自动刷新 UUID |
| 通道契约 | MethodChannel('device_uuid') → getUUID() → String?(never throws) | 展示 Android / iOS·macOS / OpenHarmony 三端标识来源与 OAID 全零说明 |
表 2 · 各截图实测状态快照
| 截图时刻 | 设备 UUID 卡(格式 / 获取时间 / 延迟 / 次数 / 平台) | 一致性测试(档位 / 结果 / unique·errors·avg) | 调用历史 | 演示设置 | 通道契约 |
|---|---|---|---|---|---|
| 00:20 | 92332691-f9a5-4e5b-8b65-5d4640e498ba · Standard UUID (36 chars) / 00:19:50 / 2420 ms / 1 / ohos | 10 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 ms | 3 条: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 / ohos | 50 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 树(包括五张卡片、ListView、Dismissible、ChoiceChip、LinearProgressIndicator等全部组件)在鸿蒙设备上完整渲染。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_interface的PlatformInterface抽象,默认实现为MethodChannelDeviceUuid,各平台通过MethodChannel('device_uuid')暴露getUUID方法。
接口说明
DeviceUuid 类
| 名称 | 描述 | 类型 | 参数类型 | 返回值 | 必填 | 鸿蒙平台支持 |
|---|---|---|---|---|---|---|
getUUID | 获取设备唯一标识 | 方法 | 无 | Future<String?> | 是 | 是 |
各平台返回值语义
| 平台 | 标识来源 | 返回值格式 | 权限要求 | 失败行为 |
|---|---|---|---|---|
| Android | ANDROID_ID 经 SHA-1 哈希 | 40 位十六进制串 | 无 | 返回 null |
| iOS / macOS | Keychain 持久化的 XYUUID | 36 位标准 UUID | 无 | 返回 null |
| OpenHarmony / HarmonyOS | @ohos.identifier.oaid 的 OAID | 36 位标准 UUID | APP_TRACKING_CONSENT(user_grant) | 返回 null(异常)/ 全零字符串(用户拒绝) |
三、环境准备
本文所有实测均在以下环境完成:
| 项 | 版本 | 说明 |
|---|---|---|
| Flutter(ohos 版) | 3.44.9+ohos-0.0.1-canary1 | 主验证环境,真机实测 |
| DevEco Studio | 26.0.0(DS-261.23567.138.36.2600821) | 构建环境 |
| 编译 SDK | 5.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.44 | 0.0.4-ohos-1.0.0-beta.1 | main |
说明:该 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 的AppLifecycleState,WidgetsBinding.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: 1,errors: 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 权限 | 通过 |
以下是操作的视屏,可以参考一下:
七、工作原理
整个调用链路如下:
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 实现了 FlutterPlugin、MethodCallHandler、AbilityAware 三个接口——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获取UIAbilityContext。UIAbilityContext是鸿蒙 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时,系统返回全零 UUID00000000-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是应用在系统权限管理中的唯一令牌标识——checkAccessTokenSync和requestPermissionsFromUser都需要这个 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会自动调用插件的onAttachedToEngine和onAttachedToAbility,建立通道和获取上下文。
八、常见问题
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_CONSENT,requestPermissionsFromUser 不会弹出对话框,checkAccessTokenSync 返回 PERMISSION_DENIED,identifier.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 的错误处理契约完全一致。插件实现了 FlutterPlugin、MethodCallHandler、AbilityAware 三个接口,通过 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+ 一致 |
| PlatformInterface | plugin_platform_interface | 默认 MethodChannel 实现 | 四端共用同一 Dart API |
| AbilityAware | onAttachedToAbility | 获取 UIAbilityContext | 鸿蒙特有接口 |
| 通道契约 | MethodChannel('device_uuid') | 通道就绪 | 通道名对齐各平台 |
| 权限要求 | APP_TRACKING_CONSENT(user_grant) | 需宿主声明 | 仅鸿蒙需要 |
更多推荐





所有评论(0)