让NSRunLoop等待标志被设置的最佳方式?

问题描述:

NSRunLoop的Apple文档中,有示例代码演示暂停执行,同时等待某个标志由其他设置。让NSRunLoop等待标志被设置的最佳方式?

BOOL shouldKeepRunning = YES;  // global 
NSRunLoop *theRL = [NSRunLoop currentRunLoop]; 
while (shouldKeepRunning && [theRL runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]]); 

我一直在使用这个,它的工作原理,但在调查性能问题,我跟踪到这段代码。我使用几乎完全相同的代码(只是国旗的名称是不同的:),如果我在设置标志(在另一种方法中)之后在线上放置NSLog,然后在while()之后有一行看似在几秒钟的两个日志语句之间随机等待。

延迟在较慢或较快的机器上似乎并不不同,但从运行到运行至少有几秒钟和多达10秒不等。

我已经用下面的代码解决了这个问题,但原始代码不起作用似乎不正确。

NSDate *loopUntil = [NSDate dateWithTimeIntervalSinceNow:0.1]; 
while (webViewIsLoading && [[NSRunLoop currentRunLoop] runMode: NSDefaultRunLoopMode beforeDate:loopUntil]) 
    loopUntil = [NSDate dateWithTimeIntervalSinceNow:0.1]; 

使用此代码时,设置标志和while循环后的日志语句现在始终小于0.1秒。

任何人有任何想法,为什么原代码表现出这种行为?

+0

我想你是误会文档中给出的示例;这个示例代码的用法是当你想终止一个runloop的执行时(不要得到关于标志变化的通知)https://developer.apple.com/library/mac/documentation/Cocoa/Reference/Foundation/Classes/ NSRunLoop_Class/Reference/Reference.html#// apple_ref/occ/instm/NSRunLoop/run – kernix 2014-04-18 20:06:40

Runloops可以是一个魔术盒,只是发生的东西。

基本上你要告诉runloop去处理一些事件然后返回。如果在超时命中之前没有处理任何事件,则返回。

在0.1秒的超时时间内,你经常会发现超时。 runloop触发,不处理任何事件并在0.1秒内返回。偶尔会有机会处理一个事件。

随着你遥远的未来超时,runloop将等待,直到它处理一个事件。所以当它回到你身上时,它只是处理了某种事件。

短暂超时值会比无限超时消耗更多的CPU,但是使用短暂超时有很好的理由,例如,如果要终止runloop运行的进程/线程,您可能需要runloop注意到一个标志已经改变,它需要尽快救助。

您可能想要与runloop观察员一起玩,以便您可以看到runloop正在做什么。

查看this Apple doc了解更多信息。

我在尝试管理NSRunLoops时遇到过类似的问题。该discussionrunMode:beforeDate:类引用页面上说:

如果没有输入源或定时器连接到运行循环,此方法立即退出;否则,在处理第一个输入源或达到limitDate后返回。手动从运行循环中删除所有已知的输入源和定时器并不能保证运行循环将退出。 Mac OS X可以根据需要安装和删除额外的输入源,以处理针对接收方线程的请求。这些来源因此可以防止运行循环退出。

我最好的猜测是,一个输入源连接到您的NSRunLoop,也许OS X一样,这runMode:beforeDate:阻止,直到输入源或者有一定的输入处理,或者被删除。在你的情况下,它是“几秒钟,最多10秒”发生这种情况,此时runMode:beforeDate:将返回一个布尔值,while()将再次运行,它会检测到shouldKeepRunning已被设置为NO,并且循环会终止。

随着您的改进,runMode:beforeDate:将在0.1秒内返回,无论它是否已连接输入源或已处理任何输入。这是一个有教养的猜测(我不是运行循环内部的专家),但认为你的优化是处理这种情况的正确方法。

