HarmonyOS鸿蒙App 增加获取设备唯一标识的能力 —— oh_device_id 库的鸿蒙如何从安装到使用一步到位
开发工具: 华为云码道
本文配套仓库: 上游 deepak07082/device_id;鸿蒙适配改动位于该仓库的
main分支。
鸿蒙适配后仓库: https://atomgit.com/oh-flutter/device_id
本文配套仓库:https://github.com/deepak07082/device_id(TAG:1.0.3-ohos-1.0.0-beta.1,分支:main),文中示例代码位于仓库 example/ 目录。

设备唯一标识是移动应用的基础能力。 无论是用户统计、设备指纹、还是防欺诈风控,都需要一个稳定且匿名的设备 ID 来标识"这是同一台设备"。在 Android 上,开发者使用
Settings.Secure.ANDROID_ID;在 iOS 上,使用UIDevice.identifierForVendor。鸿蒙系统同样提供了等价能力——OAID(开放匿名设备标识符,Open Anonymous Device Identifier),它由@ohos.identifier.oaid模块提供,语义与 Android 的 ANDROID_ID、iOS 的 identifierForVendor 高度对齐:同为"可重置的匿名设备标识",同需用户授权。
本文介绍如何使用鸿蒙化适配后的 Flutter 三方库 device_platform_uid,在鸿蒙 App 内通过 OAID 获取设备唯一标识,自动请求 APP_TRACKING_CONSENT 权限,并附上 OpenHarmony 6.1.1.120 真机的完整实测记录。
一、最终运行效果
应用启动后自动调用 getDeviceId(),首次调用弹出 APP_TRACKING_CONSENT 权限请求弹窗,用户授权后返回真实 OAID(非全零值),再次调用结果一致,验证设备标识的稳定性:
| 验证点 | 结果 |
|---|---|
| 应用启动,Flutter 页面正常渲染 | 通过 |
首次调用 getDeviceId(),自动弹出 APP_TRACKING_CONSENT 权限弹窗 | 通过 |
用户授权后,返回真实 OAID(92332691-f9a5-4e5b-8b65-5d4640e498ba) | 通过 |
再次调用 getDeviceId(),返回值一致,验证稳定性 | 通过 |
权限未授予时,系统返回全零值 00000000-0000-0000-0000-000000000000 | 符合预期(见 FAQ Q1) |
| 全程权限请求由插件自动完成,业务代码无需额外处理 | 通过 |
| Example 启动 | 复制后内容 | 获取记录列表 |
|---|---|---|
| Example 启动进入页面 | 复制后显示Device_id内容 | 复制后显示获取次数的记录列表 |
以下是操作的视屏,可以参考一下:
图一:demo 应用在 OpenHarmony 真机启动(API 24 / arm64),首次获取返回全零值(权限未授予)
图二:首次调用自动弹出 APP_TRACKING_CONSENT 权限请求弹窗,由系统 abilityAccessCtrl 拉起
图三:用户授权后重新获取,返回真实 OAID 92332691-f9a5-4e5b-8b65-5d4640e498ba,耗时 580ms
检查要点:
- 权限请求弹窗由系统
abilityAccessCtrl.requestPermissionsFromUser()拉起,插件通过实现AbilityAware接口获取UIAbility上下文,业务方无需自行编写权限请求代码; - 权限未授予时系统返回全零值而非抛异常,这是 OAID 的官方语义——应用可正常调用但拿到的是占位符;
- 授权后返回的 OAID 在同一设备上稳定不变,应用层无法重置,如需更换需在系统设置中重置广告标识符;
- 完整实测过程见"六、运行与验证"。
HarmonyOS 技术点:OAID(开放匿名设备标识符)
鸿蒙系统的设备标识体系包含多种标识:UDID(设备唯一标识符,需系统级权限)、Serial(设备序列号,需系统级权限)、OAID(开放匿名设备标识符,user_grant 权限即可)。其中 OAID 是最接近 Android ANDROID_ID 语义的等价物:它是一个 UUID 格式的字符串,由系统统一生成和管理,应用层无法伪造或重置。
@ohos.identifier.oaid模块提供getOAID()方法返回该标识,需要ohos.permission.APP_TRACKING_CONSENT权限(user_grant 类型)。未授权时系统返回全零字符串00000000-0000-0000-0000-000000000000,授权后返回真实值。
二、device_platform_uid 是什么
device_platform_uid 原库(GitHub deepak07082/device_id,版本 1.0.3)是一个获取设备唯一标识的 Flutter 插件。它使用 Pigeon 生成类型安全的 Dart ↔ 原生通信代码,在 Android 上读取 Settings.Secure.ANDROID_ID,在 iOS 上读取 UIDevice.identifierForVendor。鸿蒙适配版在其基础上新增了 OpenHarmony / HarmonyOS 平台支持:通过 ArkTS 原生插件调用 identifier.getOAID() 获取 OAID,并实现 AbilityAware 接口自动完成运行时权限请求。
为什么使用 Pigeon 而非手写 MethodChannel?
Pigeon 是 Flutter 官方推荐的代码生成工具,通过在
.dart文件中定义@HostApi()接口描述,自动生成 Dart 侧和各平台侧的类型安全通信代码。与手写 MethodChannel 相比,Pigeon 的优势在于编译期类型检查:方法签名、参数类型、返回值类型在编译时就已确定,运行时不会出现"参数传错类型"或"通道名拼写错误"等问题。device_platform_uid 使用 Pigeon 生成BasicMessageChannel(而非 MethodChannel),这是 Pigeon 的默认通信方式,消息格式为:成功时回复[result],失败时回复[code, message, details]。
几个对使用者友好的特点:
- 自动权限请求:插件实现
AbilityAware接口,首次调用时自动弹出APP_TRACKING_CONSENT权限弹窗,业务代码无需自行处理权限逻辑; - 语义对齐:OAID 与 Android
ANDROID_ID、iOSidentifierForVendor语义一致——同为可重置的匿名设备标识,跨平台行为统一; - Pigeon 类型安全:Dart 层由 Pigeon 生成,通信协议编译期确定,运行时无类型错误风险;
- Dart 层零改动:鸿蒙适配完全在原生侧完成,Dart 代码一行未改。
接口说明:
| 名称 | 描述 | 类型 | 参数类型 | 返回值 | 必填 | 鸿蒙平台支持 |
|---|---|---|---|---|---|---|
DeviceId().getDeviceId() | 获取设备唯一标识 | method | 无 | Future<String?> | 是 | 是 |
三、环境准备
本文所有实测均在以下环境完成:
| 项 | 版本 | 说明 |
|---|---|---|
| Flutter(ohos 版) | 3.44.9+ohos-0.0.1-canary1 | 主验证环境,真机实测 |
| 编译 SDK | 26.0.0(26) | DevEco Studio 26.0.0 自带 |
| 最低兼容 SDK | 5.1.0(18) | 工程最低 compatibleSdkVersion |
| 真机 | OpenHarmony 6.1.1.120 | API 24,arm64,设备 ID 4UQ9K25508013016 |


