开发工具: 华为云码道

DevEco Studio: 26.0.0(26.0.0.851)

原始仓库: flappygod/flutter_cached_uuid(GitHub)

本文配套仓库: oh-flutter/flutter_cached_uuid(AtomGit)

flutter_cached_uuid 为应用提供一个「首次生成、之后每次返回同一个值」的 UUID:Android 侧优先由 ANDROID_ID 与 FINGERPRINT 派生,iOS 侧用 CFUUIDCreate 生成后写入 Keychain,两端都以「装一次、认一个」的方式对外提供标识。本文以 flutter_cached_uuid 1.0.10 为例,介绍源码准备、OHOS 平台实现、Example 宿主工程配置和真机运行验证。

在这里插入图片描述

这是一个插件库:Dart 层保持零改动,适配工作集中在 ArkTS 侧的 UUID 生成、preferences 持久化与上下文获取上。文中的代码以提交 1546638e3186f2332ab4a7b63c0f1cb38cf762ce 为参考,对应 TAG 1.0.10-ohos-1.0.0-beta.1。

在这里插入图片描述

一、插件简介与适配目标

应用经常需要一个「装一次就固定」的标识:埋点去重、灰度分桶、把本地配置和账号绑定、给客服提供排查用的设备代号。Android 有 Settings.Secure.ANDROID_ID,iOS 有 Keychain,OpenHarmony 上则没有可以直接照搬的等价物——这正是本库适配要解决的问题。

flutter_cached_uuid 对外只有一个方法:

FlutterCachedUuid.getUUID()  ->  Future<String?>

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

语义是「首次调用生成、之后每次返回同一个值」:

  1. 首次调用生成一个符合 RFC 4122 v4 的 UUID(32 位十六进制 + 4 个连字符,本库统一转成大写);
  2. 把它写入应用私有存储(Android 是 SharedPreferences,iOS 是 Keychain,本次适配是应用沙箱内的 preferences);
  3. 之后每次调用都读这份缓存,进程重启、应用重新打开都不变;
  4. 清除应用数据或卸载重装后,缓存消失,下次调用会生成新的值。

上游的工程结构是标准的 federated plugin 三层:门面类 FlutterCachedUuid、平台接口 FlutterCachedUuidPlatform、方法通道实现 MethodChannelFlutterCachedUuid,原生侧android/ 与 ios/ 各有一份实现。本次适配要做的是:

  1. 在 pubspec.yaml 里声明 ohos 平台,让工具链生成注册代码;
  2. 用 ArkTS 实现同名通道 flutter_cached_uuid 的同名方法 getUUID,并保证「生成一次、持久化、后续命中缓存」的语义与 Android / iOS 一致;
  3. 生成可编译、可运行、可验证的 example/ohos 宿主工程,在真机上跑通首次生成、连续调用、进程重启与清数据四条路径。

二、环境准备

环境搭建参考社区文档:Flutter OH 开发环境搭建,完成 Flutter OH SDK 安装、环境变量和 DevEco Studio 配置。

完成后,在宿主机终端执行以下命令,确认当前选中的是支持 OHOS 的 Flutter 工具链,并能发现目标设备:

flutter --version
flutter doctor -v
hdc list targets

在这里插入图片描述
在这里插入图片描述

编辑用户

工程使用的工具链和 SDK 配置如下:

项目版本或配置用途
Flutter OHOS SDK3.47.7-ohos-1.0.0Flutter 编译与 OHOS 平台工具链
Flutter 分支oh-3.47-dev对应开发分支
Dart SDK3.13.3Dart 语言与包管理环境
DevEco Studio26.0.0(26.0.0.851)OHOS 应用工程开发套件
compatibleSdkVersion6.1.1(24)当前工程声明的最低兼容版本
targetSdkVersion6.1.1(24)应用面向的行为版本
测试设备 ROMOpenHarmony-6.1.1.120(API 24)真机系统版本(TLR-AL00,nova 14)
库版本1.0.10pubspec.yaml 中的包版本
原生语言ArkTSohos/ 模块实现语言
插件产物HAR(ohos/)经 GeneratedPluginRegistrant.ets 自动注册

2.1 开发套件版本与工程中的 SDK 版本配置

6.1.1(24) 和工程配置涉及 OpenHarmony 版本号体系。在本文工程中,相关版本的含义与配置方式如下:

  • 6.1.1(24) 采用「版本(API)」写法,表示 OpenHarmony 6.1.1 版本、对应 API 24;
  • compatibleSdkVersion 与 targetSdkVersion 均为 6.1.1(24):声明最低兼容 API 24,并以 API 24 为行为目标;
  • 本机安装的 DevEco Studio 自带 SDK 为 26.0.0(26),高于工程声明值,编译按本机 SDK 执行、安装兼容性按工程声明判断,两者并不冲突。

在这里插入图片描述
判断依据是设备的 hdc shell param get const.ohos.apiversion:本次测试机返回 24,与工程声明一致,安装与运行的版本门槛都满足。


三、从源码仓库开始准备适配工程

3.1 将上游源码同步到 AtomGit

适配已有第三方库时,先从 Pub 包信息或项目 README 确认上游地址、包名、版本和许可证。同步仓库时保留源码、许可证和提交历史,方便后续跟踪上游更新。

在 AtomGit 网页的新建仓库流程中,找到从已有仓库导入的入口,填写上游 Git 地址,选择自己有写权限的组织或个人空间,设置目标仓库名称后执行导入。完成后检查目标分支、pubspec.yaml、LICENSE 和提交历史是否完整,并记录适配起点的提交号。如果上游本身托管在 AtomGit,也可以通过 Fork 获得自己的工作仓库。

本例的上游为 flappygod/flutter_cached_uuid,包名 flutter_cached_uuid,pubspec.yaml 中的版本为 1.0.10,许可证为 BSD-3-Clause(LICENSE 开头是 Copyright 2023 flappy. All rights reserved.,随后是三段式条款)。适配起点的提交是 5a0eedc(上游最后一次功能提交,信息为 set synchroized false)。

