summer的ios小记iOS Developer首页投稿(暂停使用,暂停投稿)

JSPatch中Playground的实现

2016-10-28  本文已影响160人  微微笑的蜗牛

JSPatch中有个小工具,PlaygroundTool,编辑js文件后,可以实时在模拟器中看到修改后的结果,所改即所见。怀着对此种效果的实现方式极大的好奇,去看了下它的源码。以下是盗图效果。

Screenshot.gif

原理分析

其实,原理挺简单的。监听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);

还有个重要的结构kevent。

struct kevent {
  uintptr_t       ident;
  short           filter;
  u_short         flags;
  u_int           fflags;
  intptr_t        data;
  void            *udata;
};
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文件夹路径。

<key>projectPath</key>
<string>$(SRCROOT)/$(TARGET_NAME)</string>
 NSString *rootPath = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"projectPath"];;
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];
        }
    }

最后,问题是,如何监听一个文件?

参考:
OSX/iOS中多路I/O复用总结
freebsd高级I/O,kevent的资料很详细

上一篇下一篇

猜你喜欢

热点阅读