公司动态

HarmonyOS社交通讯应用开发 28:分布式文件系统中媒体数据读取显示

📅 2026/8/25 6:07:39
HarmonyOS社交通讯应用开发 28:分布式文件系统中媒体数据读取显示
引言接续的最后一公里是媒体还原设备 B 收到了attachmentsAsset 描述数组也通过分布式文件系统在本地distributedFilesDir拿到了文件实体接下来要做的是——把文件读出来图片重建回PixelMap让九宫格能显示视频拿到可播放的 uri并把这些文件从分布式目录拷贝到应用私有目录留作本地使用。这段逻辑集中在entry/src/main/ets/utils/FileUtil.ets的fileCopy函数里由第 25 篇讲过的restored回调逐附件调用。还原流程的关键点有三个能力检查canIUse、从 name 解析类型与文件名、图片/视频分叉重建。逐个展开。先交代还原发生的时机与次序这决定了fileCopy内部为什么这样写。回顾第 25 篇接收端在status restored时才取回attachments并逐附件调用fileCopy。也就是说媒体还原严格排在描述数据可用之后。但注意描述可用 ≠ 文件可用——attachments是随分布式数据对象同步的快文件实体是随分布式文件系统同步的慢两者存在时间差。因此fileCopy内部第一步就是accessSync探测文件是否已在本地分布式目录就绪没就绪就跳过本次还原不报错、不阻塞其他附件。这是接续还原中最容易被忽略、却最体现工程经验的一个细节。一、能力检查canIUse 守卫fileCopy的第一行是能力判断exportfunctionfileCopy(context: common.UIAbilityContext, attachment: commonType.Asset, mediaUriArray:ArrayMediaInfo):void{if(canIUse(SystemCapability.DistributedDataManager.CommonType)) {// ... 完整还原逻辑} }canIUse(SystemCapability.DistributedDataManager.CommonType)用于检测当前设备是否支持分布式数据管理的CommonType能力即commonType.Asset等类型是否可用。这是一个运行时能力探测不同设备/系统版本的 API 集合可能不同直接在能力判断后使用相关类型可以避免在低版本设备上因 API 不存在而崩溃。虽然本项目 API 23 必然支持但保留这个判断是面向多设备、多版本的稳健写法——与第 24 篇提到的设备兼容性一脉相承。二、从 name 解析类型与文件名还原的第一步是解析 Asset 的name。回顾第 27 篇发送端getAssetInfo构造的name形如image_uuid/video_uuid——前半段是类型后半段是文件名。接收端用字符串 API 拆开letmediaName attachment.name.substring(attachment.name.indexOf(_) 1);letmediaType attachment.name.substring(0, attachment.name.indexOf(_));indexOf(_)找到第一个下划线的位置substring(0, indexOf(_))取类型image或video正好对应MediaType枚举的字符串值substring(indexOf(_) 1)取文件名去扩展名如 UUID 或图片名。这两行是整个媒体还原的解包动作发送端拼好的name接收端原样拆回两个信息然后拼出分布式文件路径与本地保存路径letfilePath:string context.distributedFilesDir / mediaName;// 分布式目录中的源文件letsavePath:string context.filesDir / mediaName;// 应用私有目录中的目标文件filePath是设备 B 自己的distributedFilesDir下、与发送端同名的文件分布式文件系统已同步过来见第 26 篇savePath是应用私有目录——还原的同时把文件落地到本地沙箱之后即使分布式文件失效本地副本依然可用。三、读取源文件重建图片 PixelMap主体逻辑是打开源文件 → 读入 Buffer → 按类型重建 → 写回本地letfile: fileIo.File| undefined undefined;letsaveFile: fileIo.File| undefined undefined;letimageSourceApi: image.ImageSource| undefined;try{if(fileIo.accessSync(filePath)) {//源文件存在才继续//1. 打开本地目标文件可写可建与分布式源文件只读 saveFile fileIo.openSync(savePath,fileIo.OpenMode.READ_WRITE |fileIo.OpenMode.CREATE); file fileIo.openSync(filePath,fileIo.OpenMode.READ_WRITE);//2. 按Asset.size 分配Buffer读取第一段数据letbuf:ArrayBuffernewArrayBuffer(Number(attachment.size));letreadSize 0;letreadLen fileIo.readSync(file.fd,buf, {offset:readSize});if(mediaTypeMediaType.MEDIA_IMAGE) {//3a. 图片从Buffer创建ImageSource并同步生成PixelMapletsourceOptions: image.SourceOptions { sourceDensity: 120 }; imageSourceApi image.createImageSource(buf,sourceOptions); mediaUriArray.push({ imagePixelMap: imageSourceApi.createPixelMapSync(),//重建PixelMapmediaName: mediaName, mediaType: mediaType}); }elseif(mediaTypeMediaType.MEDIA_VIDEO) {//3b. 视频保留 uri直接放入媒体列表 mediaUriArray.push({ videoUri: attachment.uri,//分布式文件 uri可直接播放 mediaName: mediaName, mediaType: mediaType}); }//4. 循环读完剩余数据写入本地文件while(readLen 0) { readSize readLen; fileIo.writeSync(saveFile.fd,buf); readLen fileIo.readSync(file.fd,buf, {offset:readSize}); } hilog.info(DOMAIN,TAG,FORMAT,${attachment.name} synchronized successfully.); } } catch (error) {...} finally {//5. 关闭文件、释放ImageSourceif(file) { fileIo.closeSync(file.fd); }if(saveFile) { fileIo.closeSync(saveFile.fd); }if(imageSourceApi) { imageSourceApi.release(); imageSourceApi undefined; } }图片分支是重点image.createImageSource(buf, sourceOptions)从读到的 Buffer 创建ImageSourcesourceDensity: 120指定源密度随后createPixelMapSync()同步生成PixelMap。createPixelMapSync是同步版本——restored回调里逐附件还原用同步 API 保证代码顺序清晰、每个附件处理完立即 push 进mediaUriArray。最终 push 进媒体列表的MediaInfo与发送端的结构完全一致imagePixelMapmediaNamemediaTypeUI 的AddMedia九宫格直接复用同一套渲染逻辑if (item.imagePixelMap)显示Image收发两端共用一个 UI 模型——这就是MediaInfo设计的意义。把图片重建这步拆细一点有三个值得理解的点createImageSource(buf, sourceOptions)的两个参数第一个是图片数据的 BufferJPEG 编码第二个是SourceOptions这里只设置了sourceDensity: 120。sourceDensity表示图片的源像素密度120 是缩略图场景的常见取值与屏幕真实密度无关——它主要影响解码时像素尺寸的换算基准。对九宫格缩略图而言这个值足够。**createPixelMapSync()与createPixelMap()**前者同步返回PixelMap后者异步返回PromisePixelMap。还原是逐附件串行 立即入数组的流程同步版让代码平铺直叙若图片多且大可改用异步版配合Promise.all并发解码本例为了清晰选择了同步。imageSourceApi.release()的对称性ImageSource创建后持有解码资源用完必须release()。注意项目把imageSourceApi声明在 try 之外、finally 里释放保证无论解码成功还是中途异常资源都被回收——这是资源获取即初始化、块结束即释放的标准姿势。四、视频分支uri 直达视频分支非常简单mediaUriArray.push({videoUri:attachment.uri,// Asset 里携带的分布式文件 urimediaName:mediaName,mediaType:mediaType});视频不需要重建任何对象——attachment.uri是发送端getAssetInfo里用fileUri.getUriFromPath(filePath)生成的分布式文件 uri设备 B 拿到后可直接交给Video组件播放AddMedia.ets中Video({ src: item.videoUri, ... })。为什么图片要重建 PixelMap、视频却能直接用 uri因为 UI 上图片组件需要PixelMap或图片资源而Image组件虽支持 uri但项目选择统一用 PixelMap 渲染视频组件则天然支持 uri 播放无需转换。这一分叉体现了按消费端需求重建的原则还原到什么形态取决于界面组件要什么。五、读改写把分布式文件落地到本地无论图片还是视频文件本身都要从distributedFilesDir**拷贝到filesDir**。实现用的是读改写循环letbuf:ArrayBuffernewArrayBuffer(Number(attachment.size));letreadSize 0;letreadLen fileIo.readSync(file.fd, buf, {offset: readSize });// ... 类型处理 ...while(readLen 0) { readSize readLen; fileIo.writeSync(saveFile.fd, buf);// 把已读数据写入本地readLen fileIo.readSync(file.fd, buf, {offset: readSize });// 继续读下一段}为什么不用第 26 篇的copyFileSync一是演示两种拷贝手法二是这段代码在读取过程中已经需要 Buffer 供图片分支重建 PixelMap——既然 Buffer 已经在手顺手writeSync落盘即可避免二次 IO。readSync的offset语义是从文件的第 offset 字节开始读每次循环推进readSize直到readLen为 0文件读完。注意new ArrayBuffer(Number(attachment.size))按 Asset 记录的字节数分配缓冲正好容纳整个文件。把循环的执行过程走一遍会更清楚假设附件 1000 字节、单次readSync读满 1000 字节——第一次调用后readLen 1000进入循环readSize变 1000writeSync把整个 Buffer 写入本地文件再readSync发现已到文件尾返回 0循环退出。若文件大于 Bufferattachment.size比实际文件小或一次读不满循环会多次读一段、写一段直到读完。Buffer 复用的细节writeSync写入的是buf整体而readSync每次都覆盖buf的内容所以循环里始终是最新一段数据写盘不会写重复。唯一的假设是attachment.size与实际文件大小一致——这正是发送端statSync取size的用途。finally块收尾三件事关闭源文件与目标文件的fd、imageSourceApi.release()释放图片解码资源。release很关键——ImageSource持有底层解码内存不释放会造成内存泄漏尤其在媒体较多的场景。六、失败处理与容错fileCopy的容错设计值得单独说因为它决定了接续失败时应用会不会崩try {if(fileIo.accessSync(filePath)) { ... }// 文件未就绪 → 静默跳过} catch (error) { leterr: BusinessError errorasBusinessError; hilog.error(DOMAIN, TAG,FORMAT, ${attachment.name} fileCopy failed witherr:${JSON.stringify(err)}); } finally { ... }accessSync守卫文件还没同步到本地时直接跳过整个还原逻辑不抛异常。accessSync对不存在的路径返回 false或抛错正好充当文件是否就绪的探针。catch 只记日志任何一步出错打开失败、解码失败、写盘失败都记录带attachment.name的错误日志便于按附件定位问题但不会中断restored回调里对其他附件的还原——一个附件坏了其余照常。finally 兜底资源无论走哪条路径文件描述符与ImageSource都保证被释放避免失败路径上的资源泄漏。这种单附件隔离 静默降级的容错思路让整个媒体还原在部分失败时依然可用某个视频没同步过来九宫格里只是少一项而不是白屏或崩溃。七、完整链路回看把fileCopy放回第 25 篇的restored回调中看整体if(status restored) {// ... 六字段写入 AppStorage ...let attachments this.distributedObject[attachments]ascommonType.Assets;for(constattachment of attachments) { fileCopy(this.context, attachment,this.mediaUriArray);// 逐附件还原} AppStorage.setOrCreateArrayMediaInfo(mediaUriArray,this.mediaUriArray);// 一次性发布}每个附件走一遍fileCopy结果累积进this.mediaUriArray全部还原后一次性setOrCreate(mediaUriArray, ...)写进AppStorage——AddMedia的StorageLink(mediaUriArray)收到新数组九宫格一次性渲染出所有图片与视频。逐附件还原、整体发布的设计避免每还原一个就触发一次 UI 刷新。八、验证与常见问题如何确认媒体还原成功两个观察点一是hilog中每个附件都会打一行xxx synchronized successfully.附件数与attachments数组长度一致即说明全部还原二是九宫格 UI——图片显示缩略图、视频可播放且应用私有目录filesDir下出现同名文件可在 DevEco Studio 的设备文件管理器里查看。几个常见问题与排查方向图片不显示但视频正常优先怀疑图片解码——检查fileCopy日志是否有该附件的fileCopy failed记录以及发送端packToData时format: image/jpeg与接收端createImageSource是否匹配。附件数量对不上attachments是随分布式数据对象到达的mediaUriArray是逐附件还原的若restored回调里for循环执行次数少于attachments.length多半是某个附件accessSync探测失败被跳过——等文件同步完成后再次接续即可或加大重试。视频首次播放黑屏Video组件用attachment.uri播放若分布式文件尚未完全同步uri 指向的文件可能不完整。写回filesDir的本地副本正是规避手段之一——生产实现可以在本地副本就绪后用本地路径播放。fileCopy被整体跳过检查canIUse(SystemCapability.DistributedDataManager.CommonType)是否返回 false——模拟器或旧版本可能不支持该能力此时媒体还原静默降级为仅还原文本属预期行为。理解这些现象就真正吃透了描述与文件双通道、各自异步到达的还原模型。小结接续后媒体还原是发送端打包的镜像过程canIUse守卫能力可用性从attachment.name拆出类型image/video与文件名从distributedFilesDir读文件图片用createImageSource createPixelMapSync重建PixelMap进媒体列表视频则保留attachment.uri直达播放文件同时以读改写方式落地到filesDir留作本地副本最后整体发布到AppStorage驱动九宫格刷新。至此从设备 A 编辑、打包、同步到设备 B 还原、重建、渲染的完整接续闭环全部打通——六篇文章分别覆盖了原理、对象、还原、文件、模型、媒体六个侧面合起来正是 ContinuePublish 这个示例工程的全部接续技术骨架。