UDMF 统一数据管理框架

引言

在 HarmonyOS 应用开发中,数据在"应用内、应用间、设备间"传递时,往往会遇到一个尴尬的问题:不同的功能模块对数据的描述方式完全不同。拖拽(drag and drop)走一套数据结构,剪贴板走另一套数据结构,分享又有一套结构,跨设备传递再套一层。开发者每接入一个能力,就要重新学习一种"数据语言",代码里到处是类型强转和格式判断。

HarmonyOS 给出的答案是 UDMF(Unified Data Management Framework,统一数据管理框架)。它把"数据是什么、数据长什么样、数据能做什么"统一成一套标准化的描述体系,让拖拽、剪贴板、跨端拖拽、拖拽分享等能力可以共享同一种数据载体。本项目中的核心功能"跨设备拖拽图片、跨设备拖拽文字",底层正是由 UDMF 支撑的。

img

本文作为模块五《跨设备互通》的开篇,先讲清楚 UDMF 是什么、核心 API 怎么用,再结合本项目的真实源码,看看 UDMF 在拖拽场景中是如何"隐形地"承担数据搬运工作的。

一、知识点讲解:UDMF 是什么

1. 统一数据管理框架的定位

UDMF 是系统提供的"数据载体"层,位于 @kit.ArkData 中。它定义了一套跨应用、跨设备的统一数据模型,包含两个核心概念:

  • UnifiedData(统一数据):一次数据传递的完整载体,可以包含一条或多条数据记录。
  • UnifiedRecord(统一数据记录):UnifiedData 中的一条具体记录,每一条记录都有明确的类型标识。

官方对 UDMF 的定位是:作为拖拽、剪贴板等功能的底层数据模型,实现"一次定义,多处复用"。开发者从拖拽事件或剪贴板中拿到的数据,本质都是 UnifiedData,只是外层包装不同。

在代码层面,我们通过 unifiedDataChannel 命名空间使用它:

import { unifiedDataChannel, uniformTypeDescriptor } from '@kit.ArkData';

2. uniformTypeDescriptor:数据类型的"身份证"

UDMF 用 uniformTypeDescriptor.UniformDataType 枚举来标识数据的类型。本项目用到的有三个:

  • OPENHARMONY_PIXEL_MAP:鸿蒙系统定义的 PixelMap 像素图数据类型,用于在设备间直接传递 PixelMap。
  • IMAGE:通用图片类型,通常携带 imageUri(图片的 URI),接收方需要自己打开文件读取。
  • PLAIN_TEXT:纯文本类型,用于传递一段字符串文字。

另外还有 HYPERLINK(超链接,本项目在分享页面链接时使用)、MEDIAFILE 等类型。类型标识决定了接收方应该如何解析这条记录,是 UDMF 体系的核心。

3. UnifiedData 与 UnifiedRecord 的结构

一次拖拽产生的 UnifiedData,内部结构大致如下:

UnifiedData
├── record 0: SystemDefinedPixelMap(类型 OPENHARMONY_PIXEL_MAP)
│     ├── details: { width, height, 'pixel-format' }
│     └── rawData: Uint8Array(像素数据)
└── record 1: Image(类型 IMAGE)
      └── imageUri: string
  • data.getRecords():获取全部记录,返回 Array<unifiedDataChannel.UnifiedRecord>
  • record.getType():获取该记录的类型标识。
  • SystemDefinedPixelMapOPENHARMONY_PIXEL_MAP 类型对应的记录类,details 里携带尺寸、像素格式等元数据,rawData 里是像素数据本身。
  • PlainTextPLAIN_TEXT 类型对应的记录类,textContent 字段保存文字内容。
  • ImageIMAGE 类型对应的记录类,imageUri 字段保存图片地址。

4. UDMF 与拖拽、剪贴板的关联

UDMF 并不是一个独立的"文件传输"功能,而是一套数据格式底座

  • 拖拽:拖拽开始时,系统把被拖拽组件的数据封装成 UnifiedData;拖拽落点(onDrop)处,开发者通过 event.getData() 取回 UnifiedData。
  • 跨设备拖拽:UDMF 数据结构本身具备跨设备传输能力,键鼠共享场景下,源设备的 UnifiedData 可以完整地传到目标设备,这就是本项目"跨设备拖拽图片/文字"能实现的基础。
  • 剪贴板:剪贴板使用 pasteboard.PasteData(见模块五后续文章《系统剪贴板》),其内部同样遵循统一类型描述体系,MIMETYPE_PIXELMAPMIMETYPE_TEXT_URI 等 MIME 类型与 UDMF 类型存在对应关系。
  • 分享systemShare.SharedData 中同样通过 utd 字段声明数据类型(本项目 KnockShareModel 中分享链接时就指定了 UniformDataType.HYPERLINK)。

