当前位置: 首页 > news >正文

HarmonyOS文件基础服务(Core File Kit)实战演练04-文件监听与流式读写

官方文档对 FileWatcher 和 Stream 的描述不够详细,如何实现文件监听与流式读写?

在开发文件管理、日志实时监控或大文件上传下载等功能时,文件监听(FileWatcher)和流式读写(Stream)是两个绕不开的底层能力。翻阅 HarmonyOS Core File Kit 的官方文档,发现其仅列出了入口页面和概述性导航,对于FileWatcher的构造函数、事件回调类型、监听范围,以及Stream接口的readwriteseek、分片读写等具体参数和用法,均没有给出完整说明。这种情况下,只能通过 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创建文件流,通过readwriteseek等方法操作文件。

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}`);}});


三、注意事项汇总

  1. FileWatcher 的recursive参数:目前仅支持监听一层子目录,深目录需自行遍历创建多个Watcher。监听过多文件可能影响性能,建议按需创建。
  2. Stream 的文件指针seek后执行read/write,指针会自动移动。如果混用读写(如r+模式),要注意指针位置。
  3. 内存管理:每次read调用都会分配ArrayBuffer,如果读取循环次数多,尽量复用同一 buffer(但需确保长度足够或重新分配)。write时也要避免大块一次性写入,建议分片不超过 10MB。
  4. 错误处理:所有文件操作都可能抛出BusinessError,必须使用 try-catch 包裹。关闭流失败的错误尤其容易被忽略,建议单独 catch。
  5. 文件路径:HarmonyOS 应用沙箱路径需通过context.filesDircontext.cacheDir获取,不要硬编码路径。上述示例中/data/storage/el2/base/haps/entry/files仅为演示,实际开发应使用getContext().filesDir

如果在实际项目中使用上述代码遇到了其他问题(例如 FileWatcher 在设备休眠后失效、Stream 的seek返回值含义不明确),欢迎在评论区交流。

http://www.zskr.cn/news/1429155.html

相关文章:

  • SLAM 算法横向对比与选型指南
  • Revelation光影包:终极Minecraft写实渲染技术完全指南
  • 国产开源软件盘点:替代商业软件的 10 个优秀方案与落地边界
  • VCS仿真不出波形?从Makefile到TB代码,手把手教你生成和打开FSDB文件
  • 2026年SEO现状:精分时代的AI博弈
  • 单Agent搞不定长链路?OpenClaw动态编排架构,让多智能体协作不再“各说各话”
  • 电路设计实战指南:从原理图到PCB的完整流程与调试技巧
  • 3步极速上手:Zotero茉莉花插件中文文献管理终极指南
  • Keil MDK同名源文件处理机制解析与实践
  • Mask2Former的Mask Attention到底强在哪?一个模块拆解看懂Transformer如何提升分割精度
  • 如何快速掌握HiveWE魔兽地图编辑器:面向新手的完整教程
  • Unity UI避坑指南:TMPro文本框动态伸缩时,背景图为什么总对不齐?
  • 保姆级教程:用R语言Signac包从零处理10x Genomics单细胞ATAC数据(附避坑指南)
  • 不只是Enter Play Mode Setting:深度优化Unity工作流,手动控制Domain Reload的完整实践
  • LwIP下ICMP协议浅析
  • Pearcleaner:macOS彻底清理工具的终极指南
  • 第24篇|相机权限和设备枚举:先判断能力再打开预览
  • 打破Java字节码黑箱:JD-GUI的实战逆向工程指南
  • HS2-HF补丁:让Honey Select 2游戏体验焕然一新的终极解决方案
  • PyTorch实现的MANO手部模型:3D手势生成与计算机视觉应用终极指南
  • IGMP协议浅析
  • 2026 杭州直播代运营行业大洗牌,乱象频发,高 ROI 靠谱全链路服务商精选推荐 - 品牌榜中榜
  • 别再死磕梯度下降了!用Python手搓一个遗传算法,轻松搞定那些‘不听话’的优化问题
  • 别再让回车变空格了!手把手教你用JavaScript处理textarea换行符(含 转br实战)
  • 用Scratch打造钩针图案生成器:连接编程与手工的创意实践
  • 2026年 西安消防器材/消防设备/消防设施/灭火器材/应急消防器材最新推荐:精选品牌与实战性能深度解析! - 品牌企业推荐师(官方)
  • 从假设检验到机器学习:正态分布与卡方分布在数据分析中的实战联动指南
  • WarcraftHelper终极指南:让经典魔兽争霸3焕发新生,解决所有版本兼容问题
  • 乔布斯教会耄耋的事:在《一念成仙》,耄耋如何定义“最好的产品”
  • 告别深夜夺命Call:如何利用 AI Agent Skills 自动自愈生产环境故障