前言
之前寫(xiě)了一個(gè) Mac 下的文件同步客戶端, 需要監(jiān)控本地的文件變化并同步上去. 幾經(jīng)波折, 最有使用的是FSEventStream 來(lái)實(shí)現(xiàn)文件監(jiān)控
簡(jiǎn)介
FSEventStream 是一套 C 語(yǔ)言的方法, 類似于 CoreGraphic.
由于是 C 語(yǔ)言方法, 建議使用 Objective-C 來(lái)編寫(xiě)代碼 ,用 swift 會(huì)涉及到大量類型轉(zhuǎn)換
用到的函數(shù)有下面幾個(gè):
- FSEventStreamCreate
創(chuàng)建一個(gè)文件監(jiān)控句柄, 可以在這個(gè)函數(shù)中綁定一個(gè)函數(shù)回調(diào) - FSEventStreamRetain
- FSEventStreamRelease
FSEventStream不支持 ARC, 必須手動(dòng) retain 和 release - FSEventStreamScheduleWithRunLoop
加入到一個(gè) runloop 中, 才可以實(shí)現(xiàn)文件的監(jiān)控 - FSEventStreamStart
啟動(dòng)文件監(jiān)控 - FSEventStreamStop
停止文件監(jiān)控 - FSEventStreamInvalidate
從 runloop 中移除
整體流程就是創(chuàng)建文件監(jiān)控句柄, 加入到 runloop, 最后啟動(dòng)就可以了
停止的時(shí)候, 首先調(diào)用 stop, 然后 從 runloop 中移除, 最后使用 release 釋放
FSEventStreamCreate
這個(gè)函數(shù)負(fù)責(zé)創(chuàng)建一個(gè)文件監(jiān)控句柄, 函數(shù)聲明如下
extern FSEventStreamRef __nullable
FSEventStreamCreate(
CFAllocatorRef __nullable allocator,
FSEventStreamCallback callback,
FSEventStreamContext * __nullable context,
CFArrayRef pathsToWatch,
FSEventStreamEventId sinceWhen,
CFTimeInterval latency,
FSEventStreamCreateFlags flags)
第一個(gè)參數(shù) allocator 傳入 NULL 就可以了;
第二個(gè)參數(shù) callback 為事件回調(diào), 當(dāng)監(jiān)控的文件夾發(fā)生事件之后, 會(huì)出發(fā)回調(diào)函數(shù);
第三參數(shù)context, 由于回調(diào)函數(shù)是 C 函數(shù), 無(wú)法直接使用 self , 如果需要使用 self, 可以利用這個(gè)參數(shù)將 self 傳進(jìn)去.
FSEventStreamContext context;
context.info = (__bridge void * _Nullable)(self);
context.version = 0;
context.retain = NULL;
context.release = NULL;
context.copyDescription = NULL;
第四個(gè)參數(shù) pathsToWatch 為需要監(jiān)控的文件夾路徑數(shù)組, 也就是說(shuō)你可以同時(shí)監(jiān)控多個(gè)文件夾.
第五個(gè)參數(shù) sinceWhen 很有用, 可以指定從哪個(gè)事件開(kāi)始監(jiān)控, 如果是第一次監(jiān)控, 那么可以設(shè)置為kFSEventStreamEventIdSinceNow, 表示從現(xiàn)在開(kāi)始監(jiān)控, 后面如果發(fā)生事件之后, 回調(diào)函數(shù)中會(huì)傳入最新的事件 id, 可以將這個(gè)存下來(lái), 以后就以這個(gè)事件 id 作為起點(diǎn). 這樣可以做到即使你程序關(guān)閉了, 你也不會(huì)漏掉事件
第六個(gè)參數(shù) latency 是監(jiān)控的時(shí)間間隔, 可以指定多少秒之后監(jiān)控一次
最后一個(gè)參數(shù) flags 用于配置文件監(jiān)控, 具體可以參考文檔, 我一般使用kFSEventStreamCreateFlagFileEvents 和 kFSEventStreamCreateFlagUseCFTypes
前者可以將事件細(xì)分到具體的文件, 后者則是方便回調(diào)函數(shù)使用
完整的函數(shù)調(diào)用示例如下
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);
回調(diào)函數(shù)
所有事件都會(huì)在回調(diào)函數(shù)中響應(yīng), 函數(shù)聲明為
typedef CALLBACK_API_C( void , FSEventStreamCallback )(ConstFSEventStreamRef streamRef, void * __nullable clientCallBackInfo, size_t numEvents, void *eventPaths, const FSEventStreamEventFlags eventFlags[], const FSEventStreamEventId eventIds[]);
我們可以創(chuàng)建一個(gè)如下的函數(shù)用于接收事件
void fsevents_callback(ConstFSEventStreamRef streamRef,
void *userData,
size_t numEvents,
void *eventPaths,
const FSEventStreamEventFlags eventFlags[],
const FSEventStreamEventId eventIds[]) {
//code here
}
同樣, 來(lái)看看參數(shù)列表
第一個(gè)參數(shù) streamRef 就是create 時(shí)創(chuàng)建的句柄
第二個(gè)參數(shù) userData 則是之前 context 中的 info, 我們之前傳入了 self 進(jìn)來(lái), 那么 userData 就是 self 了
第三個(gè)參數(shù)是 eventPaths , 是一個(gè)數(shù)組, 內(nèi)容是發(fā)生事件的文件路徑. 默認(rèn)情況下是一個(gè) C 語(yǔ)言的數(shù)組, 不過(guò)我們可以在 create 的時(shí)候使用 kFSEventStreamCreateFlagUseCFTypes, 讓其變?yōu)?CFArray
第四個(gè)參數(shù) numEvents 為事件的個(gè)數(shù)
第五個(gè)參數(shù)eventFlags 是發(fā)生的事件, 注意這里有坑.
最后一個(gè)是每一個(gè)事件的事件 id
我們主要需要關(guān)注的參數(shù)就是 eventPaths 和 eventFlags, eventPaths 沒(méi)什么好說(shuō)的, 就是文件的路徑.
eventFlags 則是事件類型. 可以用按位與獲取具體的事件
如
if( eventFlags[0] & kFSEventStreamEventFlagItemCreated) {
// 發(fā)生了文件創(chuàng)建事件
}
但是坑來(lái)了, 如果你想監(jiān)控文件移動(dòng), 那么你會(huì)想移動(dòng)的事件應(yīng)該是 xxxMoved, 實(shí)際上呢, 文件移動(dòng)發(fā)生的事件是 kFSEventStreamEventFlagItemRenamed .
不過(guò)想想也對(duì), 文件移動(dòng)和重命名在命令行都是 mv 命令.
kFSEventStreamEventFlagItemRemoved, 這個(gè)事件, 你想通過(guò)他監(jiān)控文件刪除, 沒(méi)問(wèn)題, 不過(guò)當(dāng)你在 Finder 中去刪除一個(gè)文件到回收站, 你會(huì)發(fā)現(xiàn)還是一個(gè) rename 事件, 只有用命令行直接刪除文件才是 remove 事件.
另外, 移動(dòng)和重命名的事件都是成對(duì)的, 也就是說(shuō)移動(dòng)或是重命名一個(gè)文件同時(shí)會(huì)發(fā)生兩個(gè)事件, 都是 rename 事件, 這兩個(gè)時(shí)間的事件 id 也是一樣的.
寫(xiě)了一個(gè) demo, 可以略作參考https://github.com/ywwzwb/FSEventStreamDemo