FSEventStreamRef事件异常致文件夹实时镜像故障求解
解决FSEventStream同步File Package(.app)时的事件混乱问题
针对你在使用FSEventStream实现Finder文件夹镜像时,遇到的File Package(如.app)复制事件顺序混乱、同步失败的问题,可以通过以下核心策略解决:
1. 事件去重与层级合并
FSEventStream处理File Package复制时,会同时触发父路径(.app)和子路径的创建事件,导致重复同步操作。需要维护待处理事件队列,自动忽略子路径事件:
- 用字典存储待处理任务,键为文件/文件夹路径,值为对应的延迟处理任务。
- 当收到File Package父路径的创建事件时,遍历队列并取消所有子路径的待处理任务,确保只执行父路径的完整同步。
- 同一路径的重复事件,直接取消之前的任务,只保留最新的一次。
2. 延迟处理等待写入完成
Finder复制File Package时,内容会分批写入磁盘,事件也会分批触发。设置短延迟(1-2秒,略长于FSEventStream的刷新间隔),等待所有写入操作完成后再执行同步:
- 使用GCD的
dispatch_after实现延迟任务,确保同一路径只保留最新的延迟任务,避免重复执行。 - 延迟时间可根据实际情况调整,平衡同步实时性和写入完整性。
3. 检查File Package的就绪状态
处理父路径事件时,先验证File Package是否已完全写入,避免复制不完整的内容:
- 通过
LSCopyItemInfoForURL判断路径是否为File Package。 - 检查关键文件(如
Contents/Info.plist)是否存在,并验证其文件大小是否稳定(短时间内无变化),确认写入已完成。 - 若未就绪,可重试3-5次,每次间隔1秒,直到就绪后再执行同步。
4. 优化同步逻辑
调整同步操作的执行方式,避免错误覆盖或清空已同步内容:
- 处理File Package/文件夹事件时,直接同步整个目录,而非逐个处理子文件。
- 目标路径已存在时,可选择先删除再复制(镜像场景下),或增量更新(对比文件修改时间/哈希),避免部分内容残留。
- 保留
kFSEventStreamCreateFlagMarkSelf参数,确保自身的同步操作不会触发FSEventStream事件,避免循环触发。
具体代码示例
以下是修改后的事件处理函数核心逻辑:
#include <CoreServices/CoreServices.h> #include <Foundation/Foundation.h> static dispatch_queue_t syncQueue = NULL; static NSMutableDictionary *pendingTasks = NULL; // 初始化队列和任务字典 void syncInit() { syncQueue = dispatch_queue_create("com.your.sync.queue", DISPATCH_QUEUE_SERIAL); pendingTasks = [[NSMutableDictionary alloc] init]; } // 判断是否为File Package BOOL isFilePackage(NSString *path) { NSURL *url = [NSURL fileURLWithPath:path]; LSItemInfoRef itemInfo = LSCopyItemInfoForURL((__bridge CFURLRef)url, kLSRequestAllFlags); if (!itemInfo) return NO; BOOL isPackage = LSItemInfoGetIsPackage(itemInfo); CFRelease(itemInfo); return isPackage; } // 检查File Package是否就绪 BOOL isPackageReady(NSString *packagePath) { NSFileManager *fm = [NSFileManager defaultManager]; NSString *infoPlistPath = [packagePath stringByAppendingPathComponent:@"Contents/Info.plist"]; if (![fm fileExistsAtPath:infoPlistPath]) return NO; // 检查文件大小是否稳定 NSDictionary *attrs = [fm attributesOfItemAtPath:infoPlistPath error:nil]; if (!attrs) return NO; unsigned long long initialSize = [attrs fileSize]; sleep(0.5); NSDictionary *newAttrs = [fm attributesOfItemAtPath:infoPlistPath error:nil]; if (!newAttrs) return NO; return [newAttrs fileSize] == initialSize; } void realTimeSyncFoundChanges(ConstFSEventStreamRef streamRef, void *clientCallBackInfo, size_t numEvents, void *eventPaths, const FSEventStreamEventFlags eventFlags[], const FSEventStreamEventId eventIds[]) { for (size_t i = 0; i < numEvents; i++) { NSString *path = (__bridge NSString *)((const char **)eventPaths)[i]; FSEventStreamEventFlags flags = eventFlags[i]; BOOL isPackage = isFilePackage(path); BOOL isDir = (flags & kFSEventStreamEventFlagItemIsDirectory) != 0; // 取消该路径之前的待处理任务 NSString *taskKey = path; dispatch_block_t existingTask = pendingTasks[taskKey]; if (existingTask) { dispatch_block_cancel(existingTask); [pendingTasks removeObjectForKey:taskKey]; } // 创建新的延迟处理任务 dispatch_block_t task = ^{ [pendingTasks removeObjectForKey:taskKey]; // 处理File Package/文件夹 if (isPackage || isDir) { // 等待包就绪,最多重试3次 int retry = 0; while (!isPackageReady(path) && retry < 3) { sleep(1); retry++; } if (retry >= 3) return; // 执行镜像同步 NSString *destPath = [path stringByReplacingOccurrencesOfString:@"/A/" withString:@"/B/"]; NSFileManager *fm = [NSFileManager defaultManager]; if ([fm fileExistsAtPath:destPath]) { [fm removeItemAtPath:destPath error:nil]; } [fm copyItemAtPath:path toPath:destPath error:nil]; } // 处理普通文件 else { NSString *destPath = [path stringByReplacingOccurrencesOfString:@"/A/" withString:@"/B/"]; NSFileManager *fm = [NSFileManager defaultManager]; [fm createDirectoryAtPath:[destPath stringByDeletingLastPathComponent] withIntermediateDirectories:YES attributes:nil error:nil]; [fm copyItemAtPath:path toPath:destPath error:nil]; } }; // 延迟1.5秒执行 dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.5 * NSEC_PER_SEC)), syncQueue, task); pendingTasks[taskKey] = task; // 取消当前包下所有子路径的待处理任务 if (isPackage || isDir) { NSString *prefix = [path stringByAppendingString:@"/"]; NSMutableArray *keysToRemove = [NSMutableArray array]; for (NSString *key in pendingTasks) { if ([key hasPrefix:prefix] && ![key isEqualToString:path]) { dispatch_block_t subTask = pendingTasks[key]; dispatch_block_cancel(subTask); [keysToRemove addObject:key]; } } [pendingTasks removeObjectsInArray:keysToRemove]; } } }
额外注意事项
- 使用串行队列处理同步任务,避免并发操作导致的文件冲突。
- 若追求更高性能,可替换
NSFileManager的复制操作,使用rsync命令行工具实现增量同步,减少不必要的文件复制。
内容的提问来源于stack exchange,提问作者Leonardo
相关产品推荐
相关产品推荐

