官方文档对 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. 基本用法
typescript
import fs from '@ohos.file.fs';
// 监听文件变化
let watcher = 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 默认递归监听子目录,但官方文档未明确。
typescript
let dirWatcher = 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. 完整示例:实时监控日志文件并输出变化
typescript
import fs from '@ohos.file.fs';
import { BusinessError } from '@ohos.base';
let logPath = '/data/storage/el2/base/haps/entry/files/app.log';
function startLogWatcher() {
try {
let watcher = 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 失败: ${(error as BusinessError).message}`);
}
}
// 停止监听
function stopLogWatcher() {
if (globalThis.logWatcher) {
globalThis.logWatcher.stop();
globalThis.logWatcher = null;
console.info('日志监控已停止');
}
}

二、Stream:大文件分片读写
对于超大文件(如视频、数据库文件),一次性读取到内存会导致 OOM,必须使用流式读写。fs.createStream 创建文件流,通过 read、write、seek 等方法操作文件。
1. 创建读流并分片读取
typescript
import fs from '@ohos.file.fs';
import { BusinessError } from '@ohos.base';
async function readLargeFile(chunkSize: number = 1024 * 1024) { // 默认1MB
let filePath = '/data/storage/el2/base/haps/entry/files/bigfile.bin';
let stream: fs.Stream | null = null;
try {
stream = await fs.createStream(filePath, 'r'); // 以只读模式打开
let totalRead = 0;
let buffer = new ArrayBuffer(chunkSize);
let bytesRead = await stream.read(buffer);
while (bytesRead > 0) {
// 处理当前分片数据
let slice = buffer.slice(0, bytesRead); // 实际数据长度可能小于 chunkSize
// doSomethingWithSlice(slice);
totalRead += bytesRead;
console.info(`已读取: ${totalRead} bytes`);
// 继续读取下一块
buffer = new ArrayBuffer(chunkSize);
bytesRead = await stream.read(buffer);
}
console.info(`文件读取完成,总大小: ${totalRead} bytes`);
} catch (error) {
console.error(`流式读取失败: ${(error as BusinessError).message}`);
} finally {
if (stream) {
stream.close().catch((err: BusinessError) => {
console.error(`关闭流失败: ${err.message}`);
});
}
}
}
注意事项 :stream.read(buffer) 返回实际读取的字节数,如果文件剩余不足 buffer.byteLength,则返回剩余大小。最后一次读取返回 0 表示文件结束。务必在 finally 中关闭流,否则文件句柄泄漏可能导致后续操作失败。
2. 使用 seek 实现随机读写
在数据库或日志分段加载场景中,需要跳过已有内容。seek 方法可以定位到指定位置。
typescript
async function readAtPosition(filePath: string, position: number, length: number): Promise<ArrayBuffer> {
let stream: fs.Stream | null = null;
try {
stream = await fs.createStream(filePath, 'r');
// 定位到指定位置
let seekResult = await stream.seek({ position: position, whence: 0 }); // whence: 0=SEEK_SET
console.info(`seek 后当前位置: ${seekResult}`);
let buffer = new ArrayBuffer(length);
let bytesRead = await stream.read(buffer);
if (bytesRead < length) {
// 如果实际读取少于请求长度,截取有效部分
return buffer.slice(0, bytesRead);
}
return buffer;
} catch (error) {
console.error(`随机读取失败: ${(error as BusinessError).message}`);
throw error;
} finally {
if (stream) {
stream.close();
}
}
}

3. 流式写入:分段追加文件
大文件下载时,需要边下载边写入。createStream 以追加模式打开,配合 write 方法实现增量写入。
typescript
async function appendToLargeFile(filePath: string, dataChunks: ArrayBuffer[]) {
let stream: fs.Stream | null = null;
try {
// 以追加写模式打开,若文件不存在会创建
stream = await fs.createStream(filePath, 'a+');
for (let i = 0; i < dataChunks.length; i++) {
let chunk = dataChunks[i];
let bytesWritten = await stream.write(chunk);
console.info(`第 ${i + 1} 块写入 ${bytesWritten} bytes`);
// 此处可添加进度回调
}
console.info('所有数据块写入完成');
} catch (error) {
console.error(`流式写入失败: ${(error as BusinessError).message}`);
} finally {
if (stream) {
stream.close();
}
}
}
常见误区 :a+ 模式会将文件指针移到末尾,所以后续 write 总是追加。如果希望覆写文件,应使用 w+ 模式(会清空原内容)。createStream 的第二个参数支持 'r', 'r+', 'w', 'w+', 'a', 'a+',含义与标准 C 类似。
4. 结合 FileWatcher 与 Stream:增量读取日志尾部
文件监听触发后,如果日志文件很大,每次事件都重新读取整个文件效率极低。可以结合 Stream 记录上次读取位置,只读取增量。
typescript
import fs from '@ohos.file.fs';
export class TailReader {
private filePath: string;
private lastPosition: number = 0;
private stream: fs.Stream | null = null;
constructor(filePath: string) {
this.filePath = filePath;
}
async init() {
// 打开流并定位到文件末尾,实现 tail -f 效果
this.stream = await fs.createStream(this.filePath, 'r');
let stat = await fs.stat(this.filePath);
this.lastPosition = stat.size;
await this.stream.seek({ position: this.lastPosition, whence: 0 });
}
async readNewContent(): Promise<string> {
if (!this.stream) {
return '';
}
let buffer = new ArrayBuffer(4096);
let bytesRead = await this.stream.read(buffer);
if (bytesRead === 0) {
return '';
}
this.lastPosition += bytesRead;
let decoder = util.TextDecoder.create('utf-8');
return decoder.decodeWithStream(buffer.slice(0, bytesRead));
}
close() {
if (this.stream) {
this.stream.close();
this.stream = null;
}
}
}
// 使用监听读取新增内容
let tailer = new TailReader('/data/app.log');
await tailer.init();
let watcher = fs.watch('/data/app.log');
watcher.on('change', async () => {
let newContent = await tailer.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 返回值含义不明确),欢迎在评论区交流。