仓库名与 Dart 包名一致,都是 flutter_cached_uuid,因此同步到 AtomGit 时保持原名即可,不必加 fluttertpc_ 前缀。

flutter_cached_uuid 现可从 AtomGit 获取。下面使用 AtomGit 地址拉取代码;需要提交修改时,使用自己有写权限的仓库或 Fork。

3.2 将代码拉取到宿主机

在安装了 Flutter OH 和 DevEco Studio 的开发电脑上打开终端,进入准备存放项目的目录,执行:

git clone https://atomgit.com/oh-flutter/flutter_cached_uuid.git
cd flutter_cached_uuid
pwd
ls
git remote -v
git status --short --branch
git rev-parse HEAD

git clone 会创建 flutter_cached_uuid/ 目录;cd 后的位置就是下文所说的仓库根目录,这里应能看到 pubspec.yaml、lib/、ohos/、example/,以及上游保留的 android/ 与 ios/。

需要使用与本文相同的代码版本时,在没有未提交修改的仓库中切换到以下提交:

git switch --detach 1546638e3186f2332ab4a7b63c0f1cb38cf762ce

适配其他插件时,使用该插件对应版本的 tag 或 commit 作为分支起点。

请添加图片描述

图 1:在宿主机终端输入 AtomGit 仓库拉取命令。

3.3 在仓库根目录确认分支与版本标记

本库没有为 OHOS 适配单独开特性分支,适配提交直接落在 main 上,版本通过 TAG 标记。TAG 命名规则为 原库版本-ohos-版本号-beta.x,本库使用 1.0.10-ohos-1.0.0-beta.1。在 flutter_cached_uuid/ 根目录执行:

git branch --show-current
git tag --list
git log --oneline -3
git status --short --branch

git branch --show-current 用于确认当前所在分支,git tag --list 用于确认版本标记是否已推送。向接收仓库提交时按其对分支与 TAG 的要求执行。

请添加图片描述

图 2:在 flutter_cached_uuid 仓库根目录确认分支与 TAG。

图 2 同时暴露了一个需要注意的状态:工作区里还有两处未提交改动——example/analysis_options.yaml 与 example/pubspec.lock。它们不影响库本体,但提交前要么一并提交、要么还原,避免留下「文档说干净、实际有残留」的错觉(处理方式见 6.3)。

3.4 补全 ohos 平台与 example 宿主工程

上游已经按插件形式声明了 android 与 ios 平台,只是缺少 ohos。补全方式是在仓库根目录执行插件模板生成,让它同时产出 ohos/ 模块与 example/ohos/ 宿主工程:

flutter create . --template=plugin --platforms=ohos
git status --short
git diff -- pubspec.yaml
  • --template=plugin 匹配本库的插件身份,生成 HAR 模块骨架;
  • --platforms=ohos 指定只补全 OHOS 平台,不影响已有的 android/、ios/ 实现;
  • 最后的 . 表示在当前插件根目录内补全,不是另建一层目录。

生成后务必用 diff 检查:模板会连带注入与 OHOS 无关的内容,例如 .kts 构建脚本、*.iml、local.properties,还可能改写 analysis_options.yaml。本次适配保留了已有 API、其他平台注册项及依赖配置,只保留 ohos/ 与 example/ohos/ 相关产物。不同 Flutter OH 版本生成的模板可能略有差异。

pubspec.yaml 的净变化只有 2 行——在 flutter: plugin: platforms 下追加 ohos 声明:

flutter:
  plugin:
    platforms:
      android:
        package: com.flappy.flutter_cached_uuid
        pluginClass: FlutterCachedUuidPlugin
      ios:
        pluginClass: FlutterCachedUuidPlugin
      ohos:
        pluginClass: FlutterCachedUuidPlugin

pluginClass 必须与 ArkTS 侧 getUniqueClassName() 的返回值一致(都是 FlutterCachedUuidPlugin),否则插件注册表找不到类。

顺便说明一个与同类老库不同的地方:本库上游的 SDK 约束已经是 sdk: '>=3.2.6 <4.0.0'、flutter: '>=3.3.0',本身就在 Dart 3 的范围内,适配过程不需要放宽 SDK 上界。

请添加图片描述

图 3:在仓库根目录输入 OHOS 平台补全命令。

3.5 适配后的项目目录

适配后的关键目录如下:

flutter_cached_uuid/
├── lib/
│   ├── flutter_cached_uuid.dart
│   ├── flutter_cached_uuid_method_channel.dart
│   └── flutter_cached_uuid_platform_interface.dart
├── ohos/
│   ├── index.ets
│   ├── oh-package.json5
│   ├── build-profile.json5
│   ├── hvigorfile.ts
│   └── src/main/
│       ├── module.json5
│       └── ets/components/plugin/FlutterCachedUuidPlugin.ets
├── example/
│   ├── lib/main.dart
│   └── ohos/
│       ├── build-profile.json5
│       ├── oh-package.json5
│       └── entry/src/main/
│           ├── ets/entryability/EntryAbility.ets
│           ├── ets/plugins/GeneratedPluginRegistrant.ets
│           ├── module.json5
│           └── resources/
├── test/
├── README.OpenHarmony_CN.md
├── CHANGELOG.OpenHarmony.md
└── pubspec.yaml

项目根目录如下,其中包含 ohos/、example/ohos/,以及 OpenHarmony 中英文说明和变更记录:

请添加图片描述

图 4:适配后的 flutter_cached_uuid 项目根目录。

文件主要职责
lib/flutter_cached_uuid.dart门面类,业务侧唯一入口 FlutterCachedUuid.getUUID()
lib/flutter_cached_uuid_platform_interface.dart平台接口,允许注入自定义实现
lib/flutter_cached_uuid_method_channel.dart方法通道实现,固定通道名 flutter_cached_uuid
ohos/src/main/ets/components/plugin/FlutterCachedUuidPlugin.etsArkTS 插件:注册、上下文获取、生成与持久化
ohos/index.ets导出插件类,供宿主工程引用
example/lib/main.dart示例页:当前 UUID、调用历史、稳定性结论、耗时
example/ohos/build-profile.json5声明 product、SDK 版本与签名配置
README.OpenHarmony_CN.md / README.OpenHarmony.mdOpenHarmony 中英文说明
CHANGELOG.OpenHarmony.md鸿蒙化变更记录

