我最近遇到一个问题,延迟选择器没有启动(NSTimer和用performSelector:withObject:afterDelay
调用的方法)。
我读过苹果的文档,其中确实提到了特殊考虑领域,
此方法向其当前上下文的运行循环注册,并依赖于定期运行的运行循环才能正确执行。一个常见的上下文是,当调度队列调用时,您可能会调用此方法并最终注册到不是定期自动运行的运行循环。如果在调度队列上运行时需要这种类型的功能,则应该使用dispatch_after和相关方法来获得所需的行为。
除了当前上下文部分的运行循环之外,这是完全合理的。我发现自己对它到底要去哪个运行循环感到困惑。是线程的主运行循环处理所有事件,还是在我们不知情的情况下是另一个?
例如,如果在被调用为CoreAnimation完成块的块内调用performSelector之前遇到断点,则调试器会显示执行在主线程上。但是,调用performSelector:withObject:afterDelay
实际上从未运行选择器。这让我认为该调用实际上是在向与CoreAnimation框架相关联的运行循环注册,因此无论在主线程上执行performSelector
调用,如果CoreAnimity不轮询其运行循环,该操作都不会执行。
用performSelectorOnMainThread:WithObject:waitUntilDone
替换该块中的这个调用可以解决问题,但我很难说服同事这是根本原因。
更新:我能够将问题的根源追溯到UIScrollViewDelegate回调。当调用UI委托回调时,主运行循环将处于UITrackingRunLoopMode,这是有道理的。但在这一点上,处理程序将在后台队列上对一个块进行排队,然后执行将跳过其他几个队列,最终返回到主运行循环。问题是,当它返回到主运行循环时,它仍然处于UITrackingRunLoopMode。我认为当委托方法完成时,主运行循环应该退出UITracking模式,但当执行返回到主运行循环时,它仍然处于该模式。推迟从UIScrollViewDelegate方法启动作业后台排队的代码可以解决问题,例如[self performSelector:@selector(sendTaskToBackQueue) withObject:nil afterDelay:0 inModes:@[NSDefaultRunLoopMode]]
。后台任务排队返回主线程时使用的运行循环模式是否可能取决于运行循环在后台任务排队时所处的模式
从本质上讲,唯一的变化就是从这个开始。。。
- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate {
// Currently in UITrackingRunLoopMode
dispatch_async(someGlobalQueue, someBlock);
// Block execution hops along other queues and eventually comes back to main runloop and will still be in tracking mode.
}
到这个
- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate {
// Currently in UITrackingRunLoopMode
[self performSelector:@selector(backQueueTask) withObject:nil afterDelay:0 inModes:@[NSDefaultRunLoopMode]];
}
-(void)backQueueTask {
// Currently in NSDefaultRunLoopMode
dispatch_async(someGlobalQueue, someBlock);
// Hops along other queues and eventually comes back to main runloop and will still be in NSDefaultRunLoopMode.
// It's as if the runloop mode when execution returns was dependent on what it was when the background block was queued.
}
每个线程只有一个运行循环,所以如果您在主线程上,那么您也在主运行循环上。但是,运行循环可以在不同的模式下运行。
你可以尝试一些事情来弄清问题的真相:
您可以使用+[NSRunLoop currentRunLoop]
和+[NSRunLoop mainRunLoop]
来验证您是从主线程和主运行循环执行的。
您也可以将当前运行循环直接与NSTimer一起使用,以安排延迟执行选择器。例如:
void (^completionBlock)(BOOL) = ^(BOOL finished) {
NSCAssert([NSRunLoop currentRunLoop] == [NSRunLoop mainRunLoop], @"We're not on the main run loop");
NSRunLoop* runLoop = [NSRunLoop mainRunLoop];
// Immediate invocation.
[runLoop performSelector:@selector(someMethod) target:self argument:nil order:0 modes:@[NSDefaultRunLoopMode]];
// Delayed invocation.
NSTimer* timer = [NSTimer timerWithTimeInterval:1.0 target:self selector:@selector(someMethod) userInfo:nil repeats:NO];
[runLoop addTimer:timer forMode:NSDefaultRunLoopMode];
};
这些调用本质上等同于-performSelector:withObject:
和-performSelector:withObject:afterDelay:
。
这允许您确认正在使用哪个运行循环。如果您在主运行循环上,而延迟调用没有运行,那么主运行循环可能在默认模式下不为计时器提供服务的模式下运行。例如,当UIScrollView正在跟踪触摸输入时,可能会发生这种情况。
-performSelector:withObject:afterDelay:
不调度调度队列上的操作;它在当前线程的运行循环上调度它。每个线程都有一个运行循环,但必须有人运行运行循环才能在上面执行操作。因此,这完全取决于代码在哪个线程上运行。
如果它在主线程上运行,则操作将在主线程的运行循环上进行调度。在基于事件的应用程序中,UIApplicationMain
在main
函数中被调用,该函数在应用程序的整个生命周期内在主线程上运行一个运行循环。
如果这是在您创建的另一个线程上运行的,那么操作将放在该线程的运行循环上。但是,除非显式运行线程的运行循环,否则在运行循环上调度的操作将不会运行。
如果这是在GCD调度队列上运行的,则意味着它正在某个未知线程上运行。GCD调度队列以对用户不透明的方式在内部管理线程。一般来说,没有人会在这样的线程上运行运行循环,所以在运行循环上调度的操作不会运行。(当然,您可以在计划操作的同一个地方显式地运行运行循环,但这会阻塞线程,从而阻塞调度队列,这没有多大意义。)
performSelector:withObject:afterDelay
这将调用调用该函数的线程上的选择器。
performSelectorOnMainThread:WithObject:waitUntilDon
,这将确保选择器在主线程上被调用
什么是运行循环:
运行循环是与线程相关联的基本基础结构的一部分。运行循环是一个事件处理循环,用于安排工作和协调接收传入事件。运行循环的目的是在有工作要做时让线程保持忙碌,在没有工作时让线程进入睡眠状态。