如果您希望能够设置标志变量并立即注意到运行循环,只需使用-[NSRunLoop performSelector:target:argument:order:modes:就可以让运行循环调用将标志设置为false的方法。这将导致你的运行循环立即旋转,被调用的方法,然后该标志将被检查。

+1

感谢Chris,不幸的是,由于该标志是在WebView的委托方法中设置的,因此我不能立即调用它。 – 2008-10-07 05:11:05

在您的代码中,当前线程将检查每0.1秒更改一次的变量。在Apple代码示例中,更改变量不会产生任何影响。 runloop将运行直到它处理一些事件。如果webViewIsLoading的值已更改,则不会自动生成任何事件,因此它将保留在循环中,为什​​么会跳出它?它会留在那里,直到它得到一些其他的事件来处理,然后它会打破它。这可能发生在1,3,5,10甚至20秒内。在这种情况发生之前,它不会跳出runloop,因此它不会注意到这个变量已经改变。 IOW你引用的Apple代码是不确定的。这个例子只有在webViewIsLoading的值更改也会创建一个导致runloop醒来的事件并且这似乎不是这种情况(或者至少并非总是)时才起作用。

我认为你应该重新考虑这个问题。由于您的变量名为webViewIsLoading,您是否等待加载网页?你使用的是Webkit吗?我怀疑你根本就不需要这样的变量,也没有你发布的任何代码。相反,你应该编写你的应用程序异步。您应该启动“网页加载过程”,然后返回到主循环,并且一旦页面完成加载,您应该异步发布在主线程中处理的通知,并运行尽快运行的代码加载已完成。

+1

这是一个很好的解释,为什么RunLoop没有爆发,谢谢。有一个很好的理由,为什么WebView加载需要模态,但我需要保持这样。 – 2008-10-07 05:10:21

好吧,我解释你的问题,这里有一个可能的解决方案:

@implementation MyWindowController 

volatile BOOL pageStillLoading; 

- (void) runInBackground:(id)arg 
{ 
    NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init]; 

    // Simmulate web page loading 
    sleep(5); 

    // This will not wake up the runloop on main thread! 
    pageStillLoading = NO; 

    // Wake up the main thread from the runloop 
    [self performSelectorOnMainThread:@selector(wakeUpMainThreadRunloop:) withObject:nil waitUntilDone:NO]; 

    [pool release]; 
} 


- (void) wakeUpMainThreadRunloop:(id)arg 
{ 
    // This method is executed on main thread! 
    // It doesn't need to do anything actually, just having it run will 
    // make sure the main thread stops running the runloop 
} 


- (IBAction)start:(id)sender 
{ 
    pageStillLoading = YES; 
    [NSThread detachNewThreadSelector:@selector(runInBackground:) toTarget:self withObject:nil]; 
    [progress setHidden:NO]; 
    while (pageStillLoading) { 
     [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]]; 
    } 
    [progress setHidden:YES]; 
} 

@end 

开始显示进度指示器和捕捉在内部runloop主线程。它会留在那里,直到其他线程宣布它完成。要唤醒主线程,除了唤醒主线程之外,它将使其无需处理任何功能。

这只是你如何做到的一种方式。在主线程上发布和处理通知可能更可取(其他线程也可以注册),但上述解决方案是我能想到的最简单的解决方案。顺便说一句,它并不是真的线程安全的。要真正成为线程安全的,每个对布尔的访问都需要被任一线程的NSLock对象锁定(使用这样的锁也会使“volatile”过时,因为受锁保护的变量根据POSIX标准是隐式的volatile; C标准,但不知道锁,所以在这里只有volatile才能保证这段代码正常工作; GCC不需要为被lock保护的变量设置volatile。

+0

非常酷。这增加了一个新的工具,我objc武库。考虑将您的技术添加到[此线程] [1] [1]:http://*.com/questions/155964/what-are-best-practices-that-you-use-when-writing-objective- c-and-cocoa#156343 – schwa 2008-10-07 14:54:43

一般来说,如果你自己在一个循环中处理事件,你就是在做错了。根据我的经验,它可能会导致大量杂乱的问题。

如果你想模态运行 - 例如,显示进度面板 - 模态运行!继续使用NSApplication方法,以模态方式运行进度表,然后在加载完成后停止模式。请参阅Apple文档,例如http://developer.apple.com/documentation/Cocoa/Conceptual/WinPanel/Concepts/UsingModalWindows.html

如果您只是希望视图能够在您的加载期间内运行,但您不希望它是模态的(例如,您希望其他视图能够响应事件),那么您应该更简单一些。例如,你可以这样做:

- (IBAction)start:(id)sender 
{ 
    pageStillLoading = YES; 
    [NSThread detachNewThreadSelector:@selector(runInBackground:) toTarget:self withObject:nil]; 
    [progress setHidden:NO]; 
} 

- (void)wakeUpMainThreadRunloop:(id)arg 
{ 
    [progress setHidden:YES]; 
} 

你就完成了。无需保持运行循环的控制!

-Wil

你的第二个例子,是解决你轮询的时间间隔内0.1检查运行循环的输入。

偶尔我找到你的第一个例子中的解决方案:

BOOL shouldKeepRunning = YES;  // global 
NSRunLoop *theRL = [NSRunLoop currentRunLoop]; 
while (shouldKeepRunning && [theRL runMode:NSRunLoopCommonModes beforeDate:[NSDate distantFuture]]);