四、Dart 实现分析

本库的 Dart 层只有 3 个文件、共 53 行,典型的插件三层结构,本次适配零改动。三个文件全文如下:

// lib/flutter_cached_uuid.dart
import 'flutter_cached_uuid_platform_interface.dart';

class FlutterCachedUuid {
  static Future<String?> getUUID() {
    return FlutterCachedUuidPlatform.instance.getUUID();
  }
}
// lib/flutter_cached_uuid_method_channel.dart
class MethodChannelFlutterCachedUuid extends FlutterCachedUuidPlatform {
  
  final methodChannel = const MethodChannel('flutter_cached_uuid');

  
  Future<String?> getUUID() async {
    final version = await methodChannel.invokeMethod<String>('getUUID');
    return version;
  }
}
// lib/flutter_cached_uuid_platform_interface.dart
abstract class FlutterCachedUuidPlatform extends PlatformInterface {
  FlutterCachedUuidPlatform() : super(token: _token);

  static final Object _token = Object();
  static FlutterCachedUuidPlatform _instance = MethodChannelFlutterCachedUuid();

  static FlutterCachedUuidPlatform get instance => _instance;

  static set instance(FlutterCachedUuidPlatform instance) {
    PlatformInterface.verifyToken(instance, _token);
    _instance = instance;
  }

  Future<String?> getUUID() {
    throw UnimplementedError('getUUID() has not been implemented.');
  }
}

4.1 跨端架构与调用时序

Dart 侧只做一件事:把 getUUID 原封不动地交给平台实现。真正决定「返回什么」的是原生侧。

业务代码
FlutterCachedUuid.getUUID()

门面类
FlutterCachedUuid

FlutterCachedUuidPlatform.instance

MethodChannel('flutter_cached_uuid')

ArkTS
FlutterCachedUuidPlugin

@ohos.data.preferences
沙箱内键值存储

对适配工作的直接含义是:通道名和方法名在 Dart 侧已经钉死,ArkTS 侧只能照着 flutter_cached_uuid / getUUID 这两个名字实现,改任何一个都会立刻变成 MissingPluginException。

4.1.1 一次 getUUID 的完整时序
preferences FlutterCachedUuidPlugin MethodChannel Dart 业务 preferences FlutterCachedUuidPlugin MethodChannel Dart 业务 alt [命中缓存] [未命中] invokeMethod("getUUID") onMethodCall(getUUID) 选择上下文(Ability 优先,应用级兜底) getPreferences(context, "flutter_cached_uuid") get("flutter_cached_uuid", "") 已存在的 UUID util.generateRandomUUID(true).toUpperCase() put + flush result.success(uuid) Future<String?> 完成

时序图里有两个容易被忽略的点,也是本库适配的核心:上下文从哪来、什么时候必须 flush,分别写在 5.2 与 5.3。

4.2 三层结构的意义

PlatformInterface 这一层看着多余,实际是这个库能在四个平台共用一套 Dart 代码的原因:

层作用适配时是否需要改
门面 FlutterCachedUuid静态方法,业务只认这一层否
平台接口 FlutterCachedUuidPlatform定义契约、持有默认实现、支持替换否
通道实现 MethodChannelFlutterCachedUuid真正发通道消息否(通道本身与平台无关)

PlatformInterface.verifyToken 保证只有本库创建的实例才能被 set instance 替换,避免业务侧误用别的实现——这也是单元测试能注入 mock 的前提(见 8.1)。

4.3 公开 API

名称描述类型返回值
FlutterCachedUuid.getUUID()获取缓存过的 UUID,首次调用生成并持久化静态方法Future<String?>
FlutterCachedUuidPlatform.instance获取 / 设置平台实现实例静态属性FlutterCachedUuidPlatform
FlutterCachedUuidPlatform.getUUID()平台实现需覆写的方法抽象方法Future<String?>

返回值声明为 Future<String?> 而不是 Future<String>:通道方法在原生侧返回非字符串时(例如插件异常路径),Dart 侧可能拿到 null。业务侧建议按 7.3 的写法处理空值。

4.4 为什么 Dart 层可以零改动

因为「缓存」这件事天然属于平台侧:Android 的 SharedPreferences、iOS 的 Keychain、OpenHarmony 的 preferences 都是各自的持久化设施,Dart 侧既拿不到也不该拿。Dart 层只负责发一条 getUUID 请求,把「生成与存储」全部留给原生,这正是本库在鸿蒙上能不改一行 Dart 就落地的原因。

对比上一类「纯 Dart 库」(例如十六进制色值解析),那种库的适配是「宿主工程 + 文档」,而本库必须写真实的 ArkTS 实现——两者的适配工作量不在一个量级,判断依据就是 Dart 层有没有 MethodChannel。


五、ArkTS 平台实现

5.1 插件注册

pubspec.yaml 声明 ohos 平台后,Flutter 工具会在构建时为宿主工程生成注册代码。本库适配后的 example/ohos/entry/src/main/ets/plugins/GeneratedPluginRegistrant.ets 内容如下:

import { FlutterEngine, Log } from '@ohos/flutter_ohos';
import IntegrationTestPlugin from 'integration_test';
import FlutterCachedUuidPlugin from 'flutter_cached_uuid';

export class GeneratedPluginRegistrant {
  static registerWith(flutterEngine: FlutterEngine) {
    try {
      flutterEngine.getPlugins()?.add(new IntegrationTestPlugin());
      flutterEngine.getPlugins()?.add(new FlutterCachedUuidPlugin());
    } catch (e) {
      Log.e(TAG, "Tried to register plugins with FlutterEngine failed.");
      Log.e(TAG, "Received exception while registering", e);
    }
  }
}