一句话总结:**UDMF 是"数据怎么描述",拖拽/剪贴板是"数据怎么流动"**。本项目几乎所有的跨设备数据传递,都用到了 UDMF 的类型体系。

二、结合本项目源码分析

1. 拖拽落点:从 DragEvent 取出 UnifiedData

本项目接收拖拽图片的入口在 entry/src/main/ets/view/contentEditor/AddMedia.ets。目标组件(图片列表区域所在的 Column)通过 allowDrop 声明自己能接收 IMAGEOPENHARMONY_PIXEL_MAP 两类数据,然后在 onDrop 回调中把拖拽事件携带的数据取出来:

.draggable(true)
.allowDrop([uniformTypeDescriptor.UniformDataType.IMAGE,
  uniformTypeDescriptor.UniformDataType.OPENHARMONY_PIXEL_MAP])
.onDrop((dragEvent?: DragEvent) => {
  this.getDataFromUdmf((dragEvent as DragEvent), async (event: DragEvent) => {
    try {
      let records: Array<unifiedDataChannel.UnifiedRecord> = event.getData().getRecords();
      for (let i = 0; i < records.length; i++) {
        // PixelMap converted from image to pixelMap in the image system.
        if (records[i].getType() === uniformTypeDescriptor.UniformDataType.OPENHARMONY_PIXEL_MAP) {
          let pixelMapRecord = records[i] as unifiedDataChannel.SystemDefinedPixelMap;
          // ...
        } else {
          // Convert the image from imageUri to PixelMap.
          this.uri2pixelMap((records[i] as unifiedDataChannel.Image).imageUri);
        }
      }
      event.useCustomDropAnimation = false;
      event.setResult(DragResult.DRAG_SUCCESSFUL);
    } catch (err) {
      hilog.error(DOMAIN, TAG, FORMAT, `GetData failed. Cause code: ${err.code}, message: ${err.message}`);
    }
  })
})

关键点逐一拆解:

  • event.getData() 返回的就是 UnifiedData 类型的实例。这里没有经过任何序列化、格式转换,拖拽系统已经把源端数据封装成了 UDMF 结构,这正是 UDMF "统一数据模型"的体现。
  • data.getRecords() 拿到记录数组后,用 records[i].getType()UniformDataType 枚举比较,判断每条记录到底是什么类型。
  • 对于 OPENHARMONY_PIXEL_MAP 类型的记录,强转为 SystemDefinedPixelMap,从其 detailsrawData 中读取像素信息(详见《跨设备拖拽图片》一文)。
  • 对于 IMAGE 类型的记录,强转为 Image 类型,读取 imageUri 字段,再调用 uri2pixelMap() 去文件系统中解析图片。
  • 处理完毕后调用 event.setResult(DragResult.DRAG_SUCCESSFUL),把结果回传给源端(源端在 onDragEnd 中据此判断拖拽是否成功)。

接收文字的场景在 entry/src/main/ets/view/contentEditor/EditorComponent.ets,逻辑同构:

.allowDrop([uniformTypeDescriptor.UniformDataType.PLAIN_TEXT])
.onDrop((dragEvent?: DragEvent) => {
  this.getDataFromUdmf((dragEvent as DragEvent), (event: DragEvent) => {
    try {
      let records: Array<unifiedDataChannel.UnifiedRecord> = event.getData().getRecords();
      let plainText: unifiedDataChannel.PlainText = records[0] as unifiedDataChannel.PlainText;
      this.mainTitle = plainText.textContent;
    } catch (err) {
      hilog.error(DOMAIN, TAG, FORMAT, `GetData failed. Cause code: ${err.code}, message: ${err.message}`);
    }
  })
})

这里 records[0] 被强转为 PlainText,直接取 textContent 写入标题输入框。注意:**本项目只负责"读" UDMF 数据,不负责"写"**。源端是系统的 TextInput、TextArea、Image 等内置组件,拖拽数据由系统组件自动封装为 UnifiedData,开发者无需手工构造,这正是使用系统组件做源端的好处。

2. 异步取数的重试机制

event.getData() 在跨设备拖拽时存在异步时序问题:数据尚未就绪时就调用可能拿到空结果。本项目的解决办法是 getDataFromUdmfRetry + getDataFromUdmf 两段式封装(AddMedia.ets 与 EditorComponent.ets 中各有一份相同实现):

