跨设备传大文件,进度条卡住不动不是网速慢
分布式文件系统(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。不要相信网络永远在线,用流分块处理,管好你的退出条件,做好异常兜底,跨设备文件的大坑就能安全填平。
更多推荐
所有评论(0)