开发工具: 华为云码道

本文配套仓库: 上游 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)
全程权限请求由插件自动完成,业务代码无需额外处理通过

应用主界面 授权弹窗 授权后返回真实 OAID

Example 启动复制后内容获取记录列表
Example 启动进入页面复制后显示Device_id内容复制后显示获取次数的记录列表

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

清空后状态(需要授权)

图一:demo 应用在 OpenHarmony 真机启动(API 24 / arm64),首次获取返回全零值(权限未授予)

图二:首次调用自动弹出 APP_TRACKING_CONSENT 权限请求弹窗,由系统 abilityAccessCtrl 拉起

图三:用户授权后重新获取,返回真实 OAID 92332691-f9a5-4e5b-8b65-5d4640e498ba,耗时 580ms

检查要点

  1. 权限请求弹窗由系统 abilityAccessCtrl.requestPermissionsFromUser() 拉起,插件通过实现 AbilityAware 接口获取 UIAbility 上下文,业务方无需自行编写权限请求代码;
  2. 权限未授予时系统返回全零值而非抛异常,这是 OAID 的官方语义——应用可正常调用但拿到的是占位符;
  3. 授权后返回的 OAID 在同一设备上稳定不变,应用层无法重置,如需更换需在系统设置中重置广告标识符;
  4. 完整实测过程见"六、运行与验证"。

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]

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

  1. 自动权限请求:插件实现 AbilityAware 接口,首次调用时自动弹出 APP_TRACKING_CONSENT 权限弹窗,业务代码无需自行处理权限逻辑;
  2. 语义对齐:OAID 与 Android ANDROID_ID、iOS identifierForVendor 语义一致——同为可重置的匿名设备标识,跨平台行为统一;
  3. Pigeon 类型安全:Dart 层由 Pigeon 生成,通信协议编译期确定,运行时无类型错误风险;
  4. Dart 层零改动:鸿蒙适配完全在原生侧完成,Dart 代码一行未改。

接口说明:

名称描述类型参数类型返回值必填鸿蒙平台支持
DeviceId().getDeviceId()获取设备唯一标识methodFuture<String?>

三、环境准备

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

版本说明
Flutter(ohos 版)3.44.9+ohos-0.0.1-canary1主验证环境,真机实测
编译 SDK26.0.0(26)DevEco Studio 26.0.0 自带
最低兼容 SDK5.1.0(18)工程最低 compatibleSdkVersion
真机OpenHarmony 6.1.1.120API 24,arm64,设备 ID 4UQ9K25508013016

在这里插入图片描述

在这里插入图片描述

编辑用户

两点提醒:

  1. 编译鸿蒙目标需要使用 ohos 版 Flutter SDK,普通 Flutter SDK 无法编译 ohos 产物;
  2. 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.441.0.3-ohos-1.0.0-beta.1main

说明:该 TAG 已在 3.44.9+ohos-0.0.1-canary1 真机上实测通过。宿主工程 compatibleSdkVersion 设为 5.1.0(18) 即可在 API 24 真机运行。原库的 Dart 层 API 与上游完全一致,适配过程对 Dart 代码零改动。

pubspec.yaml 中的 ohos 平台声明

适配后的 pubspec.yamlflutter.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.json5requestPermissions 中声明 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_GRANTEDPERMISSION_DENIEDrequestPermissionsFromUser(context, permissions) 异步弹出系统权限弹窗,返回 requestPermissionsFromUserResult,其中 authResults 数组包含每个权限的授权结果。device_platform_uid 的插件代码正是通过这两个方法实现了"先检查后请求"的权限链路。

5.4 跨平台行为

同一对接口在各端的行为:

平台设备标识来源权限要求通信方式
AndroidSettings.Secure.ANDROID_ID无需权限Pigeon BasicMessageChannel
iOSUIDevice.identifierForVendor无需权限Pigeon BasicMessageChannel
OpenHarmony / HarmonyOSidentifier.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 真机
设备 ID4UQ9K25508013016
系统版本OpenHarmony 6.1.1.120
API 版本24
架构arm64

HarmonyOS 技术点:FlutterAbility 与 EntryAbility

鸿蒙 Flutter 应用的入口 Ability 需要继承 FlutterAbility(由 @ohos/flutter_ohos 提供),而非标准的 UIAbilityFlutterAbility 内部封装了 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 与原生平台之间的通信有两种主要方式:MethodChannelBasicMessageChannel。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.getDeviceIdBasicMessageChannel,向原生侧发送 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 类同时实现 FlutterPluginAbilityAware 两个接口——前者管理插件生命周期,后者在 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 需要 UIAbilitycontext 参数——这正是插件实现 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 引用

onAttachedToEngineonAttachedToAbility 的调用顺序由引擎控制:先挂载引擎(创建通道、设置消息处理器),后挂载 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 而非 MethodChannelBasicMessageChannel 采用消息模型(发送一条消息,回复一条消息),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 权限(配置 reasonusedScene),调用 DeviceId().getDeviceId() 即可在鸿蒙 App 内获取设备唯一标识。插件通过 AbilityAware 接口自动请求权限,授权后返回真实 OAID,未授权返回全零值。Dart 层由 Pigeon 生成类型安全的 BasicMessageChannel 通信代码,鸿蒙侧注册同名通道并严格遵循 Pigeon 消息格式,实现 Dart 层零改动。同一段代码在 Android、iOS 上也各自生效,三端共用 Pigeon 生成的同一套接口。

总结对比

维度AndroidiOSOpenHarmony / HarmonyOS
设备标识来源Settings.Secure.ANDROID_IDidentifierForVendoridentifier.getOAID()
权限要求APP_TRACKING_CONSENT(user_grant,自动请求)
通信方式Pigeon BasicMessageChannelPigeon BasicMessageChannelPigeon BasicMessageChannel
Dart 层改动
未授权时行为N/AN/A返回全零值 00000000-...
标识可重置是(恢复出厂设置)是(用户可重置)是(系统设置重置广告标识符)
权限状态OAID 返回值业务建议
已授权真实 OAID(如 92332691-...正常使用,用于用户统计/设备指纹
未授权全零值 00000000-...降级使用应用生成的 UUID,引导用户授权
用户重置后新的 OAID检测变更,更新本地关联关系

核心要点:OAID 是鸿蒙系统面向第三方应用的匿名设备标识,需 user_grant 权限,插件通过 AbilityAware 自动请求,Dart 层由 Pigeon 生成零改动。全零值是合法返回而非错误,业务层应据此做降级处理。

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

相关链接

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

Logo

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

更多推荐