getDataFromUdmfRetry(event: DragEvent, callback: (data: DragEvent) => void) {
  try {
    let data: UnifiedData = event.getData();
    if (!data) {
      return false;
    }
    let records: Array<unifiedDataChannel.UnifiedRecord> = data.getRecords();
    if (!records || records.length <= 0) {
      return false;
    }
    callback(event);
    return true;
  } catch (e) {
    let err = e as BusinessError;
    hilog.error(DOMAIN, TAG, FORMAT, `getData failed. Cause code: ${err.code}, message: ${err.message}`);
    return false;
  }
}

getDataFromUdmf(event: DragEvent, callback: (data: DragEvent) => void) {
  if (this.getDataFromUdmfRetry(event, callback)) {
    return;
  }
  setTimeout(() => {
    this.getDataFromUdmfRetry(event, callback);
  }, 1500);
}

逻辑很简单:先尝试立即读取,若 getData() 返回空、记录为空或抛异常,则延迟 1500ms 再试一次。这个设计恰好利用了 UDMF 数据"可能晚于拖拽事件到达"的特性——特别是跨设备拖拽时,源设备的数据需要通过网络链路传输,落点侧的 onDrop 触发与数据真正可读之间存在时间差。用一个超时重试兜底,比盲目同步读取要稳健得多。

2.1 拖拽数据的生命周期与释放

很多初学者会担心:从 event.getData() 拿到的 UnifiedData 用完后要不要手动释放?在本项目的代码里可以看到,拖拽数据并不需要开发者显式释放。UnifiedData 的生命周期由系统托管:拖拽开始时系统创建并填充数据,onDrop 回调里开发者读取,回调结束后系统统一回收。开发者需要自己负责的,是从 UDMF 数据派生出来的资源——比如从 rawData 重建出来的 PixelMap、从 URI 打开的文件句柄。本项目在 uri2pixelMapfinally 中调用 fileIo.closeSync(file.fd) 关闭句柄、在 PixelMap 处理完后调用 imageSource.release() 释放图像源,正是这个分工的体现:系统管 UDMF,开发者管自己创建的派生资源。

另外值得注意的细节是 records 数组的处理方式。项目在 getDataFromUdmfRetry 中先判断 records.length <= 0 就返回 false,在 onDrop 中又用 for 循环遍历全部记录——这两步看似重复,实则分工明确:前者是"确认数据真的到了"的守卫条件,后者是"逐条消费数据"的业务逻辑。空记录时提前退出,避免后续循环体里对不存在的记录做类型强转而抛异常。

3. 类型描述体系在分享场景的复用

UDMF 的类型体系不止用于拖拽。在 entry/src/main/ets/model/KnockShareModel.ets 中,碰一碰分享页面链接时,同样用 UniformDataType 声明数据类型:

let shareData: systemShare.SharedData = new systemShare.SharedData({
  // Set the shared data type to Link.
  utd: uniformTypeDescriptor.UniformDataType.HYPERLINK,
  title: title,
  description: this.pageParamsData?.title,
  content: `${CommonConstants.SHARE_URL_SCHEME}://www.example.com?pageUrl=${this.pageUrl}&pageParams=${encodedParams}&sharingMechanism=knockShare`,
});

在 PC 碰一碰接收文件(dataReceiveListeningPC)中,能力声明同样使用 uniformTypeDescriptor.UniformDataType.MEDIAFILE 来注册"我能接收什么类型的数据"。这说明 UDMF 的类型描述是整个跨设备互通体系的通用语言:拖拽、分享、碰一碰接收,全部围绕 UniformDataType 展开。

三、小结

本文围绕 UDMF 统一数据管理框架,梳理了以下几点:

  1. UDMF 是一套数据描述标准UnifiedData 装载多条 UnifiedRecord,每条记录通过 uniformTypeDescriptor.UniformDataType 标识类型,SystemDefinedPixelMapImagePlainText 是项目用到的具体记录类。
  2. UDMF 是拖拽、剪贴板、分享的公共底座:本项目拖拽图片/文字、分享链接、碰一碰接收文件,全部基于这套类型体系,只是外层 API 不同。
  3. 读取 UDMF 数据的标准姿势event.getData() 取 UnifiedData → getRecords() 取记录 → getType() 判断类型 → 强转对应记录类读取字段;配合 1500ms 延迟重试应对跨设备传输的异步时序。

理解了 UDMF,后续几篇文章(跨设备拖拽图片、跨设备拖拽文字、系统剪贴板、PasteButton 安全粘贴、PC 碰一碰接收文件)读起来就会轻松很多——因为它们都在同一个"数据语言"体系里工作。

Logo

一站式 AI 云服务平台

更多推荐