JSPatch中Playground的实现

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);
  • 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];
        }
    }

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

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,732评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,496评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,264评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,807评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,806评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,675评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,029评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,683评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,704评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,666评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,773评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,413评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,016评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,978评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,204评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,083评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,503评论 2 343

推荐阅读更多精彩内容

  • 名称 libev - 一个 C 编写的功能全面的高性能事件循环。 概要 示例程序 关于 libev Libev 是...
    hanpfei阅读 15,153评论 0 5
  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,596评论 18 139
  • Spring Boot 参考指南 介绍 转载自:https://www.gitbook.com/book/qbgb...
    毛宇鹏阅读 46,733评论 6 342
  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,448评论 25 707
  • ❲追根究底❳Libevent内部实现原理初探 Libevent确实方便了开发人员,对于定时器、信号处理、关心的文件...
    meng_philip123阅读 4,563评论 0 4