GeneratedPluginRegistrant.ets 是生成文件,通常不应手工编辑,下次构建可能覆盖它;注册是否生效可以直接看 import 语句里有没有本库条目。

5.2 上下文从哪来:AbilityAware 而不是只要 Ability

preferences 的读写必须拿到 Context。Flutter OHOS 的插件框架给了两个来源,本实现两个都用,并按「Ability 优先、应用级兜底」的顺序取:

export default class FlutterCachedUuidPlugin
  implements FlutterPlugin, MethodCallHandler, AbilityAware {

  private applicationContext: common.Context | null = null;
  private abilityContext: common.UIAbilityContext | null = null;

  onAttachedToEngine(binding: FlutterPluginBinding): void {
    this.applicationContext = binding.getApplicationContext();
    this.channel = new MethodChannel(binding.getBinaryMessenger(), CHANNEL_NAME);
    this.channel.setMethodCallHandler(this);
    hilog.info(DOMAIN, TAG, 'attached to engine, channel [%{public}s] registered', CHANNEL_NAME);
  }

  onAttachedToAbility(binding: AbilityPluginBinding): void {
    this.abilityContext = binding.getAbility().context;
    hilog.info(DOMAIN, TAG, 'attached to ability');
  }

  onDetachedFromAbility(): void {
    // 释放引用避免泄漏,后续调用自动回退到应用级上下文
    this.abilityContext = null;
  }
}
上下文来源何时可用用途
applicationContextFlutterPluginBinding.getApplicationContext()插件挂载到引擎时即获得,始终可用兜底,保证没有 Ability 时也能读写
abilityContextAbilityPluginBinding.getAbility().contextUIAbility 附着后才有首选,语义对齐 Android 的 onAttachedToActivity

如果只实现 FlutterPlugin 不实现 AbilityAware,在「引擎已创建但 UIAbility 尚未附着」的时间窗里调用 getUUID 就会拿不到上下文——本次实现用 applicationContext 兜住了这种情况,代码里对「两者都为空」也留了明确的错误返回,而不是抛空指针。

5.3 getOrCreateUUID:读缓存、未命中则生成并落盘

真正实现「缓存」语义的是这一段:

private async getOrCreateUUID(context: common.Context): Promise<string> {
  const store: preferences.Preferences = await preferences.getPreferences(context, PREFERENCES_NAME);

  const cachedValue: preferences.ValueType = await store.get(KEY_CACHED_UUID, NO_CACHED_UUID);
  if (typeof cachedValue == 'string' && (cachedValue as string).length > 0) {
    // 命中缓存:直接返回首次生成的值
    return cachedValue as string;
  }

  // 未命中:生成 RFC 4122 v4 UUID,并转大写与 Android / iOS 的返回格式保持一致
  const uuid: string = util.generateRandomUUID(true).toUpperCase();
  await store.put(KEY_CACHED_UUID, uuid);
  // 主动落盘,避免进程被杀死时缓存丢失(等价于 Android 的 editor.apply())
  await store.flush();
  hilog.info(DOMAIN, TAG, 'generated a new UUID and cached it into preferences');
  return uuid;
}

几个刻意的对齐点:

细节Android / iOS本次 ArkTS 实现
生成方式Android 优先设备标识派生,iOS 用 CFUUIDCreateutil.generateRandomUUID(true),与 iOS 同思路
大小写两端口径不一致,本库对外统一大写.toUpperCase()
缓存位置SharedPreferences / Keychain应用沙箱内 preferences/flutter_cached_uuid 文件
何时落盘editor.apply() / Keychain 写入即持久显式 await store.flush()
空值判定未缓存时为空get 的默认值传 '',长度为 0 视为未命中

flush() 这一步不能省。preferences 的 put 先改内存,flush 才写文件;省掉它的话,应用被系统回收时刚生成的 UUID 可能还没落盘,下次启动就变成「重新生成」——表现是「同一台机器每次冷启动都换 ID」,而看代码又找不出问题。

5.4 为什么不用 deviceInfo.udid

OpenHarmony 上确实存在「设备唯一标识」,但对普通应用不可用:

字段需要的权限三方应用能否申请
deviceInfo.udid / deviceInfo.serialohos.permission.sec.ACCESS_UDID否,仅系统应用与企业应用
deviceInfo.distributionOSVersion 等版本字段无可以

申请不到就意味着「拿设备 SN 当 ID」这条路对三方应用是堵死的。因此本实现选择了与 iOS 相同的策略:本地随机生成一次、持久化缓存,不采集任何硬件信息,也不需要任何权限。

这样做的代价与收益都写在下表里,接入前应对齐业务预期:

平台缓存位置生成方式卸载重装后同设备不同应用
AndroidSharedPreferences优先由设备标识派生变化可能相同(同源派生)
iOSKeychain随机生成(CFUUIDCreate)通常不变(Keychain 数据保留)不同
OpenHarmony应用沙箱 preferences随机生成并 flush 落盘变化(随应用数据清除)不同

也就是说:鸿蒙侧的 UUID 是「应用安装维度」的标识,不是「设备维度」的标识。如果业务需要跨应用、跨重装都稳定的标识,应当改用账号体系或系统级授权标识,而不是本库——这一点在 README.OpenHarmony_CN.md 的「其他」小节也写明了。

5.5 权限

5.5.1 插件侧:HAR 无权限

ohos/src/main/module.json5 是 HAR 模块声明,内容只有模块名、类型和设备类型,没有 requestPermissions 配置段:

{
  "module": {
    "name": "flutter_cached_uuid",
    "type": "har",
    "deviceTypes": ["default", "tablet"]
  }
}

util.generateRandomUUID 与 @ohos.data.preferences 都属于免权限接口,读写的也只是应用自己的沙箱目录,因此本插件不声明、也不需要任何权限。

5.5.2 应用 entry 的权限

example/ohos/entry/src/main/module.json5 中只有模板自带的 ohos.permission.INTERNET:

"requestPermissions": [
  {"name" :  "ohos.permission.INTERNET"},
]