两点提醒:
- 编译鸿蒙目标需要使用 ohos 版 Flutter SDK,普通 Flutter SDK 无法编译 ohos 产物;
ohos.permission.APP_TRACKING_CONSENT是 user_grant 权限,在module.json5中声明时必须同时配置reason(引用字符串资源)和usedScene(声明使用场景和时机),缺一不可,否则编译报错00303218 Configuration Error。
HarmonyOS 技术点:user_grant 权限三要素
鸿蒙系统的权限按授权方式分为
system_grant(系统自动授予,声明即获得)和user_grant(用户授权,需运行时弹窗请求)。APP_TRACKING_CONSENT属于 user_grant 类型。在module.json5中声明 user_grant 权限时,必须配置三个要素:name(权限名)、reason(用途说明,引用string.json中的字符串资源)、usedScene(使用场景,包含abilities列表和when时机)。这三个要素缺一不可,编译器会强制校验。这一机制确保用户在授权前能清楚了解权限用途,是鸿蒙系统对用户隐私的保护设计。
四、引入依赖
进入工程目录,在 pubspec.yaml 中添加 git 依赖:
dependencies:
device_platform_uid:
git:
url: https://github.com/deepak07082/device_id.git
# ref: 根据下方表格选择不同框架适配的 TAG 版本
ref: 1.0.3-ohos-1.0.0-beta.1
执行命令拉取依赖:
flutter pub get
TAG 命名规则:原库版本-ohos-版本号-beta.x。
| Flutter 框架版本 | TAG 名称 | 分支名 |
|---|---|---|
| 3.44 | 1.0.3-ohos-1.0.0-beta.1 | main |
说明:该 TAG 已在 3.44.9+ohos-0.0.1-canary1 真机上实测通过。宿主工程 compatibleSdkVersion 设为 5.1.0(18) 即可在 API 24 真机运行。原库的 Dart 层 API 与上游完全一致,适配过程对 Dart 代码零改动。
pubspec.yaml 中的 ohos 平台声明
适配后的
pubspec.yaml在flutter.plugin.platforms下新增了ohos配置项:flutter: plugin: platforms: android: package: com.example.device_id pluginClass: DeviceIdPlugin ios: pluginClass: DeviceIdPlugin ohos: pluginClass: DeviceIdPlugin
pluginClass的值必须与 ArkTS 插件类getUniqueClassName()的返回值完全一致。Flutter 鸿蒙适配层在构建时扫描此配置,自动生成GeneratedPluginRegistrant.ets文件,将插件类注册到引擎中。
五、代码接入
5.1 导入库
import 'package:device_platform_uid/device_id.dart';
导入后即可使用 DeviceId 类和 getDeviceId() 方法。
5.2 获取设备唯一标识
final DeviceId deviceId = DeviceId();
final String? id = await deviceId.getDeviceId();
debugPrint('Device ID: $id');
getDeviceId() 返回 Future<String?>。在鸿蒙设备上,首次调用时插件会自动弹出 APP_TRACKING_CONSENT 权限请求弹窗。用户授权后返回真实 OAID(如 92332691-f9a5-4e5b-8b65-5d4640e498ba),未授权时系统返回全零值 00000000-0000-0000-0000-000000000000。
为什么返回
String?而非String?Pigeon 生成的 Dart 接口定义为
String getDeviceId()(非空),但上层封装DeviceId类将其转为Future<String?>。这是为了兼容各平台上可能出现的 null 返回值场景——例如 Android 在极少数情况下ANDROID_ID可能为 null。在鸿蒙端,OAID 总是有值的(授权后为真实值,未授权为全零值),但为保持跨平台接口一致性,仍以String?返回。
5.3 配置权限声明
在 module.json5 的 requestPermissions 中声明 APP_TRACKING_CONSENT 权限:
"requestPermissions": [
{ "name": "ohos.permission.INTERNET" },
{
"name": "ohos.permission.APP_TRACKING_CONSENT",
"reason": "$string:reason_oaid",
"usedScene": {
"abilities": ["EntryAbility"],
"when": "inuse"
}
}
]
并在 entry/src/main/resources/base/element/string.json 中补充权限说明文案:
{
"string": [
{
"name": "reason_oaid",
"value": "用于获取设备匿名标识符(OAID)以提供设备唯一标识"
}
]
}
reason 字段引用的字符串资源会在权限弹窗中展示给用户,说明应用为何需要该权限。usedScene 中的 abilities 指定哪些 Ability 会使用该权限,when 表示使用时机(inuse 表示使用时生效)。
HarmonyOS 技术点:
abilityAccessCtrl权限管理鸿蒙系统的权限管理通过
@ohos.abilityAccessCtrl模块完成。createAtManager()返回AtManager实例,其上挂载了权限检查和请求方法。checkAccessTokenSync(tokenId, permission)同步检查某权限的授予状态,返回GrantStatus.PERMISSION_GRANTED或PERMISSION_DENIED。requestPermissionsFromUser(context, permissions)异步弹出系统权限弹窗,返回requestPermissionsFromUserResult,其中authResults数组包含每个权限的授权结果。device_platform_uid 的插件代码正是通过这两个方法实现了"先检查后请求"的权限链路。
5.4 跨平台行为
同一对接口在各端的行为:
| 平台 | 设备标识来源 | 权限要求 | 通信方式 |
|---|---|---|---|
| Android | Settings.Secure.ANDROID_ID | 无需权限 | Pigeon BasicMessageChannel |
| iOS | UIDevice.identifierForVendor | 无需权限 | Pigeon BasicMessageChannel |
| OpenHarmony / HarmonyOS | identifier.getOAID() | APP_TRACKING_CONSENT(user_grant,自动请求) | Pigeon BasicMessageChannel |
三个平台共用 Pigeon 生成的同一套 Dart 代码,通道名统一为 dev.flutter.pigeon.device_id.DeviceIdApi.getDeviceId,Dart 层零改动。
5.5 实战:在应用启动时获取设备 ID
实际业务中常见的场景是应用启动时获取设备 ID,用于用户统计或设备指纹。下面是一个可直接使用的封装:
import 'package:device_platform_uid/device_id.dart';
class DeviceIdService {
static String? _cachedDeviceId;
/// 获取设备唯一标识(带缓存,避免重复请求权限)
static Future<String?> getDeviceId() async {
if (_cachedDeviceId != null) {
return _cachedDeviceId;
}
final deviceId = DeviceId();
_cachedDeviceId = await deviceId.getDeviceId();
return _cachedDeviceId;
}
/// 判断设备 ID 是否为有效值(非全零)
static bool isValid(String? id) {
if (id == null || id.isEmpty) return false;
return id != '00000000-0000-0000-0000-000000000000';
}
}
上述组件的设计思路是:通过静态缓存避免重复调用 getDeviceId() 触发多次权限弹窗(系统在首次授权后后续调用不再弹窗,但缓存可以减少系统 API 调用开销)。isValid() 方法判断返回值是否为真实 OAID(排除全零占位符),业务可据此决定是否引导用户前往系统设置授权。
全零值的业务处理策略
全零值场景 推荐处理 用户拒绝授权 降级使用其他标识(如应用生成的 UUID 存本地) 用户在系统设置中重置了广告标识符 重新获取即可,OAID 会变更 系统尚未初始化 OAID 服务 延迟重试,通常 1 秒内可用 OAID 的全零值是一个合法返回,不表示错误。业务层应区分"获取失败"(PlatformException)和"获取成功但值为全零"两种情况,分别处理。
六、运行与验证
以下为 demo 工程的真机实测记录。仓库中 example 的 Bundle Name 为 com.example.ohos_example_scaffold,签名配置使用 DevEco Studio 自动签名。
| 设备项 | 值 |
|---|---|
| 机型 | OpenHarmony 真机 |
| 设备 ID | 4UQ9K25508013016 |
| 系统版本 | OpenHarmony 6.1.1.120 |
| API 版本 | 24 |
| 架构 | arm64 |
HarmonyOS 技术点:FlutterAbility 与 EntryAbility
鸿蒙 Flutter 应用的入口 Ability 需要继承
FlutterAbility(由@ohos/flutter_ohos提供),而非标准的UIAbility。FlutterAbility内部封装了FlutterEngine的初始化、Surface 注册、路由管理等逻辑。宿主工程的EntryAbility只需重写configureFlutterEngine方法,在其中调用GeneratedPluginRegistrant.registerWith(flutterEngine)即可完成所有原生插件的注册:export default class EntryAbility extends FlutterAbility { configureFlutterEngine(flutterEngine: FlutterEngine) { super.configureFlutterEngine(flutterEngine) GeneratedPluginRegistrant.registerWith(flutterEngine) } }这一设计与 Android 端的
FlutterActivity.configureFlutterEngine高度对称。
6.1 验证一:首次获取(权限未授予,返回全零值)
构建 hap 后安装到真机并启动:
# 构建 hap(debug,含签名)
flutter build hap --debug
# 安装到真机
hdc install entry-default-signed.hap
# 启动 demo
hdc shell aa start -b com.example.ohos_example_scaffold -a EntryAbility
应用启动后自动调用 getDeviceId(),由于 APP_TRACKING_CONSENT 权限尚未授予,系统返回全零值。通过 hilog 观察插件日志:
hdc shell "timeout 5 hilog | grep -E 'DeviceIdPlugin|OAID|oaid_service'"
实测日志输出:
FlutterEngineCxnRegistry --> Adding plugin: DeviceIdPlugin
DeviceIdPlugin: onAttachedToAbility, ability attached: yes
DeviceIdPlugin: APP_TRACKING_CONSENT granted: false
oaid_service/OAIDService: getOaid success
插件注册成功,AbilityAware 生效(ability attached: yes),权限检查结果为 false,系统 OAID 服务调用成功但返回全零值。界面显示 00000000-0000-0000-0000-000000000000,耗时 611ms。
日志解读:
Adding plugin: DeviceIdPlugin表示 Flutter 引擎的插件注册器成功将DeviceIdPlugin实例添加到引擎插件列表。onAttachedToAbility表示插件成功获取到UIAbility引用——这是AbilityAware接口的关键回调,只有持有UIAbility才能调用requestPermissionsFromUser拉起权限弹窗。granted: false表示权限尚未授予,但getOaid success表示系统 OAID 服务本身调用成功,只是返回了全零占位符。
6.2 验证二:权限弹窗与授权
首次调用后,插件检测到权限未授予,通过 abilityAccessCtrl.requestPermissionsFromUser() 拉起系统权限弹窗。弹窗中展示 reason_oaid 字符串资源定义的说明文案:“用于获取设备匿名标识符(OAID)以提供设备唯一标识”。
用户点击"允许"后,插件再次调用 identifier.getOAID(),此时系统返回真实 OAID:
DeviceIdPlugin: APP_TRACKING_CONSENT granted: true
oaid_service/OAIDService: getOaid success
DeviceIdResult: 92332691-f9a5-4e5b-8b65-5d4640e498ba (elapsed 1015ms)
界面显示真实 OAID 92332691-f9a5-4e5b-8b65-5d4640e498ba,首次授权耗时 1015ms(含权限弹窗交互时间)。

