当前位置: 首页 > 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.jsqmd.com/news/917839/

相关文章:

  • 2026年C++最热实测(二)——C++26那些“不起眼”却救命的新特性
  • 深入探索MuPDF mutool:PDF处理的命令行高效解决方案
  • 【紧急预警】传统知识库系统将在18个月内集体失效:AI原生知识管理迁移倒计时启动(含兼容性评估工具包)
  • 为什么你的独立站SEO没询盘?高手都在偷偷用这套“低成本拿大单”打法
  • 告别依赖地狱:用linuxdeployqt把QT程序打包成AppImage,一个文件搞定所有Linux发行版
  • 告别eMMC卡顿:手把手教你理解手机里的UFS 4.0闪存到底快在哪
  • TypeScript高级特性:提升代码质量
  • 基于ESP32与LVGL的嵌入式GUI开发:圣诞雪花球交互项目全解析
  • SLAM 算法横向对比与选型指南
  • Gemini数据分析报告生成逻辑首度公开:基于217份企业级报告的逆向工程分析(限期内部资料)
  • Ovito 3.6.0基础版也能搞定:手把手教你用CNA和W-S法可视化辐照损伤中的晶界与点缺陷
  • Revelation光影包:终极Minecraft写实渲染技术完全指南
  • 3分钟掌握Sketch批量重命名:告别混乱图层管理的终极指南
  • 2026年美妆品牌用AI工具做海报:618电商节生图到生视频一站式方案来了!
  • ComfyUI-WanVideoWrapper架构深度解析:PyTorch编译优化与显存管理最佳实践
  • 国产开源软件盘点:替代商业软件的 10 个优秀方案与落地边界
  • vJoy虚拟手柄终极方案:5分钟让键盘变身专业游戏控制器
  • Windows平台安卓应用安装器:告别模拟器,拥抱高效智能的一体化解决方案
  • 【限时解密】:某Top3律所内部使用的Claude文档推理增强框架(含OCR对齐校验模块源码片段)
  • VCS仿真不出波形?从Makefile到TB代码,手把手教你生成和打开FSDB文件
  • AI Agent工具调用精通路线图:掌握从推理到执行的关键桥梁
  • Anthropic深夜炸场,最强旗舰 Claude Opus 4.8 发布,代码与Agent能力全面进化!
  • 2026年SEO现状:精分时代的AI博弈
  • ComfyUI视频助手套件:3分钟学会将AI图片变动态视频的终极指南
  • 单Agent搞不定长链路?OpenClaw动态编排架构,让多智能体协作不再“各说各话”
  • 销售团队为什么需要 CRM 与合同、回款、审批联动
  • 电路设计实战指南:从原理图到PCB的完整流程与调试技巧
  • 3步极速上手:Zotero茉莉花插件中文文献管理终极指南
  • Keil MDK同名源文件处理机制解析与实践
  • 3步完成HS2-HF Patch安装:解锁Honey Select 2完整汉化与功能增强