这一条是宿主工程模板自带的,与本库的功能无关,保留或删除都不影响运行。除此之外整个工程没有任何权限声明,也不涉及运行时授权弹窗。


六、补全交付文件并提交适配分支

6.1 除代码外还要补全哪些文件

代码适配完成后,还需要整理安装说明、接口文档、版本记录和开源信息。接收仓库有专用模板时,按其格式填写:

文件应写清楚的内容
README.OpenSource上游名称、源码地址、适配版本或提交、版权及许可证信息;按仓库模板列出第三方依赖
README.md原项目说明、OHOS 支持入口、配套 Demo 和文档链接;保留上游信息
README.OpenHarmony_CN.md简介、安装方式、版本对应关系、环境约束、权限、接口表、示例、已验证范围和遗留问题
README.OpenHarmony.md与中文说明对应的英文文档
CHANGELOG.OpenHarmony.mdOHOS 新增能力、适配版本、兼容限制与测试范围
LICENSE / NOTICE保留上游 LICENSE 原文;上游未声明协议时不要擅自添加
example/README.md依赖方式、运行目录、签名、操作步骤与效果图
pubspec.yaml核对包名、版本、SDK 约束和依赖

本仓库已提供 README.OpenHarmony_CN.md、README.OpenHarmony.md 与 CHANGELOG.OpenHarmony.md(其中 CHANGELOG.OpenHarmony.md 把「不用 UDID 的原因」也写进去了,这一点值得保留)。发布前有三处需要核对统一:

  1. 两份 README 的下载地址目前写的是 atomgit.com/CPF-Flutter/fluttertpc_flutter_cached_uuid,组织名和仓库名都与实际托管位置 atomgit.com/oh-flutter/flutter_cached_uuid 不一致;接入时以本文 7.2 的配置为准,或先统一文档地址;
  2. README 的 TAG 表中分支列写的是 br_v1.0.10_ohos,实际分支为 main,发布前统一;
  3. 许可证表述不一致:上游 LICENSE 是 BSD-3-Clause,而两份 README 的「开源协议」小节与 ohos/oh-package.json5 的 license 字段都写着 Apache-2.0。分发时以 LICENSE 原文为准,同时把这两处描述改成 BSD-3-Clause,避免下游误判合规义务。

6.2 提交前检查

提交前先完成第八节的 example 和真机测试,再从根目录检查改动:

git branch --show-current
git diff --check
git status --short
git diff --stat
git diff

检查 diff 中的依赖变化,移除本机路径及签名信息,并使文档中的版本和分支与提交内容一致。

签名凭据必须清理(本仓库当前不满足): example/ohos/build-profile.json5 里 signingConfigs 带着完整的本机签名材料——certpath / storeFile / profile 指向 C:\Users\<用户名>\.ohos\config\...,keyPassword 与 storePassword 是加密后的口令串,而这份文件已经随适配提交 1546638 进入了仓库历史(git show 1546638:example/ohos/build-profile.json5 能直接看到)。正确做法是提交前把它还原成空数组:

"signingConfigs": [],

调试签名材料只应留在开发者本机(~/.ohos/config 下的自动签名材料与个人账号绑定,不适合进入公开仓库)。已经提交的记录需要按接收仓库的安全要求决定是否清理历史;后续提交务必先还原。

6.3 提交并推送到 AtomGit

文档和代码整理完成后,在根目录暂存并提交。文件名按项目实际情况调整:

git add lib pubspec.yaml ohos example test
git add README.OpenHarmony_CN.md README.OpenHarmony.md
git add CHANGELOG.OpenHarmony.md
git diff --cached --check
git diff --cached --stat
git commit -m "feat(ohos): add OpenHarmony platform support"
git remote -v
git branch --show-current
git push -u origin main
git tag 1.0.10-ohos-1.0.0-beta.1
git push origin 1.0.10-ohos-1.0.0-beta.1

本例的适配已合并为一次提交 1546638,推送在 AtomGit 的 main 分支上,TAG 1.0.10-ohos-1.0.0-beta.1 已随提交推送(见图 2)。

图 2 里那两处未提交改动,处理建议如下:

# example/analysis_options.yaml:新增了 analyzer.exclude(build/**、android/**、ios/**)
# 本库已经补了 ohos/,建议同时排除 ohos/**,否则 analyze 会扫到宿主工程
git diff example/analysis_options.yaml

# example/pubspec.lock:随依赖解析更新,随 example 一起提交即可
git add example/analysis_options.yaml example/pubspec.lock
git commit -m "chore(example): exclude build output from analysis and refresh lockfile"

推送后在 AtomGit 发起合并请求,说明上游来源和版本、适配范围(ArkTS 生成 + preferences 持久化,Dart 层零改动)、依赖变化(仅 pubspec 新增 ohos 平台声明)、测试环境、操作结果、已知限制,并附 Demo 运行图。目标分支和评审流程以接收仓库要求为准。


七、使用根目录 example 演示接入

仓库自带 example/,可以直接用来调试和体验「首次生成、之后命中缓存」的完整链路。

7.1 本地适配时使用路径依赖

当前 example/pubspec.yaml 的依赖是:

dependencies:
  flutter:
    sdk: flutter
  flutter_cached_uuid:
    path: ../

../ 相对于 example/pubspec.yaml 指向仓库根目录,修改根目录的库代码后可直接联调。

7.2 通过 AtomGit 引入库

业务应用通过 AtomGit 引入时,将 flutter_cached_uuid 的 path 配置替换为下面的 Git 依赖。这里固定到本文使用的 TAG:

dependencies:
  flutter:
    sdk: flutter
  flutter_cached_uuid:
    git:
      url: https://atomgit.com/oh-flutter/flutter_cached_uuid.git
      ref: 1.0.10-ohos-1.0.0-beta.1

使用自己的适配版本时,先推送分支,再将 url 改为对应仓库,ref 改为分支名。正式发布后可固定到 tag 或 commit。

从仓库根目录执行:

