Flutter flutter_udid 库在鸿蒙(OpenHarmony)平台的适配实践
引言
鸿蒙生态的快速发展,让不少 Flutter 应用都开始考虑跨平台部署。flutter_udid 这个插件在 Android 和 iOS 上用得挺多,主要用来获取一个稳定的设备唯一标识(UDID)------它的最大特点是应用重装后标识不变,所以常用在用户追踪、数据关联这些业务场景里。
不过,官方并没有提供对鸿蒙(OpenHarmony)的原生支持。这就导致 Flutter 应用跑在鸿蒙设备上时,拿不到那个持久化的唯一标识,直接影响功能完整性。这篇文章会从 Flutter 插件的架构讲起,把它的通信机制拆解开,然后给出一套完整的适配方案,包括原理分析、代码实现、性能优化和最终的集成验证。目标不只是解决 flutter_udid 的问题,更希望能为其他 Flutter 插件适配鸿蒙提供一个可以参考的技术思路和实践模板。
一、先搞明白:技术背景和适配原理
1.1 Flutter 插件是怎么跟原生平台通信的?
Flutter 插件本质上就是 Dart 框架和原生平台(Android/iOS)之间的桥梁。它的核心是 平台通道(Platform Channel),两边通过约定好的消息编码格式(比如 StandardMessageCodec)进行异步通信。这个设计保证了 Flutter 本身是平台无关的。
- MethodChannel :用于"方法调用-结果返回"这种模式。Dart 端调用一个方法,然后等着原生端返回结果(成功或失败)。这是插件最常用的通道,
flutter_udid的核心接口getUDID()就是基于它实现的。 - 映射到鸿蒙 :我们要做的,就是在鸿蒙这一侧也建立一个对等的
MethodChannel处理模块,让它能响应 Dart 端发过来的同名方法调用。
1.2 鸿蒙和 Android 有哪些关键区别?
要做适配,首先得弄清楚目标平台和原有平台到底哪里不一样。下面是一些关键点的对比:
| 特性维度 | Android | OpenHarmony (ArkTS) | 适配影响与策略 |
|---|---|---|---|
| 开发语言 | Java, Kotlin | ArkTS (TypeScript 超集), C++ | 需要用 ArkTS 重写原生代码,并适应它的异步编程模型(基于 Promise/Async)。 |
| 包管理与构建 | Gradle, build.gradle |
Hvigor, build-profile.json5 |
需要创建鸿蒙模块(oh-package.json5),配置 HAP 构建依赖和 Native 能力。 |
| 设备标识 API | Settings.Secure.ANDROID_ID TelephonyManager.getDeviceId() |
system.deviceInfo (需权限) deviceInfo.networkId deviceInfo.udid (受限) |
不能直接照搬 Android API。得研究鸿蒙 @ohos.deviceInfo 接口怎么用、唯一性怎么样、要哪些权限。 |
| 持久化存储 | SharedPreferences android.content.Context |
Preferences (@ohos.data.preferences) |
用鸿蒙的 Preferences API 替代 SharedPreferences 来存、取 UDID。 |
| 项目结构 | android/ 目录嵌入 Flutter 项目 |
独立的鸿蒙工程,通过 ohos/ 目录或子模块和 Flutter 项目关联 |
需要把 Flutter 项目改造成支持鸿蒙的混合工程结构。 |
1.3 flutter_udid 的核心工作流程
要有效适配,得先吃透原插件是怎么工作的。它的核心逻辑其实是一个 "生成-存储-读取" 的持久化流程:
- 首次获取 :应用第一次调用
getUDID()时,原生端会尝试从安全存储(比如SharedPreferences)里读取。如果读不到,就新生成一个随机的 UUID(版本4)。 - 持久化存储:把新生成的 UUID 以键值对的形式,写入平台提供的安全、持久的存储中。
- 后续读取:应用之后再调用,或者重启后再调用,都直接从那个持久化存储里读之前存好的 UUID 并返回。这样就保证了标识在应用生命周期内稳定不变。
- Dart 层缓存:为了提升性能,插件在 Dart 层会对获取到的 UDID 做内存缓存,避免频繁走平台通道调用。
适配关键点 :我们要做的,就是在鸿蒙端 "复现" 这一整套行为逻辑。只需要替换掉它底层的平台实现(比如标识生成的来源、存储用的 API),而保持 Dart 层的接口和行为完全不变。
二、动手之前:环境准备和工程改造
2.1 把开发环境搭起来
bash
# 1. 确认 Flutter 环境 (推荐 3.13.0+,对鸿蒙支持更好)
flutter doctor -v
# 确保 Flutter SDK 包含了鸿蒙桌面设备支持
# 2. 配置鸿蒙原生开发环境
# - 下载安装 DevEco Studio 4.0 Release 或更高版本
# - 在 SDK Manager 里安装 HarmonyOS SDK (API 9+)
# - 安装对应的 ArkTS/JS SDK 和 Toolchains
# 3. 配置环境变量 (以 macOS/Linux 为例)
export HARMONYOS_SDK_HOME=~/Library/Huawei/Sdk
export PATH=$PATH:$HARMONYOS_SDK_HOME/toolchains:$HARMONYOS_SDK_HOME/openharmony
# 把 `ohos` 命令行工具加入 PATH
2.2 改造 Flutter 项目,让它支持鸿蒙
我们需要把普通的 Flutter 项目改造成支持鸿蒙的 "混合工程"。
bash
# 在你的 Flutter 项目根目录执行
flutter create --platforms=ohos .
# 执行后,项目会生成或更新一个 `ohos/` 目录,结构大概是这样:
# ohos/
# ├── AppScope/ # 应用级资源
# ├── entry/ # 主模块,Flutter 引擎的入口
# │ ├── src/
# │ │ └── main/
# │ │ ├── ets/
# │ │ │ ├── entryability/ # Ability 入口(类似 Android 的 Activity)
# │ │ │ └── pages/ # 页面
# │ │ └── resources/ # 模块资源
# │ ├── build-profile.json5 # 模块构建配置
# │ └── oh-package.json5 # 模块依赖声明
# └── build-profile.json5 # 工程级构建配置
这个命令会自动配置好 Flutter 引擎和鸿蒙 entry Ability 之间的桥接,是后面插件开发的基础。
三、核心步骤:用 ArkTS 实现鸿蒙原生代码
这部分是适配的核心。我们会在 entry 模块里创建插件的具体实现。
3.1 创建鸿蒙侧的 MethodChannel 处理器
文件路径 :ohos/entry/src/main/ets/FlutterUdidPlugin.ets
typescript
// 导入鸿蒙系统能力接口
import deviceInfo from '@ohos.deviceInfo';
import preferences from '@ohos.data.preferences';
import { BusinessError } from '@ohos.base';
import { UIAbilityContext } from '@ohos.abilityKit';
// 定义和 Dart 端约定好的常量
const CHANNEL_NAME = 'com.example.flutter_udid/udid';
const STORE_KEY = 'flutter_udid';
const PREFERENCE_NAME = 'FlutterUdidStore';
export class FlutterUdidPlugin {
private methodChannel: any; // 实际应该是 Flutter 引擎提供的 MethodChannel 类型,这里先用 any 表示
private context: UIAbilityContext;
private preferences: preferences.Preferences | null = null;
// 构造函数,需要传入 Ability 的上下文
constructor(context: UIAbilityContext) {
this.context = context;
// 注意:如何获取 Flutter 引擎的 MethodChannel 是个技术关键点。
// 通常需要通过 Flutter 自动生成的 `FlutterOhosPlugin` 基类,或者特定的注册方式来获取。
// 这里先假设我们能通过一个全局的插件注册表来设置。具体集成方式看下文。
}
// 初始化方法,Flutter 引擎在注册插件时会调用这个
initialize(methodChannel: any): void {
this.methodChannel = methodChannel;
this.methodChannel.setMethodCallHandler(this.handleMethodCall.bind(this));
this.initPreferences();
}
// 异步初始化持久化存储 Preferences
private async initPreferences(): Promise<void> {
try {
// 获取应用沙箱路径下的 Preferences 文件
this.preferences = await preferences.getPreferences(this.context, PREFERENCE_NAME);
console.info('[FlutterUdidPlugin] Preferences 初始化成功。');
} catch (error) {
const err: BusinessError = error as BusinessError;
console.error(`[FlutterUdidPlugin] 初始化 Preferences 失败: 错误码: ${err.code}, 信息: ${err.message}`);
this.preferences = null;
}
}
// 核心:处理从 Dart 端发过来的方法调用
private async handleMethodCall(call: any): Promise<any> { // call.method, call.arguments
switch (call.method) {
case 'getUDID':
return await this.getUdid();
default:
// 按照 Platform Channel 的规范,抛出未实现的异常
return Promise.reject({
code: 'notImplemented',
message: `鸿蒙端暂未实现方法 ${call.method}。`,
details: null
});
}
}
// 获取 UDID 的核心逻辑
private async getUdid(): Promise<string> {
// 1. 先尝试从持久化存储里读
let storedUdid = await this.readUdidFromStore();
if (storedUdid) {
return storedUdid;
}
// 2. 存储里没有,就生成一个新的
let newUdid = this.generateFallbackUdid();
console.info(`[FlutterUdidPlugin] 生成了新的 UDID: ${newUdid}`);
// 3. 把新生成的 UDID 存起来
const success = await this.saveUdidToStore(newUdid);
if (success) {
return newUdid;
} else {
// 如果存失败了,还是把生成的 ID 返回,但要记录错误
console.error('[FlutterUdidPlugin] 保存 UDID 到存储失败。');
return newUdid; // 具体业务上也可以考虑抛出异常
}
}
// 从 Preferences 里读取 UDID
private async readUdidFromStore(): Promise<string | null> {
if (!this.preferences) {
await this.initPreferences();
if (!this.preferences) return null;
}
try {
const value = await this.preferences.get(STORE_KEY, '');
return value !== '' ? value as string : null;
} catch (error) {
const err: BusinessError = error as BusinessError;
console.error(`[FlutterUdidPlugin] 读取 UDID 失败: 错误码: ${err.code}, 信息: ${err.message}`);
return null;
}
}
// 把 UDID 保存到 Preferences
private async saveUdidToStore(udid: string): Promise<boolean> {
if (!this.preferences) {
await this.initPreferences();
if (!this.preferences) return false;
}
try {
await this.preferences.put(STORE_KEY, udid);
await this.preferences.flush(); // 确保数据写到磁盘
return true;
} catch (error) {
const err: BusinessError = error as BusinessError;
console.error(`[FlutterUdidPlugin] 保存 UDID 失败: 错误码: ${err.code}, 信息: ${err.message}`);
return false;
}
}
// 生成备用 UDID 的策略 (先尝试拿系统 ID,不行再生成随机 UUID)
private generateFallbackUdid(): string {
try {
// 方案 A:尝试获取系统定义的 deviceId (需要权限 `ohos.permission.GET_SENSITIVE_PERMISSIONS`)
// 注意:这个 ID 可能在清除应用数据或设备重置后变化,需要评估其唯一性和持久性。
const networkId = deviceInfo.deviceId;
if (networkId && networkId.length > 0) {
// 可以对它做一次哈希处理,增加隐私安全性和格式统一性
// 比如: return this.hashString(networkId);
return `HARMONY_${networkId.substring(0, 8).toUpperCase()}`;
}
} catch (error) {
console.warn('[FlutterUdidPlugin] 无法获取 deviceId,回退到随机 UUID。');
}
// 方案 B:生成随机 UUID (版本4)
// 这是纯软件实现,不依赖硬件,兼容性最好,但完全依赖本地存储的持久性。
return this.generateV4Uuid();
}
// 生成一个版本4的 UUID
private generateV4Uuid(): string {
// 简化示例,生产环境建议用更健壮的 UUID 生成算法
const hexDigits = '0123456789abcdef';
let uuid = '';
for (let i = 0; i < 32; i++) {
if (i === 12) {
uuid += '4'; // version 4
} else if (i === 16) {
uuid += hexDigits[(Math.random() * 4) | 8]; // variant 10xx
} else {
uuid += hexDigits[(Math.random() * 16) | 0];
}
// 加连字符
if (i === 7 || i === 11 || i === 15 || i === 19) {
uuid += '-';
}
}
return uuid;
}
}
3.2 把插件注册到 Flutter 引擎
文件路径 :ohos/entry/src/main/ets/entryability/EntryAbility.ets
鸿蒙的 EntryAbility 是应用的入口点。我们需要在这里初始化和注册我们的插件。
typescript
import { UIAbility } from '@ohos.abilityKit';
import window from '@ohos.window';
import { FlutterUdidPlugin } from '../FlutterUdidPlugin'; // 引入我们写的插件类
// 注意:下面的导入路径和类名取决于 Flutter 鸿蒙引擎模板的具体生成内容
import { FlutterOhosPluginRegistrant } from '@ohos/flutter_engine/FlutterOhosPluginRegistrant';
export default class EntryAbility extends UIAbility {
private flutterUdidPlugin: FlutterUdidPlugin | null = null;
onCreate(want, launchParam): void {
console.info('[EntryAbility] onCreate');
// 1. 初始化插件实例
this.flutterUdidPlugin = new FlutterUdidPlugin(this.context);
// 2. 关键步骤:把插件注册到 Flutter 引擎。
// 这通常是通过一个全局的插件注册函数完成的。具体 API 可能随着 Flutter 对鸿蒙支持的迭代而变化。
// 假设存在下面这样的注册机制:
FlutterOhosPluginRegistrant.registerCustomPlugin((engine) => {
// 创建一个和 Dart 端同名的 MethodChannel
const channel = new engine.MethodChannel(engine.dartExecutor, 'com.example.flutter_udid/udid');
// 把 channel 传给我们的插件进行初始化
this.flutterUdidPlugin!.initialize(channel);
return this.flutterUdidPlugin;
});
}
onWindowStageCreate(windowStage: window.WindowStage): void {
console.info('[EntryAbility] onWindowStageCreate');
windowStage.loadContent('pages/Index', (err) => {
if (err.code) {
console.error(`[EntryAbility] 加载页面内容失败。错误码: ${err.code}, 信息: ${err.message}`);
return;
}
console.info('[EntryAbility] 页面内容加载成功。');
});
}
onDestroy(): void {
console.info('[EntryAbility] onDestroy');
this.flutterUdidPlugin = null;
}
}
重要说明 :上面这种注册方式 (FlutterOhosPluginRegistrant) 目前还只是概念示意。Flutter 对鸿蒙的插件注册机制可能还在完善中,实际实现时需要参考最新的 Flutter 引擎源码,或者遵循特定的插件开发模板。
3.3 配置模块依赖和权限
文件路径 :ohos/entry/oh-package.json5
json5
{
"license": "MIT",
"devDependencies": { },
"name": "@ohos/entry",
"description": "Please describe the basic information.",
"main": "ets/entryability/EntryAbility.ets",
"version": "1.0.0",
"dependencies": {
// 确保依赖了 Flutter 引擎模块
"@ohos/flutter_engine": "file:../../flutter/build/ohos/engine",
"@ohos/hmos_sdk": "^1.0.0" // 或对应的 SDK 版本
}
}
文件路径 :ohos/entry/src/main/module.json5
需要在 module.json5 里声明插件要用到的系统权限。
json5
{
"module": {
"requestPermissions": [
{
"name": "ohos.permission.GET_SENSITIVE_PERMISSIONS", // 如果需要获取 deviceId 就得申请这个
"reason": "用于生成更稳定的设备唯一标识",
"usedScene": {
"ability": ["EntryAbility"],
"when": "always"
}
}
]
}
}
四、Dart 侧代码的调整和封装(建议做)
虽然 flutter_udid 的 Dart 接口是标准的,但为了更好的兼容性和错误处理,可以专门为鸿蒙做一个包装层,或者对原插件进行条件导入。
4.1 创建一个平台兼容层
dart
// lib/harmony_udid.dart
import 'dart:async';
import 'package:flutter/services.dart';
class HarmonyUdid {
static const MethodChannel _channel =
const MethodChannel('com.example.flutter_udid/udid');
static Future<String?> get udid async {
try {
final String? result = await _channel.invokeMethod('getUDID');
return result;
} on PlatformException catch (e) {
print("从鸿蒙端获取 UDID 失败: '${e.message}'。");
// 返回一个临时的本地生成 ID(不持久)
return _generateLocalFallbackId();
} catch (e) {
print("获取 UDID 时发生意外错误: $e");
return null;
}
}
static String _generateLocalFallbackId() {
// 一个简单的本地回退方案,只用于当前会话
return 'harmony_fallback_${DateTime.now().millisecondsSinceEpoch}';
}
}
4.2 在应用里使用
dart
import 'package:your_app/harmony_udid.dart'; // 如果原插件已经通过条件编译支持鸿蒙了,也可以直接用原插件
void fetchDeviceId() async {
String? deviceUdid = await HarmonyUdid.udid;
if (deviceUdid != null) {
print('在鸿蒙上获取到的设备 UDID: $deviceUdid');
// 用到你的业务逻辑里...
} else {
print('没能获取到稳定的 UDID。');
}
}
五、一些优化和安全方面的考虑
- 存储性能 :
Preferences的flush()操作是耗时的 I/O 操作。确保只在必要的时候调用(比如首次生成并保存时)。 - ID 生成策略 :
- 隐私 :避免直接上传原始的、可能全局唯一的设备 ID(比如
deviceId)。建议对它做不可逆的哈希处理(比如 SHA-256),并且混入应用特有的盐值(Salt)。 - 稳定性 :测试一下不同鸿蒙版本和机型上
deviceInfo.deviceId的稳定性和获取成功率。明确什么情况下该用兜底的随机 UUID 方案。
- 隐私 :避免直接上传原始的、可能全局唯一的设备 ID(比如
- 错误恢复 :就像上面代码写的,当
Preferences初始化失败或者读写异常时,要有清晰的日志和合理的回退机制(比如返回一个会话 ID),保证应用功能不会因此崩溃。 - 多进程情况 :如果你的 Flutter 应用在鸿蒙上涉及多进程,需要考虑
Preferences的进程间同步问题,可能得用到分布式数据管理。
六、集成测试与验证
- 编译运行 :在 DevEco Studio 里打开
ohos目录下的工程,连接鸿蒙真机或模拟器,运行entry模块。 - 功能测试 :
- 首次安装应用,调用
getUDID,记下生成的 ID。 - 杀掉应用再重新启动,再次调用
getUDID,看看 ID 是不是一样。 - 关键测试 :清除应用数据(或者在鸿蒙设置里卸载重装),验证 ID 是否变化(按照设计,这时候应该会变,这正是"应用重装不变性"要体现的)。
- 首次安装应用,调用
- 看日志 :通过
hdc shell hilog命令查看插件打印的日志,确认生成、存储、读取的流程没问题。 - 性能测试 :用鸿蒙的性能分析工具,测一下多次调用
getUDID的耗时,确保 Dart 层的缓存生效了,没有频繁地进行平台通道交互。
七、总结
这篇文章详细介绍了把 flutter_udid 插件适配到鸿蒙平台的整个过程,从原理分析、环境配置、ArkTS 原生代码实现、Dart 层封装,到性能安全和测试验证,算是走通了一个全链路。适配的核心在于理解 Flutter 插件架构的抽象层,然后在新的原生平台上实现它约定好的接口和行为。
这套方法其实可以推广到大多数基于 MethodChannel 的 Flutter 插件的鸿蒙适配工作上。随着 OpenHarmony 生态的壮大和 Flutter 官方支持的完善,以后插件适配的流程肯定会越来越标准化、自动化。我们现在做这些实践,不仅能解决具体的技术需求,更能深入理解跨平台框架和原生系统是怎么"对话"的,为以后构建更健壮、兼容性更好的跨平台应用积累经验。