分布式文件系统(Distributed File System)是鸿蒙跨端协同的杀手锏。手机和平板组网之后,手机拍的高清视频,平板上的应用可以直接读取。听起来很爽对吧?

我曾经做过一个跨设备传图编辑的功能。用手机选了一张 50MB 的原图,发送给平板端的应用。平板这边弹出一个进度条,结果进度条直接卡死在 99%,等了一分钟都没动静,最后报了个超时。用户以为死机了,直接把 App 划掉了。

当我去查分布式软总线(DSoftBus)的日志时,发现文件其实早就传完了,卡住的是我自己的代码。这就引出了一个大坑:如何在分布式环境中正确地做流式文件处理和进度反馈。

跨设备文件的 URI 与读取方式

在鸿蒙里,跨设备的文件通常通过统一的 URI 来访问。比如 file://com.example.app/data/storage/el2/distributedfiles/dir/test.jpg。

千万不要直接用同步方法(如 fs.readSync)去读跨设备的文件。因为跨设备通信受网络影响极大,一次同步读取几十兆,主线程会直接冻结,用户看到的就是应用无响应(ANR)。

必须使用流式读写(Stream)。在鸿蒙里,用 @ohos.file.fs 模块的 createStream 接口最稳妥。

typescript

import { fileIo as fs } from '@kit.CoreFileKit';

async function copyDistributedFile(srcUri: string, destUri: string) {
  // 以流的方式打开源文件和目标文件
  const srcFile = fs.openSync(srcUri, fs.OpenMode.READ_ONLY);
  const destFile = fs.openSync(destUri, fs.OpenMode.CREATE | fs.OpenMode.READ_WRITE);

  // 创建流对象
  const srcStream = fs.fdopenStreamSync(srcFile.fd, 'r');
  const destStream = fs.fdopenStreamSync(destFile.fd, 'w');

  // ... 后续分块读取
}

关键语句是 fdopenStreamSync。它把一个文件描述符(fd)包装成了一个流对象。在分布式环境中,底层软总线会接管数据的分块传输,你只需要像操作本地文件流一样调用它的 read 和 write。

分块读写与进度回调

为什么进度条会卡死?因为很多人在循环里拷贝文件,不知道拷贝到哪了,或者写错了循环退出的条件。如果读取到了文件末尾,返回的 buf.length 是 0,这时候必须 break,否则一旦陷入死循环,进程就被卡住了。

我们需要在循环里计算进度。

typescript

async function streamCopyWithProgress(srcUri: string, destUri: string, onProgress: (progress: number) => void) {
  const srcFile = fs.openSync(srcUri, fs.OpenMode.READ_ONLY);
  // 获取文件总大小
  const stat = fs.statSync(srcUri);
  const totalSize = stat.size;
  
  const destFile = fs.openSync(destUri, fs.OpenMode.CREATE | fs.OpenMode.READ_WRITE);
  const buf = new ArrayBuffer(1024 * 1024); // 每次读 1MB,不要太大也不要太小
  
  let offset = 0;
  
  try {
    while (true) {
      // 分块读取
      const readLen = fs.readSync(srcFile.fd, buf, { offset: offset });
      
      // 读到 0 说明文件结束了,必须退出循环
      if (readLen === 0) {
        break;
      }
      
      // 将读到的数据写入目标文件
      fs.writeSync(destFile.fd, buf, { offset: offset, length: readLen });
      
      offset += readLen;
      // 回调计算进度
      const progress = Math.floor((offset / totalSize) * 100);
      onProgress(progress);
      
      // 让出微任务队列,防止卡死主线程
      await new Promise(resolve => setTimeout(resolve, 0)); 
    }
  } finally {
    // 无论成功失败,必须关闭流和文件描述符
    fs.closeSync(srcFile.fd);
    fs.closeSync(destFile.fd);
  }
}

重点看 if (readLen === 0) 这行。这是解决卡在 99% 的根本原因:如果不判断这个条件,当文件读完时,fs.readSync 可能一直返回 0,你的 while (true) 就变成了死循环。

另外,await new Promise(resolve => setTimeout(resolve, 0)) 极其重要。如果在一个循环里疯狂读写,即使你的单次操作很小,也会阻塞 UI 线程的更新。加个零秒延时,等于告诉系统:“我这段繁重计算先让一让,你去把那一帧画面渲染出来,然后再接着跑。”

断点续传与异常处理

跨设备传输极其不稳定,用户可能拿着平板走进电梯,或者手机突然断开 WiFi。这时候流会瞬间抛错。

如果你在读的时候抛了异常,不要直接崩溃,也不要从头开始传。记录下你上次成功写入的 offset,存入 Preferences。

下次应用拉起时,先检查 Preferences 里有没有未完成的传输任务。如果有,打开源文件和目标文件,直接把 offset 传进 fs.readSync 和 fs.writeSync 的配置项里,断点续传就实现了。

分布式文件处理,本质上是在极端不稳定的网络环境下做本地 IO。不要相信网络永远在线,用流分块处理,管好你的退出条件,做好异常兜底,跨设备文件的大坑就能安全填平。

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