cd example
flutter pub get
flutter pub deps

检查 example/pubspec.lock 中 flutter_cached_uuid 的来源为 git,并核对 url、ref 和 resolved-ref。同时检查没有 dependency_overrides 或 pubspec_overrides.yaml 将其覆盖回本地依赖,确认应用使用的是 Git 依赖。

7.3 调用接口获取 UUID

业务侧的典型用法只有一行,但要处理好空值与异常:

import 'package:flutter/services.dart';
import 'package:flutter_cached_uuid/flutter_cached_uuid.dart';

Future<String> ensureDeviceId() async {
  try {
    final String? uuid = await FlutterCachedUuid.getUUID();
    if (uuid == null || uuid.isEmpty) {
      // 原生侧返回了非字符串:按「无标识」降级,不要当成崩溃
      return '';
    }
    return uuid;
  } on PlatformException catch (e) {
    // 原生侧抛错时 code 为 CACHED_UUID_ERROR,message 带具体原因
    print('getUUID failed: ${e.code} / ${e.message}');
    return '';
  } on MissingPluginException {
    // 当前平台没有注册插件(例如 Web / 未适配的平台)
    return '';
  }
}

调用时机建议放在应用启动后的第一屏,而不是 main() 的最前面——preferences 依赖 Ability 上下文,太早调用会走到应用级上下文兜底或直接报「no context available」。

7.4 业务侧的三条注意事项

  1. 不要把它当设备唯一标识:同设备不同应用、卸载重装后都会变(见 5.4 的对照表);
  2. 不要高频调用后自己再缓存一层:库本身已经缓存,同进程内每次调用只花 1–2 ms(见 8.6 的实测耗时),再套一层容易在清数据后与库内真实值不一致;
  3. 不要用它做安全凭据:UUID 是随机串,可被清数据重置,不具备防伪能力,只能做「同一安装实例的关联键」。

八、验证、构建与鸿蒙设备运行效果

8.1 分别验证库与 example

本仓库包含单元测试 test/flutter_cached_uuid_method_channel_test.dart,用 setMockMethodCallHandler 把通道打桩成固定返回 '42',验证 MethodChannelFlutterCachedUuid.getUUID() 的透传行为。从仓库根目录执行:

flutter pub get
flutter test
flutter analyze
cd example
flutter pub get
flutter analyze

一个需要留意的历史遗留:这个用例的函数名还叫 test('getPlatformVersion', ...),是从插件模板复制后没改名,实际断言的是 getUUID。它不影响结果,但会让第一次读代码的人误以为漏测了方法,建议顺手改名。

单元测试验证的是 Dart 层透传,所有平台一致;生成、持久化与上下文获取只能在鸿蒙设备上确认。

8.2 确认设备连接

hdc list targets
flutter devices

设备首次连接电脑时,需要在手机端确认调试授权。列表为空时,检查 USB 连接、调试模式和电脑授权。

8.3 配置签名

真机安装的 HAP 需要有效签名。本仓库当前提交里的 signingConfigs 是别人的本机材料,不能直接用(见 6.2),请替换为自己的调试签名:

  1. 用 DevEco Studio 打开 example/ohos,不是仓库根目录;
  2. 等待工程 Sync 成功,确认 Project 视图中存在 entry 模块;
  3. 打开 File > Project Structure > Signing Configs;
  4. 勾选 Automatically generate signature;
  5. 确认设备、应用包名、证书和 Profile 匹配(调试 Profile 与 bundleName: com.flappy.flutter_cached_uuid_example 强绑定);
  6. 再回到终端执行构建或安装。

8.4 构建与安装

在 example/ 目录执行 flutter run -d <device-id> 可以直接构建运行。仅构建 HAP 时:

flutter build hap --debug

Windows 环境下该命令可能因 DevEco Studio 26 的 ohpm.bat 批处理递归缺陷中断(见 9.12),可改用工程内等价的构建命令:

cd example/ohos
hvigorw assembleHap -p product=default -p buildMode=debug --no-daemon

本次验证用的正是后者,构建结尾输出:

> hvigor Finished :entry:assembleHap... after 1 ms
> hvigor BUILD SUCCESSFUL in 15 s 273 ms
50 tasks in total: 24 executed, 26 up-to-date

产物位于 example/ohos/entry/build/default/outputs/default/entry-default-signed.hap(另有 entry-default-unsigned.hap)。真机安装选择已签名产物:

hdc install -r entry-default-signed.hap
hdc shell aa start -a EntryAbility -b com.flappy.flutter_cached_uuid_example

这次执行里编译与打包任务多为 UP-TO-DATE(源码自 10-19 起未再改动),构建结论为 BUILD SUCCESSFUL,因此产物与当前源码一一对应。pack.info 中的 apiVersion 为 {"compatible": 24, "releaseType": "Release", "target": 24},与测试设备(API 24)一致。

8.5 在设备上测试缓存语义

example 页面按「当前值 / 操作 / 稳定性 / 历史」分区,逐项核对:

  1. 启动应用:Cached UUID 卡片显示 Tap "Get UUID" below,稳定性格显示 Not enough samples,历史区为 No call yet.(图 5);
  2. 点按 Get UUID:卡片出现大写 UUID,历史区新增第 1 条并给出耗时(首次调用含生成与落盘,通常 130–150 ms,图 5);
  3. 点按 Call x3:连续调用 3 次,稳定性结论变为 Stable: same UUID returned,副标题为 Calls: 4 · Distinct values: 1,历史区第 2–4 条的耗时降到 1–2 ms(图 6 左);
  4. 退出应用后重新打开(或 hdc shell aa force-stop 后重新 aa start):再点 Get UUID,返回值与重启前完全相同——这是「缓存」生效的直接证据(图 6 右);
  5. 点按历史条目右侧的复制图标:底部出现 UUID copied 提示,说明 clipboard 在鸿蒙上同样可用(图 7 左);
  6. 清除应用数据(hdc shell bm clean -n com.flappy.flutter_cached_uuid_example -d)后重新启动再取一次:返回值变为新的 UUID——与 5.4 表格里「随应用数据清除而变化」一致(图 7 中);
  7. 对照 About 区两条说明(Generated once and persisted、No permission required),确认运行环境与文档一致(图 7 右)。

