【问题标题】:NSRunLoop returns early, before any FSEvent or timer firesNSRunLoop 在任何 FSEvent 或计时器触发之前提前返回
【发布时间】:2013-04-22 22:14:29
【问题描述】:

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

下面复制的是(我能理解的)最小实现 EventHandler 班级。这是从主函数调用的 分配和初始化一个实例,然后发送一个 startWatching"/tmp/fussybot-test" 的消息,最后是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:beforeDate RunLoop 对象上的消息不会阻塞, 但返回YES,即使此消息的文档说 它只返回YES“如果运行循环运行并处理了输入 源或如果达到指定的超时值”,两者都不是 这在上述pathsSeen=0 案例中是正确的。在每一个 我希望在pathsSeen=0 行之前看到 5 秒的延迟 出现,作为 RunLoop,没有看到任何 FSEvents,阻塞到末尾 waitEnd 区间。

这两个特点都表明我误解了一些东西 非常基础,大概是关于对象的生命周期。我觉得我可以 说明以下各项:

  • 我确实应该致电NSRunLoop runMode:beforeDate 程序的主线程(程序没有别的 等待时做,所以被阻止是正确的 事物)。这与 RunLoops 中的解释兼容 Threading Programming Guide
  • 每个线程只有一个 RunLoop,所以我正在调度 event_stream 在我等待的 RunLoop 上。
  • 我拥有event_stream,根据创建规则,所以这不是 在我背后回收。
  • waitEnd 每次循环都会不同——也就是说,它是 没有从一个通道保存到另一个通道。
  • createStream:runLoop 初始化一个instance 变量 pathsToWatch 表示我不必担心它会消失 在FSEventStreamCreate 用这个创建了流之后 争论。 ARC 管理层将在本年度结束时收回这一点 方法如果这是一个局部变量,但不是,因为它是一个 实例变量。
  • 没有其他事件会导致 RunLoop 解锁。即使操作系统确实在这个 RunLoop 上安排了一些东西 (文档似乎仔细地不排除这一点),我会 在回调中看到这样的事件。
  • 第一种情况下事件上的FSEventStreamEventFlags 是预期的- 没有任何迹象表明任何事件已被删除 某种原因。

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

FSEvent API 是否代表“基于端口的输入源”,在 “事件的运行循环序列”条款内 Threading Programming Guide? 如果是这样,那么肯定应该在该序列的第 7 步中收到 FSEvent。

上面的代码与示例代码密切相关 在File System Events API documentation。 我认为我的理解与this thoughtful answer 中的解释兼容,但是 我还没有找到许多其他相关的 RunLoop 问题。 SO 系统建议的问题主要与专门添加 NSTimers 而不是使用 RunLoop 调用的内置计时器有关。 This question on FSEvent and 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 上。

(这是一个很长的问题:抱歉。通常在您问 这么长的问题,您已经为自己找到了答案,但是 – 不 – 我现在和以前一样困惑。)

【问题讨论】:

    标签: objective-c cocoa nsthread fsevents


    【解决方案1】:

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

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

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

    【讨论】:

    • 谢谢,肯。我以为我已经排除了来自其他框架的活动,因为如果发生此类活动,那么我的回调会听到它。但是,该回调是由 FSEvents 事件流调用的真实情况,然后(单独)将一个事件传递给 RunLoop,除了允许 RunLoop 退出之外什么都不做?然而FSEvents docs 似乎说回调是“从他们的运行循环”接收的。可能没关系,但是...
    • 再一次...我已经修改了我的代码,以便它在默认模式下创建和安排一个计时器。不过,我仍然不确定是谁/在哪里/什么触发了我的回调,但我暂时先放过它。以您描述的方式共享运行循环是一个非常关键的信息,我很长时间都不会从 NSRunLoop 文档中获取(我倾向于向 Apple bugparade 报告文档错误) .同样有趣的是 NSRunLoop 的关键特性是它不是循环(它在循环中调用),并且它不运行(它阻塞)。谢谢指点。
    • 在 Apple bugparade 中作为 issue 13719849 添加(不是库比蒂诺以外的任何人都可以看到...)
    • 每个运行循环源都有一个单独的回调。仅当 FSEvents 源有活动时才会调用您的回调。其他来源(如果有的话)调用他们自己的回调,你显然不会看到。对于它的价值,我希望你的回调是从运行循环中调用的,具有一定的间接性。也就是说,您的回调不是自定义运行循环源的执行回调所需的形式,因此显然 FSEvents 源有一个内部回调通知它活动并使用适当的参数调用您的回调。
    猜你喜欢
    • 2014-11-05
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2010-11-22
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多