6.3 验证三:再次获取(权限已授予,结果一致)
再次点击"重新获取"按钮,此时权限已授予,无需弹窗:
DeviceIdResult: 92332691-f9a5-4e5b-8b65-5d4640e498ba (elapsed 580ms)
两次获取结果完全一致(同一设备同一 OAID),验证设备标识的稳定性。第二次耗时降至 580ms(无权限弹窗交互开销)。
实测结论:
| 验证点 | 结果 |
|---|---|
| 应用启动,Flutter 页面正常渲染 | 通过 |
首次调用,自动弹出 APP_TRACKING_CONSENT 权限弹窗 | 通过 |
| 授权后返回真实 OAID(非全零) | 通过 |
| 再次调用返回值一致,验证稳定性 | 通过 |
| 权限请求由插件自动完成,业务代码无需额外处理 | 通过 |
| 全程 Dart 层零改动 | 通过 |


七、工作原理
整个调用链路如下:
Dart: DeviceId().getDeviceId()
→ DeviceIdApi.getDeviceId() [Pigeon 生成]
→ BasicMessageChannel('dev.flutter.pigeon.device_id.DeviceIdApi.getDeviceId')
→ ArkTS: DeviceIdPlugin.onMessage
├─ 检查 APP_TRACKING_CONSENT 权限
│ ├─ 已授予 → 直接读取 OAID
│ └─ 未授予 → requestPermissionsFromUser 弹窗
│ ├─ 用户允许 → 读取 OAID
│ └─ 用户拒绝 → 读取 OAID(系统返回全零值)
→ identifier.getOAID()
→ reply.reply([oaid]) // Pigeon 成功格式
Dart 侧 DeviceIdApi (Pigeon 生成):
→ BasicMessageChannel.send(null)
→ 收到 [result] → 返回 result as String
→ 收到 [code, message, details] → 抛 PlatformException
HarmonyOS 技术点:BasicMessageChannel vs MethodChannel
Flutter 与原生平台之间的通信有两种主要方式:
MethodChannel和BasicMessageChannel。MethodChannel 采用方法调用模型——Dart 端发送方法名 + 参数,原生端处理后回复结果;BasicMessageChannel 采用消息模型——Dart 端发送一条消息,原生端处理后回复一条消息。Pigeon 默认使用 BasicMessageChannel,其消息编解码器为StandardMessageCodec。device_platform_uid 的 Pigeon 定义为@HostApi() abstract class DeviceIdApi { String getDeviceId(); },生成的通道名为dev.flutter.pigeon.device_id.DeviceIdApi.getDeviceId,消息格式为:成功时回复[result](单元素数组),失败时回复[code, message, details](三元素数组)。这一格式是 Pigeon 的标准协议,原生侧必须严格遵循才能与 Dart 层正确通信。
7.1 Pigeon 定义与 Dart 层
Pigeon 的输入定义文件 pigeons/platform_device_id.dart 非常简洁:
import 'package:pigeon/pigeon.dart';
()
abstract class DeviceIdApi {
String getDeviceId();
}
@HostApi() 注解表示这是一个从 Dart 调用原生方向 的 API。Pigeon 生成的 Dart 代码 device_id_api.dart 中,DeviceIdApi 类通过 BasicMessageChannel 发送消息并等待回复:
class DeviceIdApi {
Future<String> getDeviceId() async {
final String pigeonVar_channelName =
'dev.flutter.pigeon.device_id.DeviceIdApi.getDeviceId';
final BasicMessageChannel<Object?> pigeonVar_channel =
BasicMessageChannel<Object?>(
pigeonVar_channelName,
pigeonChannelCodec,
binaryMessenger: pigeonVar_binaryMessenger,
);
final Future<Object?> pigeonVar_sendFuture = pigeonVar_channel.send(null);
final List<Object?>? pigeonVar_replyList =
await pigeonVar_sendFuture as List<Object?>?;
if (pigeonVar_replyList == null) {
throw _createConnectionError(pigeonVar_channelName);
} else if (pigeonVar_replyList.length > 1) {
// 错误格式: [code, message, details]
throw PlatformException(
code: pigeonVar_replyList[0]! as String,
message: pigeonVar_replyList[1] as String?,
details: pigeonVar_replyList[2],
);
} else if (pigeonVar_replyList[0] == null) {
throw PlatformException(
code: 'null-error',
message: 'Host platform returned null value for non-null return value.',
);
} else {
// 成功格式: [result]
return (pigeonVar_replyList[0] as String?)!;
}
}
}
上述代码的核心逻辑是:创建一个名为 dev.flutter.pigeon.device_id.DeviceIdApi.getDeviceId 的 BasicMessageChannel,向原生侧发送 null 消息(无参数),等待回复。回复是一个 List<Object?>——如果列表长度为 1,表示成功,取第一个元素作为返回值;如果长度大于 1,表示错误,前三个元素分别为 error code、message、details,封装为 PlatformException 抛出。这个消息格式是 Pigeon 的标准协议,鸿蒙侧原生插件必须严格按此格式回复。
上层的 DeviceId 类只是简单的封装:
import 'device_id_api.dart';
class DeviceId {
Future<String?> getDeviceId() async {
final api = DeviceIdApi();
final deviceId = await api.getDeviceId();
return deviceId;
}
}
7.2 ArkTS 插件实现逐段解析
鸿蒙侧插件的核心实现包含以下几个关键部分,逐段解析如下。
第一段:导入与类声明
import {
FlutterPlugin,
FlutterPluginBinding,
AbilityAware,
AbilityPluginBinding,
BasicMessageChannel,
StandardMessageCodec,
Reply,
} from '@ohos/flutter_ohos';
import identifier from '@ohos.identifier.oaid';
import abilityAccessCtrl, { Permissions } from '@ohos.abilityAccessCtrl';
import bundleManager from '@ohos.bundle.bundleManager';
import UIAbility from '@ohos.app.ability.UIAbility';
import common from '@ohos.app.ability.common';
import { BusinessError } from '@ohos.base';
import hilog from '@ohos.hilog';
export default class DeviceIdPlugin implements FlutterPlugin, AbilityAware {
这段代码完成了三件事:从 @ohos/flutter_ohos 导入 Flutter 鸿蒙适配层的插件接口(包括 FlutterPlugin 生命周期管理、AbilityAware 能力感知、BasicMessageChannel 消息通道等);从系统模块导入 OAID 服务(@ohos.identifier.oaid)、权限管理(@ohos.abilityAccessCtrl)、应用信息(@ohos.bundle.bundleManager);声明 DeviceIdPlugin 类同时实现 FlutterPlugin 和 AbilityAware 两个接口——前者管理插件生命周期,后者在 UIAbility 挂载时获取上下文引用。
HarmonyOS 技术点:
AbilityAware接口
AbilityAware是 Flutter 鸿蒙适配层提供的接口,用于让原生插件感知宿主UIAbility的生命周期。它包含两个回调:onAttachedToAbility(binding)在 Ability 挂载到引擎时调用,binding.getAbility()返回UIAbility实例;onDetachedFromAbility()在 Ability 卸载时调用。许多系统 API(如requestPermissionsFromUser)需要UIAbility上下文才能调用,因此需要权限请求或窗口操作的插件必须实现AbilityAware。这与 Android 端的ActivityAware、iOS 端的FlutterPlugin.AppDelegateView概念对应。
第二段:通道注册与消息处理
private static readonly CHANNEL_NAME: string =
'dev.flutter.pigeon.device_id.DeviceIdApi.getDeviceId';
private static readonly OAID_PERMISSION: Permissions = 'ohos.permission.APP_TRACKING_CONSENT';
private channel: BasicMessageChannel<Object> | null = null;
private ability: UIAbility | null = null;
onAttachedToEngine(binding: FlutterPluginBinding): void {
this.channel = new BasicMessageChannel<Object>(
binding.getBinaryMessenger(),
DeviceIdPlugin.CHANNEL_NAME,
StandardMessageCodec.INSTANCE
);
this.channel.setMessageHandler({
onMessage: (message: Object, reply: Reply<Object>): void => {
this.onMessage(reply);
}
});
}
onAttachedToEngine 在引擎加载插件时调用:通过 binding.getBinaryMessenger() 获取消息信使,创建与 Pigeon 通道名完全一致的 BasicMessageChannel(使用 StandardMessageCodec 编解码器,与 Dart 侧的 _PigeonCodec 对齐),并设置消息处理器。当 Dart 侧调用 channel.send(null) 时,原生侧的 onMessage 回调被触发,reply 对象用于回传结果。
关键点:通道名必须与 Pigeon 生成的 Dart 侧通道名完全一致(
dev.flutter.pigeon.device_id.DeviceIdApi.getDeviceId),否则消息无法送达。编解码器必须使用StandardMessageCodec.INSTANCE,这是 Pigeon 的_PigeonCodec的基类,确保 Dart 与原生之间的消息编解码格式一致。
第三段:权限请求与 OAID 读取
private async requestOaidPermission(context: common.Context): Promise<boolean> {
const atManager = abilityAccessCtrl.createAtManager();
const bundleInfo = bundleManager.getBundleInfoForSelfSync(
bundleManager.BundleFlag.GET_BUNDLE_INFO_WITH_APPLICATION);
const tokenId = bundleInfo.appInfo.accessTokenId;
const grantStatus = atManager.checkAccessTokenSync(tokenId, DeviceIdPlugin.OAID_PERMISSION);
if (grantStatus === abilityAccessCtrl.GrantStatus.PERMISSION_GRANTED) {
return true; // 已授权,无需弹窗
}
// 未授权,弹窗请求
const result = await atManager.requestPermissionsFromUser(
context, [DeviceIdPlugin.OAID_PERMISSION]);
return result.authResults.length > 0 &&
result.authResults[0] === abilityAccessCtrl.GrantStatus.PERMISSION_GRANTED;
}
这段代码实现了"先检查后请求"的权限链路。首先通过 bundleManager.getBundleInfoForSelfSync() 获取当前应用的 BundleInfo,从中取出 accessTokenId——这是应用在鸿蒙系统中的唯一安全标识。然后通过 atManager.checkAccessTokenSync() 同步检查 APP_TRACKING_CONSENT 权限的授予状态:如果已授予,直接返回 true;如果未授予,调用 requestPermissionsFromUser() 异步弹出系统权限弹窗。requestPermissionsFromUser 需要 UIAbility 的 context 参数——这正是插件实现 AbilityAware 的原因。
HarmonyOS 技术点:
accessTokenId与权限校验鸿蒙系统中的每个应用都有一个
accessTokenId(访问令牌 ID),它是应用在系统安全子系统中的唯一标识。权限校验的本质是检查某accessTokenId是否被授予了特定权限。bundleManager.getBundleInfoForSelfSync()获取的应用信息中包含appInfo.accessTokenId,将其传给checkAccessTokenSync()即可查询权限状态。这一机制类似于 Android 的Context.checkSelfPermission(),但鸿蒙使用 token-based 的安全模型。
第四段:OAID 读取与 Pigeon 格式回复
private readOaid(reply: Reply<Object>): void {
identifier.getOAID().then((oaid: string) => {
if (oaid == null || oaid.length == 0) {
reply.reply(["unknown_device_id"]); // Pigeon 错误格式
} else {
reply.reply([oaid]); // Pigeon 成功格式: [result]
}
}).catch((error: BusinessError) => {
reply.reply([
"getOAID_error",
error.message != null ? error.message : "Failed to get OAID",
""
]); // Pigeon 错误格式: [code, message, details]
});
}
readOaid 是 OAID 读取的核心方法。identifier.getOAID() 返回 Promise<string>,成功时拿到 OAID 字符串。回复格式严格遵循 Pigeon 协议:成功时 reply.reply([oaid])(单元素数组,Dart 侧取 [0] 作为返回值);失败时 reply.reply([code, message, details])(三元素数组,Dart 侧封装为 PlatformException)。注意 reply.reply() 的参数是一个数组,不是单个值——这是 Pigeon 的 BasicMessageChannel 协议要求。
权限未授予时为什么仍然调用
getOAID()?权限未授予时,系统 OAID 服务不会抛异常,而是正常返回全零字符串
00000000-0000-0000-0000-000000000000。这是鸿蒙系统的设计:应用始终可以调用getOAID(),但未授权时拿到的是占位符。插件代码中onMessage方法的逻辑是:无论权限是否授予,最终都会调用readOaid(reply)读取 OAID——权限请求只是"尽力而为"的优化,即使被拒绝也不阻断流程。这种设计保证了 API 调用不会因权限问题而抛异常,业务层只需处理返回值(全零 vs 真实值)即可。
7.3 插件注册机制
鸿蒙侧的插件注册是自动完成的。Flutter 鸿蒙适配层在构建时扫描 pubspec.yaml 中的 ohos: pluginClass 配置,自动生成 GeneratedPluginRegistrant.ets 文件:
import { FlutterEngine, Log } from '@ohos/flutter_ohos';
import IntegrationTestPlugin from 'integration_test';
import DeviceIdPlugin from 'device_platform_uid';
export class GeneratedPluginRegistrant {
static registerWith(flutterEngine: FlutterEngine) {
try {
flutterEngine.getPlugins()?.add(new IntegrationTestPlugin());
flutterEngine.getPlugins()?.add(new DeviceIdPlugin());
} catch (e) {
Log.e(TAG, "Tried to register plugins with FlutterEngine failed.");
}
}
}
整个注册链路为:
EntryAbility.configureFlutterEngine()
→ GeneratedPluginRegistrant.registerWith(flutterEngine)
→ flutterEngine.getPlugins().add(new DeviceIdPlugin())
→ DeviceIdPlugin.onAttachedToEngine(binding)
→ new BasicMessageChannel(messenger, CHANNEL_NAME)
→ setMessageHandler({ onMessage: ... })
→ DeviceIdPlugin.onAttachedToAbility(binding)
→ this.ability = binding.getAbility() // 持有 UIAbility 引用
onAttachedToEngine 和 onAttachedToAbility 的调用顺序由引擎控制:先挂载引擎(创建通道、设置消息处理器),后挂载 Ability(获取上下文引用)。当 Dart 侧的 getDeviceId() 被调用时,onMessage 回调触发,此时 this.ability 已经赋值,可以安全地用于权限请求。
以下是操作的视屏,可以参考一下:
八、常见问题
Q1:getDeviceId() 返回全零值 00000000-0000-0000-0000-000000000000 是怎么回事?
这是 APP_TRACKING_CONSENT 权限未授予的表现。OAID 的系统语义是:未授权时返回全零占位符,不抛异常。插件在首次调用时会自动弹出权限弹窗,但如果用户选择了"拒绝",后续调用都将返回全零值。业务层应判断返回值是否为全零,据此降级处理(如使用应用生成的 UUID 兜底)。用户可在系统设置 → 隐私 → 广告标识符中重新授权。
Q2:为什么选择 OAID 而非 UDID 或设备序列号?
鸿蒙系统提供了多种设备标识:UDID(设备唯一标识符)和 Serial(设备序列号)需要 ohos.permission.sec.ACCESS_UDID 权限,该权限仅系统应用与企业应用可用,第三方应用无法申请。而 OAID 仅需 APP_TRACKING_CONSENT(user_grant,第三方应用可申请)。OAID 与 Android ANDROID_ID、iOS identifierForVendor 语义一致——同为"可重置的匿名设备标识",是面向第三方应用的最合适选择。
Q3:声明了 APP_TRACKING_CONSENT 权限后编译报错怎么办?
APP_TRACKING_CONSENT 是 user_grant 权限,在 module.json5 中声明时必须同时配置 reason(引用 string.json 中的字符串资源)和 usedScene(包含 abilities 列表和 when 时机)。缺少任何一个都会编译报错 00303218 Configuration Error: reason and usedScene attributes are mandatory for user_grant permissions。正确格式见"五、代码接入"中的 5.3 节。
Q4:Pigeon 的 BasicMessageChannel 和普通 MethodChannel 有什么区别?
Pigeon 生成的代码使用 BasicMessageChannel 而非 MethodChannel。BasicMessageChannel 采用消息模型(发送一条消息,回复一条消息),MethodChannel 采用方法调用模型(发送方法名+参数,回复结果)。Pigeon 选择 BasicMessageChannel 是因为它的消息格式更灵活,可以编码复杂的数据结构。device_platform_uid 的通道名为 dev.flutter.pigeon.device_id.DeviceIdApi.getDeviceId(Pigeon 自动生成),消息格式为:成功 [result],失败 [code, message, details]。鸿蒙侧必须注册同名的 BasicMessageChannel 并使用 StandardMessageCodec 编解码器,才能与 Dart 层正确通信。
Q5:OAID 会在什么情况下变更?
OAID 是系统级稳定标识,应用层无法重置。但用户可以在系统设置中手动"重置广告标识符",此时系统会生成一个新的 OAID,旧的 OAID 不再可用。这与 iOS 的 identifierForVendor 可以被用户重置的语义一致。此外,恢复出厂设置也会重新生成 OAID。业务层不应将 OAID 作为永久唯一标识,而应结合时间戳和变更检测来处理可能的 ID 变更。
Q6:后台引擎(无 UIAbility)时能获取 OAID 吗?
可以调用,但无法弹窗请求权限。插件代码中 onMessage 方法检查了 this.ability 是否为 null:如果 Ability 未挂载(如后台引擎场景),插件跳过权限请求直接调用 readOaid(reply)。此时由于权限未授予,系统返回全零值。只有在 UIAbility 可用的前台场景下,插件才能通过 requestPermissionsFromUser 弹窗请求权限并获取真实 OAID。
九、结语
回顾一下:在 pubspec.yaml 中以 git TAG 引入 device_platform_uid,在 module.json5 中声明 APP_TRACKING_CONSENT 权限(配置 reason 和 usedScene),调用 DeviceId().getDeviceId() 即可在鸿蒙 App 内获取设备唯一标识。插件通过 AbilityAware 接口自动请求权限,授权后返回真实 OAID,未授权返回全零值。Dart 层由 Pigeon 生成类型安全的 BasicMessageChannel 通信代码,鸿蒙侧注册同名通道并严格遵循 Pigeon 消息格式,实现 Dart 层零改动。同一段代码在 Android、iOS 上也各自生效,三端共用 Pigeon 生成的同一套接口。
总结对比
维度 Android iOS OpenHarmony / HarmonyOS 设备标识来源 Settings.Secure.ANDROID_IDidentifierForVendoridentifier.getOAID()权限要求 无 无 APP_TRACKING_CONSENT(user_grant,自动请求)通信方式 Pigeon BasicMessageChannel Pigeon BasicMessageChannel Pigeon BasicMessageChannel Dart 层改动 无 无 无 未授权时行为 N/A N/A 返回全零值 00000000-...标识可重置 是(恢复出厂设置) 是(用户可重置) 是(系统设置重置广告标识符)
权限状态 OAID 返回值 业务建议 已授权 真实 OAID(如 92332691-...)正常使用,用于用户统计/设备指纹 未授权 全零值 00000000-...降级使用应用生成的 UUID,引导用户授权 用户重置后 新的 OAID 检测变更,更新本地关联关系 核心要点:OAID 是鸿蒙系统面向第三方应用的匿名设备标识,需 user_grant 权限,插件通过 AbilityAware 自动请求,Dart 层由 Pigeon 生成零改动。全零值是合法返回而非错误,业务层应据此做降级处理。
使用中发现任何问题,欢迎到配套仓库提 Issue,也欢迎发 PR 共建。
相关链接
欢迎加入 CPF-Flutter 鸿蒙社区,社区入口、环境搭建指南和本文相关链接统一放在这里:
更多推荐





所有评论(0)