本文已在真机完成上述全部 7 项,对应 7 张实拍截图(见 8.6)。

8.6 鸿蒙设备运行效果

首次调用与连续调用,是这套语义最核心的两张图:

启动初始状态 首次调用生成 UUID

启动初始状态首次调用
卡片提示 Tap "Get UUID" below,Calls: 0 · Distinct values: 0,历史区 No call yet.生成 10DF75F5-68D3-4692-BB24-5F13C1B74BBF,历史第 1 条耗时 145 ms(含生成与 flush)

连续调用与进程重启后的返回值:

连续调用返回同一个值 杀进程重启后仍为同一个值

连续调用 x3(同一进程)强制退出并重启后
Stable: same UUID returned,Calls: 4 · Distinct values: 1,第 2–4 次耗时 1 ms / 1 ms / 2 ms进程重建、历史清空后再取一次,返回值仍是 10DF75F5-…-5F13C1B74BBF,耗时 134 ms(冷启动首次调用需打开 preferences 文件)

复制提示、清数据后重新生成,以及 About 区的行为声明:

复制 UUID 的提示 清除应用数据后重新生成 About 区说明

复制提示清除数据后About 区
底部 UUID copied,说明剪贴板能力可用bm clean -d 后重新生成 4A2E670F-EE76-4972-9D8B-84F841F96D85,耗时 151 ms明确写着「生成一次即持久化」,且「无需 ACCESS_UDID 权限」

界面之外,插件的注册、命中与生成都可以从 hilog 独立验证(TAG FlutterCachedUuidPlugin,下面按 TAG 与消息裁剪了模块前缀列):

# 进程 9413:首次调用,生成了新值并落盘
16:08:28.057  Adding plugin: FlutterCachedUuidPlugin
16:08:28.057  attached to engine, channel [flutter_cached_uuid] registered
16:08:28.057  attached to ability
16:08:48.534  onMethodCall: getUUID
16:08:48.677  generated a new UUID and cached it into preferences
16:08:48.677  getUUID succeeded

# 同一进程内连续 3 次:只有 succeeded,没有 generated,说明走的是缓存
16:09:00.756  onMethodCall: getUUID
16:09:00.757  getUUID succeeded
16:09:00.757  onMethodCall: getUUID
16:09:00.758  getUUID succeeded
16:09:00.759  onMethodCall: getUUID
16:09:00.759  getUUID succeeded

# 进程 9523:强制退出后重启,直接命中缓存
16:09:18.120  attached to engine, channel [flutter_cached_uuid] registered
16:09:24.407  onMethodCall: getUUID
16:09:24.407  getUUID succeeded

# 进程 9999:bm clean -d 清数据后重新生成
16:10:03.968  onMethodCall: getUUID
16:10:04.116  generated a new UUID and cached it into preferences
16:10:04.116  getUUID succeeded

落盘结果也可以直接在设备上查到,preferences 把键值写成了 XML:

$ hdc shell ls -l /data/app/el2/100/base/com.flappy.flutter_cached_uuid_example/haps/entry/preferences
-rw-rw---- 1 20020242 20020242 160 2026-10-22 16:10 flutter_cached_uuid
-rw-rw---- 1 20020242 20020242   0 2026-10-22 16:10 flutter_cached_uuid.lock

$ hdc shell cat /data/app/el2/100/base/com.flappy.flutter_cached_uuid_example/haps/entry/preferences/flutter_cached_uuid
<?xml version="1.0" encoding="UTF-8"?>
<preferences version="1.0"><string key="flutter_cached_uuid">4A2E670F-EE76-4972-9D8B-84F841F96D85</string></preferences>

文件里的值与图 7 中清数据后重新生成的值完全一致,说明「Dart 拿到什么」和「磁盘上存了什么」是同一条链路的结果,不存在中间层再加工。

需要说明的是:getUUID 返回的字符串取决于首次生成时的随机值,各设备、各次安装都不同;而通道名、方法名与「生成一次、之后命中」的行为与设备硬件无关,在所有支持 Flutter 的 OpenHarmony 设备上一致。


九、FAQ:适配过程与使用问题

9.1 Missing SDK components

典型错误如下:

Missing SDK components. SDK path: ...,
missing components: toolchains,ets,js,native,previewer.

这个错误发生在 Hvigor 同步阶段。通常需要检查构建工具使用的 SDK 路径、组件是否完整,以及 Hvigor 与 SDK 的版本是否匹配。

处理顺序:

  1. 在 DevEco Studio SDK Manager 中确认对应 API 的组件已经下载完整;
  2. 检查 Flutter 和 DevEco Studio 使用的 SDK 路径是否一致;
  3. 避免误用不完整的 SDK 目录;
  4. 确认 SDK 根目录下存在 toolchains、ets、js、native、previewer;
  5. 执行 flutter config --ohos-sdk <正确路径>;
  6. 重新执行 flutter doctor -v 和 DevEco Studio Sync。
为什么连接 API 24 手机仍然会报这个错误?

因为 Sync 和 Compile 首先读取宿主机本地 SDK。手机 API 版本只在部署、安装和运行时参与兼容判断。即使完全不连接手机,本地 SDK 不完整时也会得到相同错误。

当前工程的 compatibleSdkVersion 是 6.1.1(24)(API 24),因此 API 24 设备在安装版本门槛上是满足的。

9.2 DevEco Studio 中看不到 entry 模块

可安装应用的 entry 模块位于 example/ohos/entry。

请直接使用 DevEco Studio 打开:

flutter_cached_uuid/example/ohos

如果仍看不到 entry,先解决 SDK Sync 错误,再检查 example/ohos/build-profile.json5 的 modules 是否包含 ./entry。同步失败时,Project Structure 无法正确解析模块,签名界面也可能不显示 entry。

9.3 仓库里的签名配置能直接用吗

