使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秒。
任何人都知道为什么原始代码会表现出这种行为吗?
解决方案
Runloop可能只是发生事情的魔盒。
基本上,我们是在告诉Runloop处理一些事件,然后返回。如果超时之前未处理任何事件,则返回OR。
有了0.1秒的超时,我们会更经常地超时。运行循环将触发,不处理任何事件,并在0.1秒内返回。偶尔会有机会处理事件。
随着distantFuture超时,runloop将永远等待直到它处理一个事件。因此,当它返回给我们时,它刚刚处理了某种事件。
较短的超时值将比无限超时消耗更多的CPU,但是使用较短的超时有充分的理由,例如,如果我们想终止运行runloop的进程/线程。我们可能希望runloop注意到标志已更改,需要尽快纾困。
我们可能需要与runloop观察者一起玩耍,以便可以确切地看到runloop在做什么。
有关更多信息,请参阅此Apple文档。
我在尝试管理NSRunLoops时遇到了类似的问题。在类引用页面上对runMode:beforeDate:
的讨论说:
If no input sources or timers are attached to the run loop, this method exits immediately; otherwise, it returns after either the first input source is processed or limitDate is reached. Manually removing all known input sources and timers from the run loop is not a guarantee that the run loop will exit. Mac OS X may install and remove additional input sources as needed to process requests targeted at the receiver’s thread. Those sources could therefore prevent the run loop from exiting.
我最好的猜测是,输入源可能是通过OS X本身连接到NSRunLoop上的,而runMode:beforeDate:一直阻塞,直到该输入源已经处理了一些输入或者被删除了。在情况下,这需要"几秒钟到10秒"的时间,此时," runMode:beforeDate:"将返回一个布尔值," while()"将再次运行,它将检测到" shouldKeepRunning"已设置为" NO",循环将终止。
经过优化,runMode:beforeDate:
将在0.1秒内返回,无论它是否已添加输入源或者是否已处理任何输入。这是有根据的猜测(我不是运行循环内部的专家),但是认为改进是处理这种情况的正确方法。
如果我们希望能够设置标志变量并立即注意到运行循环,只需使用-[NSRunLoop performSelector:target:argument:order:modes:`来要求运行循环调用将标志设置为的方法错误的。这将导致运行循环立即旋转,调用该方法,然后将检查该标志。
在代码中,当前线程将每0.1秒检查一次变量是否已更改。在Apple代码示例中,更改变量不会有任何效果。运行循环将一直运行,直到处理某些事件为止。如果webViewIsLoading的值已更改,则不会自动生成任何事件,因此它将停留在循环中,为什么会中断呢?它将停留在那里,直到需要处理其他事件,然后它才会爆发。这可能会在1、3、5、10甚至20秒内发生。并且直到发生这种情况,它才会脱离运行循环,因此不会注意到此变量已更改。 IOW我们引用的Apple代码是不确定的。仅当webViewIsLoading的值更改还创建了一个导致Runloop唤醒的事件且似乎并非如此(或者至少并非总是如此)时,此示例才有效。
我认为我们应该重新考虑问题。由于变量名为webViewIsLoading,我们是否等待网页加载?我们是否正在使用Webkit?我怀疑我们根本不需要这样的变量,也不需要我们发布的任何代码。相反,我们应该异步编写应用程序代码。我们应该启动"网页加载过程",然后返回主循环,页面加载完成后,我们应该异步发布在主线程中处理的通知,并运行应尽快运行的代码。加载已完成。
好的,我向我们解释了问题,这是一个可能的解决方案:
@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
start将显示进度指示器,并在内部运行循环中捕获主线程。它会一直保留在那里,直到另一个线程宣布完成为止。要唤醒主线程,它将使它仅出于唤醒主线程的目的而处理函数。
这只是我们可以做到的一种方式。在主线程上发布和处理通知可能是更好的选择(其他线程也可以注册它),但是上述解决方案是我能想到的最简单的解决方案。顺便说一句,它不是真正的线程安全的。为了真正做到线程安全,每个布尔对象的访问都需要由任一线程的NSLock对象锁定(使用这种锁定也会使" volatile"过时,因为根据POSIX标准,受锁保护的变量是隐式的volatile;但是C标准并不了解锁,因此这里只有volatile才能保证此代码正常工作; GCC不需要为受锁保护的变量设置volatile)。
通常,如果我们自己循环处理事件,那么我们做错了。根据我的经验,它可能会导致很多混乱的问题。
如果要模态运行-例如,显示进度面板-模态运行!继续并使用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]; }
这样就完成了。无需保持对运行循环的控制!
-会