前言
之前写了一个 Mac 下的文件同步客户端, 需要监控本地的文件变化并同步上去. 几经波折, 最有使用的是FSEventStream 来实现文件监控
简介
FSEventStream 是一套 C 语言的方法, 类似于 CoreGraphic.
由于是 C 语言方法, 建议使用 Objective-C 来编写代码 ,用 swift 会涉及到大量类型转换
用到的函数有下面几个:
- FSEventStreamCreate
创建一个文件监控句柄, 可以在这个函数中绑定一个函数回调 - FSEventStreamRetain
- FSEventStreamRelease
FSEventStream不支持 ARC, 必须手动 retain 和 release - FSEventStreamScheduleWithRunLoop
加入到一个 runloop 中, 才可以实现文件的监控 - FSEventStreamStart
启动文件监控 - FSEventStreamStop
停止文件监控 - FSEventStreamInvalidate
从 runloop 中移除
整体流程就是创建文件监控句柄, 加入到 runloop, 最后启动就可以了
停止的时候, 首先调用 stop, 然后 从 runloop 中移除, 最后使用 release 释放
FSEventStreamCreate
这个函数负责创建一个文件监控句柄, 函数声明如下
extern FSEventStreamRef __nullable
FSEventStreamCreate(
CFAllocatorRef __nullable allocator,
FSEventStreamCallback callback,
FSEventStreamContext * __nullable context,
CFArrayRef pathsToWatch,
FSEventStreamEventId sinceWhen,
CFTimeInterval latency,
FSEventStreamCreateFlags flags)
第一个参数 allocator 传入 NULL 就可以了;
第二个参数 callback 为事件回调, 当监控的文件夹发生事件之后, 会出发回调函数;
第三参数context, 由于回调函数是 C 函数, 无法直接使用 self , 如果需要使用 self, 可以利用这个参数将 self 传进去.
FSEventStreamContext context;
context.info = (__bridge void * _Nullable)(self);
context.version = 0;
context.retain = NULL;
context.release = NULL;
context.copyDescription = NULL;
第四个参数 pathsToWatch 为需要监控的文件夹路径数组, 也就是说你可以同时监控多个文件夹.
第五个参数 sinceWhen 很有用, 可以指定从哪个事件开始监控, 如果是第一次监控, 那么可以设置为kFSEventStreamEventIdSinceNow, 表示从现在开始监控, 后面如果发生事件之后, 回调函数中会传入最新的事件 id, 可以将这个存下来, 以后就以这个事件 id 作为起点. 这样可以做到即使你程序关闭了, 你也不会漏掉事件
第六个参数 latency 是监控的时间间隔, 可以指定多少秒之后监控一次
最后一个参数 flags 用于配置文件监控, 具体可以参考文档, 我一般使用kFSEventStreamCreateFlagFileEvents 和 kFSEventStreamCreateFlagUseCFTypes
前者可以将事件细分到具体的文件, 后者则是方便回调函数使用
完整的函数调用示例如下
FSEventStreamContext context;
context.info = (__bridge void * _Nullable)(self);
context.version = 0;
context.retain = NULL;
context.release = NULL;
context.copyDescription = NULL;
self.syncEventStream = FSEventStreamCreate(NULL, &fsevents_callback, &context, (__bridge CFArrayRef _Nonnull)(paths), self.syncEventID, self.syncInterval, kFSEventStreamCreateFlagFileEvents | kFSEventStreamCreateFlagUseCFTypes);
FSEventStreamScheduleWithRunLoop(self.syncEventStream, CFRunLoopGetMain(), kCFRunLoopDefaultMode);
FSEventStreamStart(self.syncEventStream);
回调函数
所有事件都会在回调函数中响应, 函数声明为
typedef CALLBACK_API_C( void , FSEventStreamCallback )(ConstFSEventStreamRef streamRef, void * __nullable clientCallBackInfo, size_t numEvents, void *eventPaths, const FSEventStreamEventFlags eventFlags[], const FSEventStreamEventId eventIds[]);
我们可以创建一个如下的函数用于接收事件
void fsevents_callback(ConstFSEventStreamRef streamRef,
void *userData,
size_t numEvents,
void *eventPaths,
const FSEventStreamEventFlags eventFlags[],
const FSEventStreamEventId eventIds[]) {
//code here
}
同样, 来看看参数列表
第一个参数 streamRef 就是create 时创建的句柄
第二个参数 userData 则是之前 context 中的 info, 我们之前传入了 self 进来, 那么 userData 就是 self 了
第三个参数是 eventPaths , 是一个数组, 内容是发生事件的文件路径. 默认情况下是一个 C 语言的数组, 不过我们可以在 create 的时候使用 kFSEventStreamCreateFlagUseCFTypes, 让其变为 CFArray
第四个参数 numEvents 为事件的个数
第五个参数eventFlags 是发生的事件, 注意这里有坑.
最后一个是每一个事件的事件 id
我们主要需要关注的参数就是 eventPaths 和 eventFlags, eventPaths 没什么好说的, 就是文件的路径.
eventFlags 则是事件类型. 可以用按位与获取具体的事件
如
if( eventFlags[0] & kFSEventStreamEventFlagItemCreated) {
// 发生了文件创建事件
}
但是坑来了, 如果你想监控文件移动, 那么你会想移动的事件应该是 xxxMoved, 实际上呢, 文件移动发生的事件是 kFSEventStreamEventFlagItemRenamed .
不过想想也对, 文件移动和重命名在命令行都是 mv 命令.
kFSEventStreamEventFlagItemRemoved, 这个事件, 你想通过他监控文件删除, 没问题, 不过当你在 Finder 中去删除一个文件到回收站, 你会发现还是一个 rename 事件, 只有用命令行直接删除文件才是 remove 事件.
另外, 移动和重命名的事件都是成对的, 也就是说移动或是重命名一个文件同时会发生两个事件, 都是 rename 事件, 这两个时间的事件 id 也是一样的.
写了一个 demo, 可以略作参考https://github.com/ywwzwb/FSEventStreamDemo
网友评论