JSPatch中有个小工具,PlaygroundTool,编辑js文件后,可以实时在模拟器中看到修改后的结果,所改即所见。怀着对此种效果的实现方式极大的好奇,去看了下它的源码。以下是盗图效果。
原理分析
其实,原理挺简单的。监听js文件的变化,然后重新加载js文件即可
。那么,如何监听文件的变化呢?哒哒哒,主角来了,就是它,kqueue
,唔,好像没听过,😭。于是乎,搜索了一番。
kqueue
kqueue是FreeBSD上的一种的多路复用机制,所以也能在OSX/iOS中使用。它是针对传统的select处理大量的文件描述符性能较低效而开发出来的。同时也能检测更多类型事件,如文件修改,文件删除,子进程操作等。
kevent
kqueue模型中最主要的函数是kevent。
int kevent(int kq,
const struct kevent *changelist,
int nchanges,
struct kevent *eventlist,
int nevents,
const struct timespec *timeout);
- kq:kqueue返回的描述符。
- changelist:kevent结构体数组,用于注册或修改事件。
- nchanges:changeList长度。
- eventlist:返回有事件发生的kevent数组。
- nevents:eventlist的最大长度。
- timeout: 超时时间。
还有个重要的结构kevent。
struct kevent {
uintptr_t ident;
short filter;
u_short flags;
u_int fflags;
intptr_t data;
void *udata;
};
- ident::事件id,一般为文件描述符。
- filter:内核用于ident的过滤器。
- flags:告诉内核对该事件完成哪些操作和处理哪些必要的标志。
- fflags:内核使用的特定于过滤器的标志。
- data:用于保存任何特定于过滤器的数据。
- udata:并不由kqueue使用,kqueue会把将它原封不动的透传。
filter
kqueue过滤器filter
EVFILT_READ:用于检测数据什么时候可读。
EVFILT_WRITE:检测数据什么时候可写。
EVFILT_VNODE:检测文件系统上一个文件的改动。然后将fflags设置成所关心的事件,如NOTE_DELETE(文件被删除),NOTE_WRITE(文件被修改),NOTE_ATTRIB(文件属性被修改)等等。(这里使用的就是这个filter)
。
flags
kqueue的标志位flags
EV_ADD:向kqueue添加事件
EV_DELETE:删除事件
EV_ENABLE:激活事件
EV_CLEAR:一旦从kqueue中获取到该事件,就将事件状态复位,否则会一直触发。
这样我们就可以通过kqueue来监听文件变化了。
源码分析
这里只分析playground的源码实现部分。
1.首先,传入文件路径,生成fd。要知道文件路径,获取工程路径,是在info.plist中,添加了projectPath。这样projectPath/js,就是js文件夹路径。
- info.plist中添加配置项
<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.创建kqueue,kevent,添加到kqueue中。eventToAdd.flags的EV_CLEAR要添加,否则callback会一直调用
。
// 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.创建CFFileDescriptor,设置回调函数KQCallback。在文件变化时,在此回调中处理。注意,这里将self(watchdog对象)传到context中。为了在收到回调时,取出实例,对比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.创建runloop source,并添加到当前runloop中。注意最后一句CFFileDescriptorEnableCallBacks
很重要,否则会收不到回调。
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.回调函数,在判断是所监听的文件描述符。info是CFFileDescriptor创建时,传入的当前SGDirWatchdog的实例,它保存了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.更新操作,通过kevent获取当前事件做判断。CFFileDescriptorEnableCallBacks,这句同样很重要,否则不会再次触发
。
- (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);
}
经过上述6步,就基本实现了所见即所得的功能。
但是我发现有点问题,不能直接监听某个文件,只能监听到文件夹,该文件夹下的所有改动都会触发callback。
在源码中,JPPlayground.m中。代码有去遍历,监听文件夹下的文件,发现删除这段代码也可以。前提是需要监听文件夹。
for (NSString *aPath in contentOfFolder) {
NSString * fullPath = [scriptRootPath stringByAppendingPathComponent:aPath];
BOOL isDir;
if ([[NSFileManager defaultManager] fileExistsAtPath:scriptRootPath isDirectory:&isDir]) {
[self watchFolder:fullPath mainScriptPath:mainScriptPath];
}
}
最后,问题是,如何监听一个文件?