HarmonyOS开发中文件监听:文件变化通知
📌 核心要点:实时感知文件系统变化,通过FileWatcher实现增量更新,避免全量扫描的性能开销
一、背景与动机
你有没有做过这种功能——展示设备里的所有图片,用户拍了张新照片,你的界面死活不更新?或者做了个配置文件热加载,改了配置得重启应用才生效?
这些问题的根源都是文件变化感知。传统做法是开个定时器轮询,每隔几秒扫描一遍目录,对比文件列表有没有变化。这方法能用,但太low了——空扫描浪费资源,扫描间隔短了耗电,间隔长了又不实时。
HarmonyOS提供了FileWatcher机制,让系统主动告诉你文件变化了,而不是你傻傻地去问。这就像从"每隔五分钟问一次快递到了没"变成"快递到了给你打电话",效率提升不是一点半点。
文件监听的应用场景很广:相册增量更新、配置热加载、日志文件监控、同步目录变化……掌握了这个技能,你的应用就能做到"所见即所得"的实时响应。
二、核心原理
2.1 文件监听机制
FileWatcher基于操作系统的inotify机制(类Unix系统)或ReadDirectoryChangesW(Windows),通过内核事件队列通知用户空间进程。相比轮询扫描,优势明显:
- 零开销等待:没有变化时不消耗CPU
- 实时通知:事件发生立即触发回调
- 精确信息:告诉你具体哪个文件、什么操作
2.2 监听事件类型
FileWatcher支持多种文件系统事件,每种事件对应不同的操作类型:
| 事件类型 | 触发条件 | 典型场景 |
|---|---|---|
| CREATE | 新建文件/目录 | 新照片、新下载 |
| DELETE | 删除文件/目录 | 用户清理、卸载 |
| MODIFY | 文件内容修改 | 配置更新、日志追加 |
| MOVE_SELF | 文件被移动 | 文件整理 |
| ATTRIB | 属性变化 | 权限修改、时间戳更新 |
2.3 监听范围与性能
监听范围越广,内核维护的监控项越多。HarmonyOS对监听数量有限制(通常1024个),超过限制会报错。所以实际使用时要权衡:
- 单文件监听:精确,资源占用小,适合配置文件
- 目录监听:覆盖面广,但事件量大,适合相册目录
- 递归监听:最全面,但资源消耗最大,慎用
三、代码实战
3.1 基础用法:监听单个文件
先从最简单的场景开始——监听一个配置文件的变化:
import fileWatcher from '@ohos.file.fileWatcher';
import fs from '@ohos.file.fs';
/**
* 配置文件监听器
* 实现配置热加载
*/
export class ConfigFileWatcher {
private watcher: fileWatcher.Watcher | null = null;
private configPath: string;
private onChangeCallback: ((config: AppConfig) => void) | null = null;
constructor(configPath: string) {
this.configPath = configPath;
}
/**
* 开始监听配置文件
*/
async start(onChange: (config: AppConfig) => void): Promise<void> {
this.onChangeCallback = onChange;
// 确保文件存在
if (!fs.accessSync(this.configPath)) {
throw new Error(`Config file not found: ${this.configPath}`);
}
// 创建监听器
this.watcher = fileWatcher.createWatcher();
// 注册监听事件
this.watcher.on('change', (event: fileWatcher.WatchEvent) => {
console.info(`[ConfigWatcher] Event: ${event.eventType}, Path: ${event.path}`);
this.handleConfigChange();
});
// 开始监听指定文件
await this.watcher.start(this.configPath, {
events: [fileWatcher.EventType.MODIFY, fileWatcher.EventType.ATTRIB]
});
console.info(`[ConfigWatcher] Started watching: ${this.configPath}`);
}
/**
* 处理配置变化
*/
private handleConfigChange(): void {
try {
// 重新读取配置文件
const config = this.loadConfig();
if (this.onChangeCallback) {
this.onChangeCallback(config);
}
} catch (error) {
console.error(`[ConfigWatcher] Failed to load config: ${error.message}`);
}
}
/**
* 加载配置文件
*/
private loadConfig(): AppConfig {
const content = fs.readTextSync(this.configPath);
return JSON.parse(content) as AppConfig;
}
/**
* 停止监听
*/
async stop(): Promise<void> {
if (this.watcher) {
await this.watcher.stop();
this.watcher = null;
console.info('[ConfigWatcher] Stopped');
}
}
}
/**
* 应用配置类型
*/
interface AppConfig {
theme: 'light' | 'dark';
language: string;
fontSize: number;
autoSync: boolean;
}3.2 进阶用法:监听目录变化
监听目录能捕获该目录下所有文件的创建、删除、修改事件:
import fileWatcher from '@ohos.file.fileWatcher';
import fs from '@ohos.file.fs';
/**
* 文件变化事件
*/
export interface FileChangeEvent {
type: 'create' | 'delete' | 'modify';
path: string;
timestamp: number;
}
/**
* 目录监听器
* 监听目录下所有文件的变化
*/
export class DirectoryWatcher {
private watcher: fileWatcher.Watcher | null = null;
private watchPath: string;
private eventQueue: FileChangeEvent[] = [];
private isProcessing: boolean = false;
constructor(watchPath: string) {
this.watchPath = watchPath;
}
/**
* 开始监听目录
*/
async start(
onFileChange: (event: FileChangeEvent) => void
): Promise<void> {
// 确保目录存在
if (!fs.accessSync(this.watchPath)) {
fs.mkdirSync(this.watchPath, true);
}
// 创建监听器
this.watcher = fileWatcher.createWatcher();
// 注册事件处理
this.watcher.on('change', async (event: fileWatcher.WatchEvent) => {
// 将事件加入队列,避免并发处理
const changeEvent: FileChangeEvent = {
type: this.mapEventType(event.eventType),
path: event.path,
timestamp: Date.now()
};
this.eventQueue.push(changeEvent);
await this.processQueue(onFileChange);
});
// 监听目录的所有事件类型
await this.watcher.start(this.watchPath, {
events: [
fileWatcher.EventType.CREATE,
fileWatcher.EventType.DELETE,
fileWatcher.EventType.MODIFY
],
recursive: false // 不递归监听子目录
});
console.info(`[DirectoryWatcher] Started watching: ${this.watchPath}`);
}
/**
* 处理事件队列
*/
private async processQueue(
onFileChange: (event: FileChangeEvent) => void
): Promise<void> {
if (this.isProcessing || this.eventQueue.length === 0) {
return;
}
this.isProcessing = true;
try {
while (this.eventQueue.length > 0) {
const event = this.eventQueue.shift();
if (event) {
// 过滤掉临时文件(如~开头或.tmp结尾)
if (!this.isTempFile(event.path)) {
onFileChange(event);
}
}
}
} finally {
this.isProcessing = false;
}
}
/**
* 映射事件类型
*/
private mapEventType(type: fileWatcher.EventType): 'create' | 'delete' | 'modify' {
switch (type) {
case fileWatcher.EventType.CREATE:
return 'create';
case fileWatcher.EventType.DELETE:
return 'delete';
default:
return 'modify';
}
}
/**
* 判断是否为临时文件
*/
private isTempFile(path: string): boolean {
const fileName = path.split('/').pop() ?? '';
return fileName.startsWith('~') || fileName.endsWith('.tmp');
}
/**
* 停止监听
*/
async stop(): Promise<void> {
if (this.watcher) {
await this.watcher.stop();
this.watcher = null;
this.eventQueue = [];
console.info('[DirectoryWatcher] Stopped');
}
}
/**
* 获取当前监听状态
*/
isWatching(): boolean {
return this.watcher !== null;
}
}3.3 实战案例:相册增量更新
结合文件监听实现相册的实时更新,新照片自动出现,删除照片自动消失:
import { DirectoryWatcher, FileChangeEvent } from './DirectoryWatcher';
import fs from '@ohos.file.fs';
import image from '@ohos.multimedia.image';
/**
* 照片信息
*/
interface PhotoInfo {
path: string;
name: string;
thumbnail: image.PixelMap | null;
createTime: number;
size: number;
}
/**
* 相册管理器
* 基于文件监听实现增量更新
*/
export class AlbumManager {
private photos: Map<string, PhotoInfo> = new Map();
private watcher: DirectoryWatcher | null = null;
private albumPath: string;
private onUpdateCallback: (() => void) | null = null;
constructor(albumPath: string) {
this.albumPath = albumPath;
}
/**
* 初始化相册
*/
async init(onUpdate: () => void): Promise<void> {
this.onUpdateCallback = onUpdate;
// 首次加载:扫描现有照片
await this.loadExistingPhotos();
// 启动文件监听
this.watcher = new DirectoryWatcher(this.albumPath);
await this.watcher.start(this.handleFileChange.bind(this));
console.info(`[AlbumManager] Initialized with ${this.photos.size} photos`);
}
/**
* 加载现有照片
*/
private async loadExistingPhotos(): Promise<void> {
const files = fs.listFileSync(this.albumPath);
for (const fileName of files) {
const filePath = `${this.albumPath}/${fileName}`;
// 只处理图片文件
if (this.isImageFile(fileName)) {
const photo = await this.loadPhotoInfo(filePath);
if (photo) {
this.photos.set(filePath, photo);
}
}
}
}
/**
* 处理文件变化
*/
private async handleFileChange(event: FileChangeEvent): Promise<void> {
console.info(`[AlbumManager] File change: ${event.type} - ${event.path}`);
switch (event.type) {
case 'create':
// 新建文件:添加到相册
if (this.isImageFile(event.path)) {
const photo = await this.loadPhotoInfo(event.path);
if (photo) {
this.photos.set(event.path, photo);
this.notifyUpdate();
}
}
break;
case 'delete':
// 删除文件:从相册移除
if (this.photos.has(event.path)) {
this.photos.delete(event.path);
this.notifyUpdate();
}
break;
case 'modify':
// 修改文件:更新照片信息
if (this.photos.has(event.path)) {
const photo = await this.loadPhotoInfo(event.path);
if (photo) {
this.photos.set(event.path, photo);
this.notifyUpdate();
}
}
break;
}
}
/**
* 加载照片信息
*/
private async loadPhotoInfo(path: string): Promise<PhotoInfo | null> {
try {
const stat = fs.statSync(path);
const fileName = path.split('/').pop() ?? '';
// 创建缩略图
const thumbnail = await this.createThumbnail(path);
return {
path,
name: fileName,
thumbnail,
createTime: stat.mtime,
size: stat.size
};
} catch (error) {
console.error(`[AlbumManager] Failed to load photo: ${error.message}`);
return null;
}
}
/**
* 创建缩略图
*/
private async createThumbnail(path: string): Promise<image.PixelMap | null> {
try {
const imageSource = image.createImageSource(path);
const decodingOptions: image.DecodingOptions = {
desiredSize: { width: 200, height: 200 },
editable: true
};
return await imageSource.createPixelMap(decodingOptions);
} catch (error) {
return null;
}
}
/**
* 判断是否为图片文件
*/
private isImageFile(path: string): boolean {
const ext = path.split('.').pop()?.toLowerCase() ?? '';
return ['jpg', 'jpeg', 'png', 'gif', 'webp', 'heic'].includes(ext);
}
/**
* 通知UI更新
*/
private notifyUpdate(): void {
if (this.onUpdateCallback) {
this.onUpdateCallback();
}
}
/**
* 获取所有照片
*/
getPhotos(): PhotoInfo[] {
return Array.from(this.photos.values())
.sort((a, b) => b.createTime - a.createTime); // 按时间倒序
}
/**
* 销毁
*/
async destroy(): Promise<void> {
if (this.watcher) {
await this.watcher.stop();
this.watcher = null;
}
this.photos.clear();
}
}3.4 完整UI示例
import { AlbumManager, PhotoInfo } from './AlbumManager';
import { Context } from '@ohos.abilityAccessCtrl';
@Entry
@Component
struct AlbumPage {
@State photos: PhotoInfo[] = [];
@State isLoading: boolean = true;
private albumManager: AlbumManager | null = null;
private readonly ALBUM_PATH = '/data/service/el2/base/hms/data/album';
async aboutToAppear(): Promise<void> {
// 初始化相册管理器
this.albumManager = new AlbumManager(this.ALBUM_PATH);
await this.albumManager.init(() => {
// 文件变化时的回调
this.photos = this.albumManager?.getPhotos() ?? [];
});
// 首次加载
this.photos = this.albumManager.getPhotos();
this.isLoading = false;
}
async aboutToDisappear(): Promise<void> {
await this.albumManager?.destroy();
}
build() {
Column() {
// 标题栏
Row() {
Text('我的相册')
.fontSize(24)
.fontWeight(FontWeight.Bold)
Blank()
Text(`${this.photos.length} 张照片`)
.fontSize(14)
.fontColor('#666666')
}
.width('100%')
.padding({ left: 20, right: 20, bottom: 15 })
// 照片网格
if (this.isLoading) {
LoadingProgress()
.width(50)
.height(50)
} else if (this.photos.length === 0) {
Column() {
Text('📷')
.fontSize(60)
Text('暂无照片')
.fontSize(16)
.fontColor('#999999')
.margin({ top: 10 })
}
.layoutWeight(1)
.justifyContent(FlexAlign.Center)
} else {
Grid() {
ForEach(this.photos, (photo: PhotoInfo) => {
GridItem() {
this.PhotoItem(photo)
}
}, (photo: PhotoInfo) => photo.path)
}
.columnsTemplate('1fr 1fr 1fr')
.rowsGap(5)
.columnsGap(5)
.layoutWeight(1)
.padding({ left: 5, right: 5 })
}
}
.width('100%')
.height('100%')
.backgroundColor('#F5F5F5')
}
@Builder
PhotoItem(photo: PhotoInfo) {
Stack() {
if (photo.thumbnail) {
Image(photo.thumbnail)
.width('100%')
.aspectRatio(1)
.objectFit(ImageFit.Cover)
.borderRadius(8)
} else {
// 缩略图加载失败,显示占位图
Column() {
Text('🖼️')
.fontSize(30)
}
.width('100%')
.aspectRatio(1)
.backgroundColor('#E0E0E0')
.borderRadius(8)
.justifyContent(FlexAlign.Center)
}
}
.width('100%')
}
}四、踩坑与注意事项
4.1 事件风暴问题
短时间内大量文件变化(如批量复制)会产生大量事件,可能阻塞UI线程:
// 问题:直接处理每个事件
watcher.on('change', (event) => {
this.handleEvent(event); // 可能被调用上千次
});
// 解决方案:防抖处理
private pendingEvents: FileChangeEvent[] = [];
private debounceTimer: number = -1;
watcher.on('change', (event) => {
this.pendingEvents.push(event);
// 清除之前的定时器
if (this.debounceTimer !== -1) {
clearTimeout(this.debounceTimer);
}
// 延迟处理,合并短时间内的多个事件
this.debounceTimer = setTimeout(() => {
this.batchHandleEvents(this.pendingEvents);
this.pendingEvents = [];
this.debounceTimer = -1;
}, 500); // 500ms防抖
});4.2 监听数量限制
系统对inotify实例数量有限制,超过会报错:
// 错误示范:监听过多文件
for (const file of files) {
await watcher.start(file); // 可能超过限制
}
// 正确做法:监听目录而非单个文件
await watcher.start(directoryPath, { recursive: true });4.3 文件路径处理
不同事件中的路径格式可能不一致,需要统一处理:
// 某些事件返回相对路径,某些返回绝对路径
watcher.on('change', (event) => {
// 统一转为绝对路径
let absolutePath = event.path;
if (!path.isAbsolute(event.path)) {
absolutePath = path.join(this.watchPath, event.path);
}
// 规范化路径(去除 ./ ../ 等)
absolutePath = path.normalize(absolutePath);
});4.4 监听丢失问题
文件被移动或重命名后,原监听会失效:
// 文件移动后需要重新监听
watcher.on('change', async (event) => {
if (event.eventType === fileWatcher.EventType.MOVE_SELF) {
// 停止旧监听
await watcher.stop();
// 如果知道新路径,重新监听
if (event.newPath) {
await watcher.start(event.newPath);
}
}
});4.5 内存泄漏
忘记停止监听会导致资源泄漏:
// 错误示范:组件销毁时未停止监听
aboutToDisappear(): void {
// 什么都没做,watcher继续运行
}
// 正确做法
aboutToDisappear(): void {
if (this.watcher) {
this.watcher.stop(); // 必须停止
this.watcher = null;
}
}五、HarmonyOS 6适配说明
5.1 API接口调整
HarmonyOS 6对FileWatcher API进行了重构:
// HarmonyOS 5写法
const watcher = fileWatcher.createWatcher();
watcher.on('change', callback);
await watcher.start(path);
// HarmonyOS 6适配
// 使用更语义化的方法名
const watcher = fileWatcher.createWatcher({
path: path,
events: [fileWatcher.EventType.ALL], // 新增ALL枚举
recursive: false
});
// 事件监听改为addEventListener
watcher.addEventListener('change', callback);
// 启动监听
await watcher.activate(); // start改名为activate5.2 权限模型变化
HarmonyOS 6对文件访问权限控制更严格:
// HarmonyOS 5:声明权限即可访问
"requestPermissions": [
{ "name": "ohos.permission.READ_MEDIA" }
]
// HarmonyOS 6:需要动态申请
import abilityAccessCtrl from '@ohos.abilityAccessCtrl';
async function requestFilePermission(): Promise<boolean> {
const atManager = abilityAccessCtrl.createAtManager();
const grantStatus = await atManager.requestPermissionsFromUser(
getContext(),
['ohos.permission.READ_MEDIA', 'ohos.permission.WRITE_MEDIA']
);
return grantStatus.authResults.every(result => result === 0);
}5.3 事件类型扩展
HarmonyOS 6新增了更多事件类型:
// HarmonyOS 6新增事件类型
enum EventType {
CREATE = 0x00000100,
DELETE = 0x00000200,
MODIFY = 0x00000002,
MOVE_SELF = 0x00000800,
ATTRIB = 0x00000004,
CLOSE_WRITE = 0x00000008, // 新增:可写文件关闭
CLOSE_NOWRITE = 0x00000010, // 新增:不可写文件关闭
OPEN = 0x00000020, // 新增:文件打开
ACCESS = 0x00000001, // 新增:文件被访问
ALL = 0xfff // 所有事件
}5.4 性能监控增强
HarmonyOS 6提供了监听性能统计:
// HarmonyOS 6新增:获取监听统计信息
const stats = await watcher.getStatistics();
console.info(`事件总数: ${stats.totalEvents}`);
console.info(`CREATE事件: ${stats.createCount}`);
console.info(`DELETE事件: ${stats.deleteCount}`);
console.info(`MODIFY事件: ${stats.modifyCount}`);
console.info(`事件丢失数: ${stats.lostEvents}`); // 检测是否事件风暴
// 设置事件缓冲区大小
watcher.setBufferSize(8192); // 默认4096,可增大应对高频事件六、总结
| 维度 | 评价 |
|---|---|
| 学习难度 | ⭐⭐⭐ |
| 使用频率 | ⭐⭐⭐⭐ |
| 重要程度 | ⭐⭐⭐⭐ |
| 调试难度 | ⭐⭐⭐⭐ |
文件监听是实现实时响应的关键技术。通过本文的学习,你应该掌握了:
核心收获:
- FileWatcher机制:基于内核事件的实时通知,告别低效轮询
- 事件类型区分:CREATE、DELETE、MODIFY等事件的正确处理
- 目录监听实践:相册增量更新的完整实现方案
- 性能优化技巧:防抖、批量处理、事件过滤等实战经验
最佳实践建议:
- 优先监听目录而非单个文件,减少监听实例数量
- 对高频事件做防抖处理,避免事件风暴
- 组件销毁时必须停止监听,防止内存泄漏
- 统一路径格式处理,避免相对路径和绝对路径混用
- 监听MOVE事件,处理文件移动后的监听失效
文件监听是把双刃剑——用好了能实现丝滑的实时更新,用不好可能导致性能问题甚至崩溃。记住:只监听必要的范围,及时处理事件,适时释放资源。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用。你还可以使用@来通知其他用户。