《HarmonyOS技术精讲-Core File Kit》第20篇:实战项目——多端文件同步应用(高级篇)

📅 2026/7/11 5:58:04
《HarmonyOS技术精讲-Core File Kit》第20篇:实战项目——多端文件同步应用(高级篇)
分布式文件同步不止是复制粘贴HarmonyOS NEXT 的分布式能力在文件系统层面确实给了开发者一把利器分布式文件标记。官方文档里说得很清楚通过distributed标记文件会自动在设备间同步。但实际做多端文件同步应用时你会发现真正的难点不是“同步文件”而是“什么时候同步”、“冲突了怎么办”、“用户怎么感知”。很多人第一次用这个能力会写成这样监听本地文件变化变化后直接调用fs.copyFile到分布式目录。但这样会遇到几个典型问题分布式文件标记不生效文件虽然拷贝过去了但远端设备看不到更新。watch 回调不触发设备 A 改了文件设备 B 的监听器没反应。冲突时直接覆盖两端同时编辑只有最后写入的生效用户没有任何感知。这篇文章就围绕这三个真实问题拆解一个手机与平板间的文件同步应用。核心逻辑不复杂但每个环节都有一些设计细节需要处理。它解决什么问题问题简单粗暴的方案本方案文件内容同步手动fs.copyFile到分布式路径应用层标记 自动同步编辑冲突最后写入覆盖记录冲突文件 提示用户选择目录结构同步无监听子目录递归同步适用场景笔记应用、文档编辑、配置文件同步等需要多端协同的场景。不适合大型二进制文件实时同步、超高频目录写入场景。环境说明DevEco Studio 版本DevEco Studio 6.1.0 及以上 HarmonyOS SDK 版本HarmonyOS 6.1.0(23) 及以上 目标设备[手机 / 平板]核心实现三步构建同步应用第一步标记分布式目录不是所有目录都会自动同步。只有通过fileIo.setLabel标记过的文件或目录才会被系统纳入分布式管理。// model/SyncDirManager.etsimport{fileIo}fromkit.CoreFileKit;import{common}fromkit.AbilityKit;exportclassSyncDirManager{privatecontext:common.UIAbilityContext;constructor(context:common.UIAbilityContext){this.contextcontext;}asyncsetupSyncDir(dirName:string):Promisestring{// 获取应用私有沙箱路径constbaseDirthis.context.filesDir;constsyncDirPath${baseDir}/${dirName};// 创建目录awaitfileIo.mkdir(syncDirPath,true);// 重点标记为分布式目录// 第二个参数必须为 DISTRIBUTEDawaitfileIo.setLabel(syncDirPath,DISTRIBUTED);console.info(SyncDirManager: 目录已标记为分布式:${syncDirPath});returnsyncDirPath;}}注意事项setLabel必须在目录创建之后调用。标记后的目录其下的子文件和子目录也会自动继承分布式属性。不要对已存在的非分布式目录直接标记部分设备上会报错API 12 已验证。第二步监听本地文件变化这里用fileIo.createWatcher来监听目录变化。关键点Watcher 必须与页面生命周期绑定否则页面销毁后监听会丢失。// viewmodel/SyncViewModel.etsimport{fileIo}fromkit.CoreFileKit;import{emitter}fromkit.BasicServicesKit;exportclassSyncViewModel{privatewatcher:fileIo.Watcher|nullnull;privatesyncDirPath:string;constructor(syncDirPath:string){this.syncDirPathsyncDirPath;}startWatching(){if(this.watcher){// 避免重复创建this.watcher.stop();}// 创建 Watcher监听修改事件this.watcherfileIo.createWatcher(this.syncDirPath,{recursive:true,// 监听子目录events:[change]});// 注册回调this.watcher.on(change,(event){// event.filename: 变更的文件相对路径// event.eventType: changeconsole.info(SyncViewModel: 文件变更:${event.filename});// 通过 emitter 通知 UI 刷新emitter.emit({eventId:fileChanged,priority:emitter.EventPriority.IMMEDIATE},{data:{filename:event.filename,timestamp:Date.now()}});});}stopWatching(){if(this.watcher){this.watcher.stop();this.watchernull;}}}为什么用 emitter 而不是直接回调Watcher 的回调运行在独立线程直接修改 UI 状态会导致State刷新异常。使用emitter将事件抛回主线程再由主线程更新 UI。第三步冲突检测与提示冲突发生在设备 A 和 B 同时对同一个文件进行了修改。我们的策略是最后写入优先但写入前会给用户一个通知让用户决定是否合并。// model/ConflictResolver.etsimport{fileIo}fromkit.CoreFileKit;import{promptAction}fromkit.ArkUI;exportclassConflictResolver{// 记录每个文件的最后修改时间戳privatelastModifiedMap:Mapstring,numbernewMap();privatesyncDirPath:string;constructor(syncDirPath:string){this.syncDirPathsyncDirPath;}asynconFileChanged(filename:string){constfilePath${this.syncDirPath}/${filename};// 1. 获取当前文件的修改时间conststatawaitfileIo.stat(filePath);constcurrentMtimestat.mtime;// 2. 检查是否有冲突constlastMtimethis.lastModifiedMap.get(filename);if(lastMtime(currentMtime-lastMtime)1000){// 如果在 1 秒内连续修改认为可能冲突// 实际开发中可以改成对比两端修改时间差this.showConflictDialog(filename,filePath);}// 3. 更新最后修改时间this.lastModifiedMap.set(filename,currentMtime);}privateshowConflictDialog(filename:string,filePath:string){// 实际应用中这里应该弹出对话框让用户选择// 这里使用 promptAction 简单提示promptAction.showToast({message:文件${filename}可能存在编辑冲突已使用最新版本,duration:3000});}}冲突处理策略说明这里采用的是简化版检测到 1 秒内两次修改就提示用户。更完善的方案远端设备写入时在文件名后加上设备 ID 和版本号通过对比版本号来决定是否冲突。最后写入优先在提示后始终以当前设备的修改为准。如果有专门的文件合并需求需要额外的业务逻辑。踩坑记录坑1Watcher 回调不触发现象在设备 A 上修改了文件设备 B 的 Watcher 没有收到change事件。原因Watcher 监听的是底层文件系统的inotify类 Linux 机制而分布式文件的同步是上层应用层完成的。系统从远端同步文件到本地后文件的属性如 mtime会更新但 inotify 事件可能不会触发。解决方案不要依赖 Watcher 来做全量同步通知。改为本地修改后主动向对端设备发送一条广播或通知通过kit.DistributedNotificationKit或featureAbility的跨设备调用。对端收到通知后手动调用fileIo.access检查文件是否存在并读取最新内容。坑2setLabel后文件不自动同步现象对目录标记DISTRIBUTED后把文件复制进去其他设备看不到。原因分布式同步不是实时的。系统有同步窗口期大约 30 秒到 2 分钟且部分低端设备、平板端同步延迟较大。解决方案文件写入后调用fileIo.sync强制刷盘写缓存到磁盘但无法强制分发到其他设备。最佳做法在文件写入后等待 3-5 秒再检查对端文件是否存在。如果是关键同步建议用分布式数据库记录同步状态由应用层发起同步确认。最佳实践Watcher 生命周期绑定到页面页面onPageShow时创建onPageHide时销毁。不要尝试全局单例持有 Watcher页面销毁后回调会崩溃。不要重复修改分布式目录的权限同一个目录只能调用一次setLabel多次调用会导致BUSY错误。冲突处理不能只依赖时间戳设备间时钟可能存在几秒的偏差。建议在写入文件头或元数据中记录设备 ID 自增版本号通过版本号决定谁的修改是最近的。Demo 入口// pages/Index.etsimport{SyncDirManager}from../model/SyncDirManager;import{SyncViewModel}from../viewmodel/SyncViewModel;import{ConflictResolver}from../model/ConflictResolver;EntryComponentstruct Index{Statemessage:string等待初始化...;privatesyncDirPath:string;privatesyncVM:SyncViewModel|nullnull;privateconflictResolver:ConflictResolver|nullnull;aboutToAppear(){this.initSync();}aboutToDisappear(){this.syncVM?.stopWatching();}asyncinitSync(){constcontextgetContext(this)ascommon.UIAbilityContext;constmanagernewSyncDirManager(context);this.syncDirPathawaitmanager.setupSyncDir(mySyncDir);this.conflictResolvernewConflictResolver(this.syncDirPath);this.syncVMnewSyncViewModel(this.syncDirPath);this.syncVM.startWatching();this.message同步目录初始化成功:${this.syncDirPath};}build(){Column(){Text(this.message).fontSize(16).padding(10)}.width(100%).height(100%)}}FAQQ为什么真机同步成功但模拟器上文件在远端看不到A模拟器不支持真实的分布式文件同步。setLabel在模拟器上不会报错但文件不会真正同步到其他设备。必须用真机测试。Q页面返回后 Watcher 失效再次打开页面需要重新创建吗A需要。页面销毁时 Watcher 会被系统回收重新进入时务必调用startWatching()。推荐在aboutToAppear中重新初始化。Q同步目录可以跨应用访问吗A不行。setLabel标记的分布式文件只在同一应用的不同设备实例间同步。不同应用之间的文件隔离依然存在。示例代码地址项目地址