4

我正在尝试使用 NSRunLoop 来监视代理应用程序中的 FSEvents(即,没有任何 GUI)。我我了解 RunLoop 的工作原理,但我显然不了解,因为我看到的行为是难以理解的。我错过了什么?(我对几种语言的线程编程很满意,但是 Objective-C 对我来说有点新奇)。

下面复制的是一个 EventHandler类的(接近我能得到的)最小实现。这是通过分配和初始化一个实例从主函数调用的,然后使用 发送该 startWatching消息"/tmp/fussybot-test",然后使用 finally发送该消息tidyUp

下面的实现代码创建、调度和启动附加到默认 RunLoop 的事件流,然后循环,等待runMode:beforeDate 任何 FSEvents 或 RunLoop 的计时器到期。

#import "EventHandler.h"

void mycallback(ConstFSEventStreamRef streamRef,
                void *userData,
                size_t numEvents,
                void *eventPaths,
                const FSEventStreamEventFlags eventFlags[],
                const FSEventStreamEventId eventIds[])
{
    EventHandler *eh = (__bridge EventHandler*)userData;

    size_t i;
    char **paths = eventPaths;
    NSLog(@"callback: %zd events to process...", numEvents);
    for (i=0; i<numEvents; i++) {
        NSLog(@"Event %llu in %s (%x)", eventIds[i], paths[i], eventFlags[i]);
        [eh changedPath:paths[i]];
    }
}

@implementation EventHandler

-(void) startWatching: (NSString*) path
{
    NSRunLoop *theRL = [NSRunLoop currentRunLoop];
    [self createStream:path runLoop:theRL];

    BOOL recentFSActivity_p = YES;
    while (recentFSActivity_p) {

        NSDate* waitEnd = [NSDate dateWithTimeIntervalSinceNow:5.0];
        //NSLog(@"waiting until %@", waitEnd); // XXX
        if (! [theRL runMode:NSDefaultRunLoopMode
                  beforeDate:waitEnd]) {
            NSLog(@"the run loop could not be started");
        }

        int ps = [self pathsSeen];
        NSLog(@"Main loop: pathsSeen=%i", ps);
        if (ps == 0) {
            recentFSActivity_p = NO;
        }
    }
}

- (void) tidyUp
{
    FSEventStreamStop(event_stream);
    FSEventStreamInvalidate(event_stream);
    return;
}


- (FSEventStreamRef) createStream: (NSString*) path
                          runLoop: (NSRunLoop*) theRL
{
    pathsToWatch = [NSArray arrayWithObject:path];
    FSEventStreamContext context = {0, (__bridge void*)self, NULL, NULL, NULL};
    CFAbsoluteTime latency = 3.0; /* Latency in seconds */

    /* Create the stream, passing in a callback */
    event_stream = FSEventStreamCreate(NULL,
                                       &mycallback,
                                       &context,
                                       (__bridge CFArrayRef) pathsToWatch,
                                       kFSEventStreamEventIdSinceNow,
                                       latency,
                                       kFSEventStreamCreateFlagNone);

    FSEventStreamScheduleWithRunLoop(event_stream,
                                     [theRL getCFRunLoop],
                                     kCFRunLoopDefaultMode);
    FSEventStreamStart(event_stream);

    return event_stream;
}

-(void)changedPath:(char *)path
{
    NSLog(@"Path %s changed", path); // log that we got here
    nchangedPaths += 1;              // ...and count the number of calls
}

-(int)pathsSeen
{
    int n = nchangedPaths;      // return instance variable
    nchangedPaths = 0;          // ...and reset it
    return n;
}
@end

好的,所以我们构建它,开始它,然后触摸监视目录中的一个文件:

% make && ./fussybot & date '+NOW: %T'; sleep 2; echo hello >/tmp/fussybot-test/hello.txt
cc -c -x objective-c -fobjc-arc -o EventHandler.o EventHandler.m
cc -o fussybot main.o EventHandler.o -framework Cocoa
[1] 57431
NOW: 22:56:54
% 2013-04-22 22:56:57.692 fussybot[57431:707] callback: 1 events to process...
2013-04-22 22:56:57.694 fussybot[57431:707] Event 645428112 in /private/tmp/fussybot-test/ (11400)
2013-04-22 22:56:57.694 fussybot[57431:707] Path /private/tmp/fussybot-test/ changed
2013-04-22 22:56:57.695 fussybot[57431:707] Main loop: pathsSeen=1
2013-04-22 22:56:57.695 fussybot[57431:707] Main loop: pathsSeen=0
Exiting...

[1]  + done       ./fussybot
% 

然后我们取消注释该行NSLog(@"waiting until %@", waitEnd);XXX上面标有),我们再试一次:

% make && ./fussybot & date '+NOW: %T'; sleep 2; echo hello >/tmp/fussybot-test/hello.txt
cc -c -x objective-c -fobjc-arc -o EventHandler.o EventHandler.m
cc -o fussybot main.o EventHandler.o -framework Cocoa
[1] 57474
NOW: 22:59:01
2013-04-22 22:59:01.190 fussybot[57474:707] waiting until 2013-04-22 21:59:06 +0000
2013-04-22 22:59:01.190 fussybot[57474:707] Main loop: pathsSeen=0
Exiting...
[1]  + done       ./fussybot
% 

现在这里有件非常奇怪的事情。

  • 首先,添加NSLog调用会改变程序的行为。诶?!
  • 其次,在这两个示例中,RunLoop 似乎立即退出,而无需等待 FSEvent。

关于第一个,NSLog有这样的效果的事实肯定告诉我一些非常重要的事情,但我一生无法弄清楚什么。

关于第二种情况,在每种pathsSeen=0情况下, runMode:beforeDateRunLoop 对象上的消息都不会阻塞,而是返回YES,尽管该消息的文档说它YES仅返回“如果运行循环运行并处理了输入源,或者如果指定的已达到超时值”,在上述情况下都不是真的pathsSeen=0。在每种情况下,我都希望在该pathsSeen=0行出现之前看到 5 秒的延迟,因为没有看到任何 FSEvent 的 RunLoop 会阻塞到waitEnd间隔的末尾。

这两个特点都表明我误解了一些非常基本的东西,大概是关于对象生命周期的。我想我可以陈述以下每一项:

  • 我确实应该调用NSRunLoop runMode:beforeDate程序的主线程(程序在等待时没有其他事情可做,所以被阻塞是完全正确的事情)。这与Threading Programming Guide中对 RunLoops 的解释兼容。
  • 每个线程只有一个 RunLoop,所以我event_stream等待的 RunLoop 上进行调度 。
  • 根据创建规则,我拥有event_stream,因此不会在我背后收回。
  • waitEnd每次循环都会不同 - 即,它不会在每次循环中保留。
  • createStream:runLoop初始化实例变量 pathsToWatch意味着我不必担心在使用此参数创建流后它会FSEventStreamCreate消失。如果这是一个局部变量,ARC 管理将在方法结束时回收它,但不是,因为它是一个实例变量。
  • 没有其他事件会导致 RunLoop 解除阻塞。即使操作系统确实在这个 RunLoop 上安排了一些东西(文档似乎仔细地不排除这种情况),我也会在回调中看到这样的事件。
  • 第一种情况下的FSEventStreamEventFlagson 事件是预期的——没有任何迹象表明任何事件已因某种原因被丢弃。

也就是说,我似乎已经证明这是行不通的。它显然不起作用,所以......我灾难性地未能得到什么?(当我得到它时,砰的一声,它会受伤吗?)。

FSEvent API 是否代表“基于端口的输入源”,根据线程编程指南中的“事件的运行循环序列” ?如果是这样,那么肯定应该在该序列的第 7 步中接收到 FSEvent。

上面的代码非常基于文件系统事件 API 文档中的示例代码。我认为我的理解与这个深思熟虑的答案中的解释兼容,但我一直无法找到许多其他相关的 RunLoop 问题。SO 系统建议的问题主要与专门添加 NSTimers 而不是使用 RunLoop 调用的内置计时器有关。 这个关于 FSEvent 和 Dropbox的问题看起来很可能,但是 (a) 没有答案,并且 (b) 可能是与 Dropbox 的交互。

这是

% cc --version 
Apple clang version 4.0 (tags/Apple/clang-421.0.60) (based on LLVM 3.1svn)

在 OS X 10.8.3 上。

(这是一个很长的问题:对不起。通常当你问这么长的问题时,你已经自己想出了答案,但是——不—​​—我现在和以前一样困惑。)

4

1 回答 1

3

运行循环是共享资源。框架可以并且确实在运行循环上安排自己的运行循环源,尤其是在默认模式下。如果-runMode:beforeDate:正在返回YES并且它没有处理您的源之一,那么它可能处理了框架安排的一个。

如果您想以只有您的源和计时器触发的方式运行运行循环,那么您需要在自定义模式下安排您的源和计时器并在该模式下运行运行循环。模式实际上只是一个字符串,所以使用类似@"com.yourcompany.yourproduct.yourmodename"或类似的东西保证是唯一的,你会没事的。

或者您可以简单地编写代码来应对并非所有在运行循环上触发的源都属于您的事实。如果您想检测超时到期,请安排一个计时器来设置标志并停止运行循环。继续循环直到设置标志。我认为您可以使用CFRunLoopStop()您的计时器方法强制-runMode:beforeDate:返回,但如果没有,您可以使用其中-performSelector...一种需要线程或延迟来执行此操作的方法。

于 2013-04-23T00:03:04.493 回答