公司动态
HarmonyOS社交通讯应用开发 26 :分布式文件系统了解
分布式文件系统了解引言前几篇讲的数据——标题、正文、位置开关——都是几 KB 的文本塞进分布式数据对象毫无压力。但本工程编辑的还有图片和视频一张照片几 MB、一段视频几十 MB如果把它们以二进制塞进分布式数据对象既违反可序列化的约束也会拖垮同步性能。HarmonyOS 为此提供了第二条跨设备通道——分布式文件系统把文件写入一个特殊的目录接续时文件会随任务自动同步到目标设备。本工程里设备 A 编辑时把图片/视频写入context.distributedFilesDir分布式文件目录接续后设备 B 就能在自己的distributedFilesDir里读到同一批文件。本文讲解分布式文件系统的概念并结合entry/src/main/ets/utils/FileUtil.ets中的writeDistributedFile逐行分析图片写 Buffer、视频拷文件两种写入方式。一、什么是分布式文件系统分布式文件系统是 HarmonyOS 分布式文件管理提供的能力应用写入分布式文件目录的文件会被纳入分布式文件体系在满足条件时设备组网、任务接续等自动同步到其他设备上的同一应用沙箱中。它和普通应用沙箱目录的对比目录获取方式是否跨设备同步应用私有目录context.filesDir否仅本机分布式文件目录context.distributedFilesDir是随接续/组网同步对开发者来说读写分布式文件的 API 与普通文件几乎一致——fileIo.openSync、fileIo.writeSync、fileIo.copyFileSync等照常使用唯一的区别是路径前缀文件放在distributedFilesDir下同步的事交给系统。这就是分布式文件系统最大的价值用本地文件 API 的体验拿到跨设备文件的能力。深入一层看工作机制。分布式文件系统底层依赖 HarmonyOS 的分布式软总线与分布式文件服务应用打开distributedFilesDir下的文件时框架会为每个文件维护本地副本 同步状态。当设备间满足组网条件同账号、可直连或经中转互通时文件变更会按需同步在接续场景下系统还会在save数据对象的同时把与该任务关联的分布式文件主动推送到目标设备。对应用而言这一切都是透明的——你只看到我在 A 设备写文件在 B 设备能读到。一个容易误解的点是同步的时机与粒度分布式文件同步是最终一致的文件可能晚于分布式数据对象到达也可能因为文件较大而分批到达。因此接收端在restored后还原附件时应该对文件还没同步完的情况有所预期本工程fileCopy用accessSync检查文件是否存在再读取就是对这种不确定性的防御详见第 28 篇。还需要澄清分布式文件的隔离边界跨设备同步的是同一应用沙箱内的分布式目录而不是设备的整个文件系统。设备 A 上 ContinuePublish 写进自己distributedFilesDir的文件只会同步到设备 B 上 ContinuePublish 自己的distributedFilesDir——不同应用之间、应用与系统之间都不会串。这种应用级沙箱 目录级同步的设计让跨设备文件能力既有分布式之便又不失隔离之安。二、writeDistributedFile两种写入策略写入逻辑位于entry/src/main/ets/utils/FileUtil.etsexportfunctionwriteDistributedFile(context: common.UIAbilityContext, displayName:string, mediaType: MediaType, buf?:ArrayBuffer, uri?:string):void{// 1. 取分布式文件目录拼出完整文件路径letdistributedDir:string context.distributedFilesDir;letfileName:string/ displayName;letfilePath:string distributedDir fileName;letfile: fileIo.File|undefinedundefined;letsrcFile: fileIo.File|undefinedundefined;try{// 2. 以读写 创建模式打开文件不存在则创建file fileIo.openSync(filePath, fileIo.OpenMode.READ_WRITE| fileIo.OpenMode.CREATE); hilog.info(DOMAIN,TAG,FORMAT,Create file success.);if(mediaType MediaType.MEDIA_IMAGE buf) {// 3a. 图片把内存中的 Buffer 直接写入文件fileIo.writeSync(file.fd, buf); }elseif(mediaType MediaType.MEDIA_VIDEO uri) {// 3b. 视频打开源文件uri用 copyFileSync 拷贝内容srcFile fileIo.openSync(uri, fileIo.OpenMode.READ_ONLY); fileIo.copyFileSync(srcFile.fd, file.fd); } }catch(error) {leterr:BusinessError errorasBusinessError; hilog.info(DOMAIN,TAG,FORMAT,Failed to openSync / writeSync / closeSync. Code:${err.code}, message:${err.message}); }finally{// 4. 无论成功失败都关闭文件描述符if(file) { fileIo.closeSync(file.fd); }if(srcFile) { fileIo.closeSync(srcFile.fd); } } }函数的参数设计很有讲究displayName是写入的文件名mediaType决定走哪条分支而数据来源用可选参数二选一——图片传buf内存 Buffer视频传uri源文件路径。下面拆解每一步。1. 路径拼接distributedFilesDir 文件名letdistributedDir:stringcontext.distributedFilesDir;letfileName:string/ displayName;letfilePath:string distributedDir fileName;context.distributedFilesDir返回本应用分布式文件目录的绝对路径末尾不带斜杠所以文件名前手动补一个/。文件名displayName由调用方传入——在本工程中图片通常是原图名去扩展名或util.generateRandomUUID()生成的 UUID视频则统一用 UUID详见第 27 篇的文件名规则。文件名的唯一性很关键因为设备 B 还原时要靠同样的名字在distributedFilesDir里找文件第 28 篇的fileCopy正是按attachment.name里的名字拼路径的。2. 打开文件OpenMode 位或file fileIo.openSync(filePath,fileIo.OpenMode.READ_WRITE |fileIo.OpenMode.CREATE);fileIo.openSync(path, mode)打开或创建文件返回fileIo.File持有fd文件描述符。模式是位标志用|组合OpenMode.READ_WRITE以读写方式打开OpenMode.CREATE文件不存在时自动创建。两者组合表示没有就建建了可读写。fileIo.openSync是同步 API在写入这种确定性的小操作里使用同步语义代码更直白也符合本函数写文件的定位。3a. 图片分支Buffer 直写if(mediaTypeMediaType.MEDIA_IMAGEbuf) { fileIo.writeSync(file.fd,buf); }图片的数据来源是内存中的 ArrayBuffer。这个 Buffer 从哪来看AddMedia.ets的PixelMapToBufferPixelMapToBuffer(pixelMap:image.PixelMap,displayName:string): void { const imagePackerApi: image.ImagePacker image.createImagePacker();letpackOpts: image.PackingOption { format: image/jpeg, quality:100}; imagePackerApi.packToData(pixelMap,packOpts).then((data: ArrayBuffer) { writeDistributedFile(this.context,displayName, MediaType.MEDIA_IMAGE,data); }) }用户选图后拿到的是PixelMap位图对象要落盘得先编码成文件格式image.createImagePacker()创建打包器packToData(pixelMap, { format: image/jpeg, quality: 100 })把 PixelMap 编码为 JPEG 格式的ArrayBuffer然后交给writeDistributedFile用writeSync(fd, buf)一次写入。writeSync把整个 Buffer 的内容写入文件描述符对应文件返回写入字节数。3b. 视频分支文件拷贝}elseif(mediaTypeMediaType.MEDIA_VIDEOuri) { srcFile fileIo.openSync(uri,fileIo.OpenMode.READ_ONLY); fileIo.copyFileSync(srcFile.fd,file.fd); }视频的场景不同视频通常很大而且本工程拿到的就是一条uri如粘贴板里的视频 uri、跨端投递的视频 uri文件已存在于某处没必要读进内存再写出来直接文件到文件拷贝更高效。做法是用OpenMode.READ_ONLY打开源 uri 得到srcFile再fileIo.copyFileSync(srcFile.fd, file.fd)把源文件内容整体复制进分布式文件目录的目标文件。视频分支的典型调用点同样在AddMedia.ets比如粘贴视频时letuuid util.generateRandomUUID(); writeDistributedFile(this.context,uuid, MediaType.MEDIA_VIDEO,undefined,data.getPrimaryUri());注意第 4 个参数传了undefined没有 buf第 5 个参数传了视频 uri——正好对应writeDistributedFile的可选参数设计。另外doInsertMedia跨端拖拽/投递视频也是同样的调用形态。4. finally务必关闭文件finally {if(file) { fileIo.closeSync(file.fd); }if(srcFile) { fileIo.closeSync(srcFile.fd); } }无论写入成功还是抛异常都要closeSync释放文件描述符。文件描述符是稀缺资源不关闭会导致泄漏长时间运行后可能打不开新文件。finally块保证必关目标文件file一定关源文件srcFile只在视频分支被赋值过判空后关。三、fileIo 文件操作 API 速览writeDistributedFile里已经出现了fileIo的多个 API本工程全量用到的还有几个一并梳理API作用在本工程的位置fileIo.openSync(path, mode)打开/创建文件返回带fd的 File写分布式文件、读源文件、读本地文件fileIo.writeSync(fd, buf)把 Buffer 写入文件图片写分布式目录、还原时写回 filesDirfileIo.readSync(fd, buf, opts)从文件读取一段到 BufferfileCopy还原读取fileIo.copyFileSync(srcFd, destFd)文件描述符级整文件拷贝视频写分布式目录fileIo.closeSync(fd)关闭文件描述符所有文件操作后的 finallyfileIo.statSync(path)取文件状态大小、时间getAssetInfo构造 AssetfileIo.accessSync(path)检查文件是否存在fileCopy还原前守卫注意到这些 API 大多是Sync 后缀的同步版本。选择同步 API 的好处是代码线性、易于阅读和调试代价是同步操作会阻塞当前线程不适合超大文件或高频调用。本工程中图片是缩略图级数据packToData的quality: 100仍属可控体积、视频走文件拷贝内核实现不经过 JS 内存因此同步版本足够。如果你要同步几十 MB 以上的文件且对 UI 流畅度敏感可以考虑write、read等异步版本。另外注意OpenMode的常用取值READ_ONLY只读、READ_WRITE读写、CREATE不存在则创建、TRUNC清空后写入。本工程打开既有文件读用READ_ONLY新建文件写用READ_WRITE | CREATE语义与动作一一对应。openSync后务必成对closeSync这是文件操作的第一纪律。四、写入之后文件如何跟着任务走文件写进distributedFilesDir后同步发生在什么时候结合第 23 篇的链路理解发送端onContinue里save(targetDevice)分布式数据对象的同时系统会把该应用分布式文件目录中的相关文件随任务一并同步到目标设备。设备 B 上应用在自己的distributedFilesDir里就能以相同路径访问到这些文件——对应用代码而言设备 A 写的文件和设备 B 读的文件是同一路径下的同一批文件这就是分布式文件系统的同步语义。接收端拿到的是文件的描述信息commonType.Asset含名字、大小、uri、路径还原时按名字去distributedFilesDir里openSync读取——第 28 篇fileCopy的完整读取逻辑会展示这一过程。因此发送端与接收端之间文件名是唯一的约定发送端怎么写的名字接收端就怎么找。四、文件名跨设备的唯一约定既然接收端按名字找文件文件名就成了两端协作的协议本工程对命名有明确的规则图片优先用原图名去扩展名如photo001从相册选图时取自asset.displayName从拖拽、粘贴、跨端投递来的图则用util.generateRandomUUID()生成的 UUID 命名。视频统一用 UUID 命名例如粘贴视频时writeDistributedFile(this.context, uuid, MediaType.MEDIA_VIDEO, undefined, uri)。为什么视频一律 UUID因为视频的原始 uri 往往指向图库或临时文件文件名不稳定且可能含非法字符UUID 保证全局唯一、无冲突、跨设备可复现。为什么图片保留原图名为了让用户在多端看到一致的文件语义调试日志也更可读。两种策略的共同点是文件名中不含路径分隔符、不含扩展名歧义并在写入分布式目录时以裸文件名形式出现方便接收端按name直接拼路径。还有一点文件名与 Asset.name 是两个概念。分布式目录里存的文件叫photo001无扩展名而getAssetInfo生成的attachment.name是image_photo001类型前缀 文件名。接收端fileCopy用indexOf(_)把两者拆开——这是第 27、28 篇反复出现的约定也是发送端MediaType枚举值取字符串image/video的原因。关于生命周期distributedFilesDir里的文件随应用沙箱存在接续完成后不会被自动删除本工程不清理旧文件示例简化生产环境可考虑按会话或时间清理避免分布式目录无限膨胀。注意不要往distributedFilesDir里写临时性数据——凡写入此目录的文件都会进入同步体系占用跨设备带宽。五、设计观察为什么图片走 Buffer、视频走拷贝同是一个writeDistributedFile两种媒体却用了两套策略这是对数据形态的务实选择图片业务侧拿到的是 PixelMap内存对象天然需要先packToData编码成 Buffer再writeSync落盘。图片体积小Buffer 方案简单直接。视频业务侧拿到的是 uri文件已存在再读成 Buffer 属于多此一举直接copyFileSync走内核拷贝又快又省内存。两个分支共享相同的打开 → 写入/拷贝 → 关闭骨架用mediaType分流代码整洁且扩展性好——如果未来要支持其他类型加一个else if分支即可。六、实践建议与注意事项结合本项目给初学分布式文件操作的读者几条可落地的建议写前先想清楚要不要同步。distributedFilesDir里的所有文件都会进入跨设备同步体系占用带宽与存储。临时缓存、日志、崩溃转储等不该放这里只有用户任务相关的、需要随接续带走的数据才放。本工程只把媒体附件放进分布式目录其余文件全部留在filesDir边界清晰。文件名与内容的匹配要靠约定更要靠校验。接收端从attachment.name拆出文件名后直接拼路径访问如果发送端命名不规范如文件名里混入路径分隔符接收端拼出的路径就可能越界。防御性做法是写入前对displayName做合法性校验拒绝/、\等字符接收端再对解析出的mediaName做一次白名单检查。示例为简洁未做但这是生产级工程必须补的功课。同步 API 与异步 API 的选择要有依据。本工程选同步 APIwriteSync/readSync/openSync是因为数据量可控、逻辑线性。如果你的场景要处理几十 MB 的原图或长视频同步读写会明显卡顿应改用fileIo.write/fileIo.read异步版本或在子线程执行。善用OpenMode组合表达意图。READ_WRITE | CREATE表示可读写、可新建READ_ONLY表示只读既有文件。打开模式与后续操作严格对应既能让代码自解释也能让框架在错误用法时尽早报错。小结分布式文件系统是接续的附件通道心智模型是路径即约定context.distributedFilesDir指向的目录跨设备同步发送端写入的文件接收端按同名路径读回。本工程的writeDistributedFile展示了两种写入姿势——图片把 PixelMap 编码成 Buffer 后writeSync直写视频用copyFileSync做文件级拷贝配套OpenMode.READ_WRITE | CREATE保证可写可建finally里closeSync保证不泄漏。写文件只是前半程后半程——接收端如何从distributedFilesDir把这些文件读出来、重建图片和视频——留待第 28 篇《接续后媒体还原》详细展开。而在那之前第 27 篇先回答一个问题文件写好了怎么把它们连同正文一起打包进分布式数据对象