JSPatch中有個(gè)小工具瓶殃,PlaygroundTool射赛,編輯js文件后,可以實(shí)時(shí)在模擬器中看到修改后的結(jié)果论衍,所改即所見(jiàn)恒水。懷著對(duì)此種效果的實(shí)現(xiàn)方式極大的好奇,去看了下它的源碼饲齐。以下是盜圖效果钉凌。
原理分析
其實(shí),原理挺簡(jiǎn)單的捂人。監(jiān)聽(tīng)js文件的變化御雕,然后重新加載js文件即可
矢沿。那么,如何監(jiān)聽(tīng)文件的變化呢酸纲?噠噠噠捣鲸,主角來(lái)了,就是它闽坡,kqueue
栽惶,唔,好像沒(méi)聽(tīng)過(guò)疾嗅,??外厂。于是乎,搜索了一番代承。
kqueue
kqueue是FreeBSD上的一種的多路復(fù)用機(jī)制汁蝶,所以也能在OSX/iOS中使用。它是針對(duì)傳統(tǒng)的select處理大量的文件描述符性能較低效而開(kāi)發(fā)出來(lái)的论悴。同時(shí)也能檢測(cè)更多類(lèi)型事件掖棉,如文件修改,文件刪除膀估,子進(jìn)程操作等幔亥。
kevent
kqueue模型中最主要的函數(shù)是kevent。
int kevent(int kq,
const struct kevent *changelist,
int nchanges,
struct kevent *eventlist,
int nevents,
const struct timespec *timeout);
- kq:kqueue返回的描述符察纯。
- changelist:kevent結(jié)構(gòu)體數(shù)組紫谷,用于注冊(cè)或修改事件。
- nchanges:changeList長(zhǎng)度捐寥。
- eventlist:返回有事件發(fā)生的kevent數(shù)組笤昨。
- nevents:eventlist的最大長(zhǎng)度。
- timeout: 超時(shí)時(shí)間握恳。
還有個(gè)重要的結(jié)構(gòu)kevent瞒窒。
struct kevent {
uintptr_t ident;
short filter;
u_short flags;
u_int fflags;
intptr_t data;
void *udata;
};
- ident::事件id,一般為文件描述符乡洼。
- filter:內(nèi)核用于ident的過(guò)濾器崇裁。
- flags:告訴內(nèi)核對(duì)該事件完成哪些操作和處理哪些必要的標(biāo)志。
- fflags:內(nèi)核使用的特定于過(guò)濾器的標(biāo)志束昵。
- data:用于保存任何特定于過(guò)濾器的數(shù)據(jù)拔稳。
- udata:并不由kqueue使用,kqueue會(huì)把將它原封不動(dòng)的透?jìng)鳌?/li>
filter
kqueue過(guò)濾器filter
EVFILT_READ:用于檢測(cè)數(shù)據(jù)什么時(shí)候可讀锹雏。
EVFILT_WRITE:檢測(cè)數(shù)據(jù)什么時(shí)候可寫(xiě)巴比。
EVFILT_VNODE:檢測(cè)文件系統(tǒng)上一個(gè)文件的改動(dòng)。然后將fflags設(shè)置成所關(guān)心的事件,如NOTE_DELETE(文件被刪除)轻绞,NOTE_WRITE(文件被修改)采记,NOTE_ATTRIB(文件屬性被修改)等等。(這里使用的就是這個(gè)filter)
政勃。
flags
kqueue的標(biāo)志位flags
EV_ADD:向kqueue添加事件
EV_DELETE:刪除事件
EV_ENABLE:激活事件
EV_CLEAR:一旦從kqueue中獲取到該事件唧龄,就將事件狀態(tài)復(fù)位,否則會(huì)一直觸發(fā)奸远。
這樣我們就可以通過(guò)kqueue來(lái)監(jiān)聽(tīng)文件變化了既棺。
源碼分析
這里只分析playground的源碼實(shí)現(xiàn)部分。
1.首先懒叛,傳入文件路徑丸冕,生成fd。要知道文件路徑芍瑞,獲取工程路徑晨仑,是在info.plist中褐墅,添加了projectPath拆檬。這樣projectPath/js,就是js文件夾路徑妥凳。
- info.plist中添加配置項(xiàng)
<key>projectPath</key>
<string>$(SRCROOT)/$(TARGET_NAME)</string>
- 獲取projectPath
NSString *rootPath = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"projectPath"];;
- 生成fd
int dirFD = open([_filePath fileSystemRepresentation], O_EVTONLY);
if (dirFD < 0) return;
2.創(chuàng)建kqueue竟贯,kevent,添加到kqueue中逝钥。eventToAdd.flags的EV_CLEAR要添加屑那,否則callback會(huì)一直調(diào)用
。
// Create a new kernel event queue
int kq = kqueue();
if (kq < 0)
{
close(dirFD);
return;
}
// Set up a kevent to monitor
struct kevent eventToAdd; // Register an (ident, filter) pair with the kqueue
eventToAdd.ident = dirFD; // The object to watch (the directory FD)
eventToAdd.filter = EVFILT_VNODE; // Watch for certain events on the VNODE spec'd by ident
eventToAdd.flags = EV_ADD | EV_CLEAR; // Add a resetting kevent
eventToAdd.fflags = NOTE_WRITE; // The events to watch for on the VNODE spec'd by ident (writes)
eventToAdd.data = 0; // No filter-specific data
eventToAdd.udata = NULL; // No user data
// Add a kevent to monitor
if (kevent(kq, &eventToAdd, 1, NULL, 0, NULL)) {
close(kq);
close(dirFD);
return;
}
3.創(chuàng)建CFFileDescriptor艘款,設(shè)置回調(diào)函數(shù)KQCallback持际。在文件變化時(shí),在此回調(diào)中處理哗咆。注意蜘欲,這里將self(watchdog對(duì)象)傳到context中。為了在收到回調(diào)時(shí)晌柬,取出實(shí)例姥份,對(duì)比FileDescriptor是否一致。
// Wrap a CFFileDescriptor around a native FD
CFFileDescriptorContext context = {0, (__bridge void *)(self), NULL, NULL, NULL};
_kqRef = CFFileDescriptorCreate(NULL, // Use the default allocator
kq, // Wrap the kqueue
true, // Close the CFFileDescriptor if kq is invalidated
KQCallback, // Fxn to call on activity
&context); // Supply a context to set the callback's "info" argument
if (_kqRef == NULL) {
close(kq);
close(dirFD);
return;
}
4.創(chuàng)建runloop source年碘,并添加到當(dāng)前runloop中澈歉。注意最后一句CFFileDescriptorEnableCallBacks
很重要,否則會(huì)收不到回調(diào)屿衅。
CFRunLoopSourceRef rls = CFFileDescriptorCreateRunLoopSource(NULL, _kqRef, 0);
if (rls == NULL) {
CFRelease(_kqRef); _kqRef = NULL;
close(kq);
close(dirFD);
return;
}
CFRunLoopAddSource(CFRunLoopGetCurrent(), rls, kCFRunLoopDefaultMode);
CFRelease(rls);
// Store the directory FD for later closing
_dirFD = dirFD;
// Enable a one-shot (the only kind) callback
CFFileDescriptorEnableCallBacks(_kqRef, kCFFileDescriptorReadCallBack);
5.回調(diào)函數(shù)埃难,在判斷是所監(jiān)聽(tīng)的文件描述符。info是CFFileDescriptor創(chuàng)建時(shí),傳入的當(dāng)前SGDirWatchdog的實(shí)例凯砍,它保存了kqRef箱硕。
static void KQCallback(CFFileDescriptorRef kqRef, CFOptionFlags callBackTypes, void *info) {
// Pick up the object passed in the "info" member of the CFFileDescriptorContext passed to CFFileDescriptorCreate
SGDirWatchdog* obj = (__bridge SGDirWatchdog*) info;
if ([obj isKindOfClass:[SLDetector class]] && // If we can call back to the proper sort of object ...
(kqRef == obj.kqRef) && // and the FD that issued the CB is the expected one ...
(callBackTypes == kCFFileDescriptorReadCallBack) ) // and we're processing the proper sort of CB ...
{
[obj kqueueFired]; // Invoke the instance's CB handler
}
}
6.更新操作,通過(guò)kevent獲取當(dāng)前事件做判斷悟衩。CFFileDescriptorEnableCallBacks剧罩,這句同樣很重要,否則不會(huì)再次觸發(fā)
座泳。
- (void)kqueueFired {
// Pull the native FD around which the CFFileDescriptor was wrapped
int kq = CFFileDescriptorGetNativeDescriptor(_kqRef);
if (kq < 0) return;
// If we pull a single available event out of the queue, assume the directory was updated
struct kevent event;
struct timespec timeout = {0, 0};
if (kevent(kq, NULL, 0, &event, 1, &timeout) == 1 && _update) {
_update();
}
// (Re-)Enable a one-shot (the only kind) callback
CFFileDescriptorEnableCallBacks(_kqRef, kCFFileDescriptorReadCallBack);
}
經(jīng)過(guò)上述6步惠昔,就基本實(shí)現(xiàn)了所見(jiàn)即所得的功能。
但是我發(fā)現(xiàn)有點(diǎn)問(wèn)題挑势,不能直接監(jiān)聽(tīng)某個(gè)文件镇防,只能監(jiān)聽(tīng)到文件夾,該文件夾下的所有改動(dòng)都會(huì)觸發(fā)callback潮饱。
在源碼中来氧,JPPlayground.m中。代碼有去遍歷香拉,監(jiān)聽(tīng)文件夾下的文件啦扬,發(fā)現(xiàn)刪除這段代碼也可以。前提是需要監(jiān)聽(tīng)文件夾凫碌。
for (NSString *aPath in contentOfFolder) {
NSString * fullPath = [scriptRootPath stringByAppendingPathComponent:aPath];
BOOL isDir;
if ([[NSFileManager defaultManager] fileExistsAtPath:scriptRootPath isDirectory:&isDir]) {
[self watchFolder:fullPath mainScriptPath:mainScriptPath];
}
}
最后扑毡,問(wèn)題是,如何監(jiān)聽(tīng)一個(gè)文件盛险?
參考:
OSX/iOS中多路I/O復(fù)用總結(jié)
freebsd高級(jí)I/O,kevent的資料很詳細(xì)