我正在尝试使用 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:beforeDate
RunLoop 对象上的消息都不会阻塞,而是返回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 上安排了一些东西(文档似乎仔细地不排除这种情况),我也会在回调中看到这样的事件。
- 第一种情况下的
FSEventStreamEventFlags
on 事件是预期的——没有任何迹象表明任何事件已因某种原因被丢弃。
也就是说,我似乎已经证明这是行不通的。它显然不起作用,所以......我灾难性地未能得到什么?(当我得到它时,砰的一声,它会受伤吗?)。
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 上。
(这是一个很长的问题:对不起。通常当你问这么长的问题时,你已经自己想出了答案,但是——不——我现在和以前一样困惑。)