不能。本仓库 example/ohos/build-profile.json5 里的 certpath / storeFile / profile 指向的是提交者本机的 ~/.ohos/config 路径,口令也是与本机证书绑定的加密串。换一台机器构建时,这些路径不存在,签名会直接失败;即使路径恰好存在,证书与包名、Profile 也不匹配。

请按 8.3 用自动签名为自己的环境重新生成,并把这份文件还原成 "signingConfigs": [] 后再提交(见 6.2)。

9.4 安装报 code:9568297 older sdk version in the device

工程声明的 compatibleSdkVersion 高于设备 API 版本时会出现这个报错。处理方式是把 example/ohos/build-profile.json5 中 product 的两个版本值下调到设备支持的 API:

"compatibleSdkVersion": "6.1.1(24)",
"targetSdkVersion": "6.1.1(24)",

判断依据是设备 param get const.ohos.apiversion 的返回值——工程声明的 API 版本不能高于目标设备的 API 版本。

9.5 安装报 no signature file 或 install version downgrade

真机安装的另外两个常见错误码:

报错原因处理
code:9568320 no signature file装了未签名 HAP使用 entry-default-signed.hap 安装
code:9568263 install version downgrade目标包名已被更高 versionCode 的应用占用卸载旧应用、换包名或提升 versionCode

9.6 清除应用数据 / 卸载重装后 UUID 变了,是不是 bug

不是,这是设计如此。缓存写在应用沙箱的 preferences 里,清数据或卸载会一并删除(见 5.4 的对照表,图 7 中就是清数据后重新生成的新值)。如果需要「重装后不变」,只能用账号体系或系统级授权标识,本库在鸿蒙上做不到,在 Android 上同样做不到。

9.7 getUUID 返回 null 或抛 MissingPluginException

按可能性排序:

  1. 插件没注册:检查 GeneratedPluginRegistrant.ets 里有没有 new FlutterCachedUuidPlugin(),以及 pubspec.yaml 的 ohos.pluginClass 是否为 FlutterCachedUuidPlugin;
  2. 通道名被改:Dart 侧固定是 flutter_cached_uuid,ArkTS 侧 CHANNEL_NAME 必须完全一致;
  3. 在非鸿蒙平台调用:Web、Windows 等没有对应实现,MissingPluginException 是预期行为,按 7.3 的方式捕获降级;
  4. getUUID 被提前调用:Ability 还没附着、应用级上下文也没拿到时会返回 CACHED_UUID_ERROR,提示 No context available to access preferences,把调用时机放到首屏之后即可(见 7.3)。

9.8 冷启动第一次调用要 130 ms 左右,正常吗

正常,而且这不是生成 UUID 的开销。preferences.getPreferences 每次进程首次调用都要打开(必要时创建)沙箱里的存储文件,这部分耗时占了大头;同一进程内的后续调用是 1–2 ms(见图 6 左)。首次「生成 + put + flush」比「仅命中」多花十几毫秒(145 ms 对 134 ms),量级差别不大。

如果业务对启动耗时敏感,可以在首屏渲染完成后再异步取,或把结果保存在内存里复用。

9.9 flutter create 不认识 ohos

先执行 flutter --version,确认使用的是 OHOS 版工具链,环境配置回到第二节的社区链接核对。注意 --template=plugin 只应对库根目录执行一次;对 example/ 或其他目录重复执行会引入多余的模板文件,生成后务必 diff 检查并清理。

9.10 AtomGit 依赖提示找不到分支或无权限

先检查 URL 是否指向已同步的目标仓库(https://atomgit.com/oh-flutter/flutter_cached_uuid.git),再确认 TAG 1.0.10-ohos-1.0.0-beta.1 已推送。仓库 README 中目前的下载地址(CPF-Flutter/fluttertpc_flutter_cached_uuid)与实际不一致(见 6.1),接入时以本文 7.2 的配置为准,或先统一文档地址。私有仓库还需在本机配置 Git 认证。

9.11 改了本地库代码,Demo 为什么没变化

先检查 example/pubspec.yaml:Git 依赖读取远程提交,不会自动读取本地库改动。本地联调切回 path: ../。Dart 文件变更用热重载/热重启即可生效;但 ohos/ 内 ArkTS 代码变更需要重新构建 HAP 并重新安装。

9.12 flutter build hap 卡在 ohpm install:BATCH RECURSION exceeds STACK limits

Windows 环境下最坑的一个问题,报错如下:

> hvigor start to execute ohpm install
****** B A T C H   R E C U R S I O N  exceeds STACK limits ******
Recursion Count=242, Stack Usage=90 percent
> hvigor ERROR: 00306053 Specification Limit Violation
Error Message: ohpm install failed.

排查路径:

  1. 手动 ohpm install 完全正常 → 排除网络与依赖问题;
  2. bash 里直接 hvigorw assembleHap 正常 → 说明与 flutter 的调用链有关;
  3. 读 hvigorw.js 发现它无条件把 process.env.ohpmBin 覆盖成 DevEco 自带的 ohpm.bat;ohpm.bat 的首参只要包含 ohpm 就会进逐字符递归;
  4. 对比 PATH:Git Bash 的 usr/bin 里有一个无扩展名的 ohpm shell 脚本,命令解析走了脚本分支,参数被整体当成 %1,递归层数正好等于整条命令行长度。

解决:构建时使用不含 Git usr/bin 的 PATH(保留 Git/cmd 供 flutter 找 git),或改用 8.4 的 hvigorw assembleHap 命令。

9.13 spawn java ENOENT

越过 ohpm 之后,打包阶段(PackageHap)报:

> hvigor ERROR: Failed :entry:default@PackageHap...
> hvigor ERROR: Error Code: 00308018 Unknown Error
spawn java ENOENT

PackageHap 用 java 跑打包工具,环境里没有 java 就报这个「Unknown Error」。补 DevEco 自带 JBR(<DevEco Studio>/jbr)或设置 JAVA_HOME 即可,与工程代码无关。


相关链接

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