官方文档对 FileWatcher 和 Stream 的描述不够详细,如何实现文件监听与流式读写?
在开发文件管理、日志实时监控或大文件上传下载等功能时,文件监听(FileWatcher)和流式读写(Stream)是两个绕不开的底层能力。翻阅 HarmonyOS Core File Kit 的官方文档,发现其仅列出了入口页面和概述性导航,对于FileWatcher的构造函数、事件回调类型、监听范围,以及Stream接口的read、write、seek、分片读写等具体参数和用法,均没有给出完整说明。这种情况下,只能通过 SDK 源码分析和实际测试来验证 API 的正确用法。本文记录了我自己验证并封装后的实现方式,包含完整代码和注意事项。
一、FileWatcher:监听文件或目录变化
HarmonyOS 的fs.watch接口用于监听文件或目录的变更事件。当前版本(API 10+)支持以下事件:
change:文件内容或元数据被修改(包括重命名、删除、权限变化等)access:文件被访问close:文件被关闭(写模式关闭时可能触发)
1. 基本用法
importfsfrom'@ohos.file.fs';// 监听文件变化letwatcher=fs.watch('/data/storage/el2/base/haps/entry/files/test.txt');watcher.on('change',(eventType:string,filename:string)=>{console.info(`文件事件:${eventType}, 文件名:${filename}`);});// 开始监听(可选,默认创建后即开始)// watcher.start();注意事项:fs.watch返回的Watcher对象在创建后会自动开始监听,无需手动调用start()。但部分版本中start()用于恢复暂停的监听,暂停调用stop()后必须调用start()才能继续。
2. 监听目录及子目录变化
监听目录时,filename参数返回的是相对路径(相对于被监听目录)。需要注意recursive参数目前仅部分版本支持,API 10 默认递归监听子目录,但官方文档未明确。
letdirWatcher=fs.watch('/data/storage/el2/base/haps/entry/files',{recursive:true// 是否递归监听子目录,默认 false,但实测 true 也仅监听一层});dirWatcher.on('change',(eventType:string,filename:string)=>{// filename 为相对路径,例如 "config.json" 或 "subdir/data.txt"console.info(`目录事件:${eventType}, 文件:${filename}`);});// 5分钟后停止监听setTimeout(()=>{dirWatcher.stop();},5*60*1000);常见误区:recursive设为true后,仅能监听到直接子目录下的文件变化,无法递归到更深层级。如需多级监听,需手动遍历子目录并逐个创建Watcher。
3. 完整示例:实时监控日志文件并输出变化
importfsfrom'@ohos.file.fs';import{BusinessError}from'@ohos.base';letlogPath='/data/storage/el2/base/haps/entry/files/app.log';functionstartLogWatcher(){try{letwatcher=fs.watch(logPath);watcher.on('change',(eventType:string,filename:string)=>{if(eventType==='change'){// 文件被修改,读取新内容(仅演示,实际可用流式增量读取)fs.readText(logPath).then((content:string)=>{console.info(`日志更新:\n${content}`);}).catch((err:BusinessError)=>{console.error(`读取日志失败:${err.message}`);});}});console.info('日志监控已启动');// 保存 watcher 引用以便后续停止globalThis.logWatcher=watcher;}catch(error){console.error(`创建 watcher 失败:${(errorasBusinessError).message}`);}}// 停止监听functionstopLogWatcher(){if(globalThis.logWatcher){globalThis.logWatcher.stop();globalThis.logWatcher=null;console.info('日志监控已停止');}}二、Stream:大文件分片读写
对于超大文件(如视频、数据库文件),一次性读取到内存会导致 OOM,必须使用流式读写。fs.createStream创建文件流,通过read、write、seek等方法操作文件。
1. 创建读流并分片读取
importfsfrom'@ohos.file.fs';import{BusinessError}from'@ohos.base';asyncfunctionreadLargeFile(chunkSize:number=1024*1024){// 默认1MBletfilePath='/data/storage/el2/base/haps/entry/files/bigfile.bin';letstream:fs.Stream|null=null;try{stream=awaitfs.createStream(filePath,'r');// 以只读模式打开lettotalRead=0;letbuffer=newArrayBuffer(chunkSize);letbytesRead=awaitstream.read(buffer);while(bytesRead>0){// 处理当前分片数据letslice=buffer.slice(0,bytesRead);// 实际数据长度可能小于 chunkSize// doSomethingWithSlice(slice);totalRead+=bytesRead;console.info(`已读取:${totalRead}bytes`);// 继续读取下一块buffer=newArrayBuffer(chunkSize);bytesRead=awaitstream.read(buffer);}console.info(`文件读取完成,总大小:${totalRead}bytes`);}catch(error){console.error(`流式读取失败:${(errorasBusinessError).message}`);}finally{if(stream){stream.close().catch((err:BusinessError)=>{console.error(`关闭流失败:${err.message}`);});}}}注意事项:stream.read(buffer)返回实际读取的字节数,如果文件剩余不足buffer.byteLength,则返回剩余大小。最后一次读取返回 0 表示文件结束。务必在finally中关闭流,否则文件句柄泄漏可能导致后续操作失败。
2. 使用 seek 实现随机读写
在数据库或日志分段加载场景中,需要跳过已有内容。seek方法可以定位到指定位置。
asyncfunctionreadAtPosition(filePath:string,position:number,length:number):Promise<ArrayBuffer>{letstream:fs.Stream|null=null;try{stream=awaitfs.createStream(filePath,'r');// 定位到指定位置letseekResult=awaitstream.seek({position:position,whence:0});// whence: 0=SEEK_SETconsole.info(`seek 后当前位置:${seekResult}`);letbuffer=newArrayBuffer(length);letbytesRead=awaitstream.read(buffer);if(bytesRead<length){// 如果实际读取少于请求长度,截取有效部分returnbuffer.slice(0,bytesRead);}returnbuffer;}catch(error){console.error(`随机读取失败:${(errorasBusinessError).message}`);throwerror;}finally{if(stream){stream.close();}}}3. 流式写入:分段追加文件
大文件下载时,需要边下载边写入。createStream以追加模式打开,配合write方法实现增量写入。
asyncfunctionappendToLargeFile(filePath:string,dataChunks:ArrayBuffer[]){letstream:fs.Stream|null=null;try{// 以追加写模式打开,若文件不存在会创建stream=awaitfs.createStream(filePath,'a+');for(leti=0;i<dataChunks.length;i++){letchunk=dataChunks[i];letbytesWritten=awaitstream.write(chunk);console.info(`第${i+1}块写入${bytesWritten}bytes`);// 此处可添加进度回调}console.info('所有数据块写入完成');}catch(error){console.error(`流式写入失败:${(errorasBusinessError).message}`);}finally{if(stream){stream.close();}}}常见误区:a+模式会将文件指针移到末尾,所以后续write总是追加。如果希望覆写文件,应使用w+模式(会清空原内容)。createStream的第二个参数支持'r','r+','w','w+','a','a+',含义与标准 C 类似。
4. 结合 FileWatcher 与 Stream:增量读取日志尾部
文件监听触发后,如果日志文件很大,每次事件都重新读取整个文件效率极低。可以结合 Stream 记录上次读取位置,只读取增量。
importfsfrom'@ohos.file.fs';exportclassTailReader{privatefilePath:string;privatelastPosition:number=0;privatestream:fs.Stream|null=null;constructor(filePath:string){this.filePath=filePath;}asyncinit(){// 打开流并定位到文件末尾,实现 tail -f 效果this.stream=awaitfs.createStream(this.filePath,'r');letstat=awaitfs.stat(this.filePath);this.lastPosition=stat.size;awaitthis.stream.seek({position:this.lastPosition,whence:0});}asyncreadNewContent():Promise<string>{if(!this.stream){return'';}letbuffer=newArrayBuffer(4096);letbytesRead=awaitthis.stream.read(buffer);if(bytesRead===0){return'';}this.lastPosition+=bytesRead;letdecoder=util.TextDecoder.create('utf-8');returndecoder.decodeWithStream(buffer.slice(0,bytesRead));}close(){if(this.stream){this.stream.close();this.stream=null;}}}// 使用监听读取新增内容lettailer=newTailReader('/data/app.log');awaittailer.init();letwatcher=fs.watch('/data/app.log');watcher.on('change',async()=>{letnewContent=awaittailer.readNewContent();if(newContent){console.info(`新增日志:${newContent}`);}});三、注意事项汇总
- FileWatcher 的
recursive参数:目前仅支持监听一层子目录,深目录需自行遍历创建多个Watcher。监听过多文件可能影响性能,建议按需创建。 - Stream 的文件指针:
seek后执行read/write,指针会自动移动。如果混用读写(如r+模式),要注意指针位置。 - 内存管理:每次
read调用都会分配ArrayBuffer,如果读取循环次数多,尽量复用同一 buffer(但需确保长度足够或重新分配)。write时也要避免大块一次性写入,建议分片不超过 10MB。 - 错误处理:所有文件操作都可能抛出
BusinessError,必须使用 try-catch 包裹。关闭流失败的错误尤其容易被忽略,建议单独 catch。 - 文件路径:HarmonyOS 应用沙箱路径需通过
context.filesDir或context.cacheDir获取,不要硬编码路径。上述示例中/data/storage/el2/base/haps/entry/files仅为演示,实际开发应使用getContext().filesDir。
如果在实际项目中使用上述代码遇到了其他问题(例如 FileWatcher 在设备休眠后失效、Stream 的seek返回值含义不明确),欢迎在评论区交流。