【问题标题】:Which queue is used for -[NSObject performSelector:withObject:afterDelay]?哪个队列用于 -[NSObject performSelector:withObject:afterDelay]?
【发布时间】:2015-07-01 14:28:13
【问题描述】:

我最近遇到了延迟选择器未触发的问题(NSTimer 和使用performSelector:withObject:afterDelay 调用的方法)。

我已经阅读了 Apple 的文档,并且在特殊注意事项区域中确实提到了,

此方法注册到其当前上下文的 runloop,并依赖于定期运行的 runloop 才能正确执行。您可能调用此方法并最终注册到一个不会定期自动运行的 runloop 的一个常见上下文是当被调度队列调用时。如果你在 dispatch queue 上运行时需要这种类型的功能,你应该使用 dispatch_after 和相关的方法来获得你想要的行为。

这很有意义,除了当前上下文部分的运行循环。我发现自己对它实际上要去哪个运行循环感到困惑。是线程的主运行循环处理所有事件,还是在我们不知情的情况下是不同的?

例如,如果我在作为 CoreAnimation 完成块调用的块内调用 performSelector 之前遇到断点,则调试器会显示执行在主线程上。但是,调用 performSelector:withObject:afterDelay 从未真正运行选择器。这让我认为调用有效地注册了与 CoreAnimation 框架关联的 runloop,因此无论在主线程上执行 performSelector 调用,如果 CoreAnimation 不轮询它的 runloop,则不会执行操作。

performSelectorOnMainThread:WithObject:waitUntilDone 替换该块内的这个调用可以解决问题,但我很难说服同事这是根本原因。

更新:我能够将问题的根源追溯到 UIScrollViewDelegate 回调。有意义的是,当调用 UI 委托回调时,主运行循环将处于 UITrackingRunLoopMode。但是此时,处理程序将在后台队列中排队一个块,然后执行将跳过其他几个队列,最终回到主运行循环。问题是当它回到主运行循环时,它仍然处于 UITrackingRunLoopMode。我认为当委托方法完成时主运行循环应该已经退出 UITracking 模式,但是当执行回到主运行循环时,它仍然处于该模式。从 UIScrollViewDelegate 方法推迟启动作业后台排队的代码可以解决问题,例如[self performSelector:@selector(sendTaskToBackQueue) withObject:nil afterDelay:0 inModes:@[NSDefaultRunLoopMode]]。后台任务排队回到主线程时使用的runloop模式是否可能取决于runloop在后台任务排队时所处的模式?

基本上,唯一的变化就是从这个开始......

- (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.
}

【问题讨论】:

    标签: ios objective-c nsrunloop performselector


    【解决方案1】:

    每个线程只有一个运行循环,所以如果你在主线程上,那么你也在主运行循环上。但是,运行循环可以在不同的模式下运行。

    您可以尝试以下几件事来查明问题的根源:

    您可以使用+[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 跟踪触摸输入时,就会发生这种情况。

    【讨论】:

    • 啊,现在这更有意义了。有问题的路径是 UITrackingRunLoopMode 模式,而快乐路径运行循环模式是 NSDefaultRunLoopMode。该块确实显示它正在主运行循环中运行。但即便如此,如果循环处于跟踪模式,不应该在下一次主循环处于默认模式时执行选择器吗?也许它不会立即再次以默认模式运行?
    • @JoeyCarson 是的,当运行循环返回默认模式时,计时器应该立即触发。您可以在 NSRunLoopCommonModes 而不是 NSDefaultRunLoopMode 中安排计时器,计时器将在默认或 UI 跟踪模式下触发。
    • 是的,我试过了,它确实有效,但我不想在我的代码中加入黑客攻击。除此之外,虽然从不调用延迟选择器。请注意,如果几分钟后我开始弄乱 UI,我会看到延迟调用运行,因此主线程可能出于某种原因永远不会返回到默认模式。有些东西必须把它放在那种模式下,而且它不会立即摆脱它。应该吗?
    • @JoeyCarson 如果运行循环始终处于 UI 跟踪模式,那么这是一个问题。您可以设置一个运行循环观察器来查看您何时进入和退出默认模式。请参阅 CFRunLoopObserverCreateWithHandler()。或者,您可以设置一个简单的重复计时器,例如,以 0.1 秒的间隔记录到控制台。如果计时器正在触发,那么您处于默认模式。
    • 我能够将原始行为追溯到将运行循环置于 UITracking 模式的 UIKit 委托回调。我已经更新了原始问题以反映这一点。似乎它与主运行循环在启动后台任务时所处的模式有关。在该后台任务完成并排队返回主运行循环后,主运行循环仍处于 UITracking 模式。我将排队后台操作更改为在主队列上发生(仍然排队到后台),但排队本身发生在默认模式而不是跟踪模式下。这解决了它。为什么?
    【解决方案2】:

    -performSelector:withObject:afterDelay: 不会在调度队列上调度操作; 在当前线程的运行循环中调度它。每个线程都有一个运行循环,但必须有人运行运行循环才能对其执行操作。所以这一切都取决于这段代码在哪个线程上运行。

    如果在主线程上运行,该操作将被调度在主线程的运行循环中。在基于事件的应用程序中,UIApplicationMainmain 函数中被调用,该函数在应用程序的整个生命周期内在主线程上运行一个运行循环。

    如果这是在您创建的另一个线程上运行的,那么该操作将放在该线程的运行循环中。但是除非你显式运行线程的运行循环,否则运行循环上调度的操作不会运行。

    如果这是在 GCD 调度队列上运行的,则意味着它正在某个未知线程上运行。 GCD 调度队列以对用户不透明的方式在内部管理线程。通常没有人会在这样的线程上运行运行循环,因此在运行循环上安排的操作不会运行。 (当然,您可以在调度操作的同一位置显式运行运行循环,但这会阻塞线程,从而阻塞调度队列,这没有多大意义。)

    【讨论】:

    • 这是一个非常全面的答案。谢谢先生。
    【解决方案3】:

    performSelector:withObject:afterDelay 这将在调用该函数的线程上调用选择器

    performSelectorOnMainThread:WithObject:waitUntilDon,这将确保选择器在主线程上被调用

    What is run loop:

    运行循环是与线程相关的基础架构的一部分。运行循环是一个事件处理循环,用于安排工作和协调接收传入事件。运行循环的目的是在有工作要做的时候让你的线程保持忙碌,而在没有工作的时候让你的线程进入睡眠状态。

    【讨论】:

    • 但是如果你在来自不同运行循环的块的上下文中执行呢?这就是我所指的场景。例如,我还注意到,当其他 CoreAnimation 稍后再次开始运行时,选择器最终会被触发。这让我相信,调用 performSelector 的不只是线程,还有将针对的特定 runloop。
    • 一个runloop是属于一个线程的。你只需要从线程的角度去理解。我不确定您描述的内容。您可以发布一些示例代码以使其清楚
    • 我了解什么是 runloop,并且每个 NSThread 都有一个用于处理事务之类的事件。但我也明白你可以有其他的运行循环,你可以轮询和运行它们,即使在同一个线程上。我相信这就是 CoreAnimation 正在做的事情,运行一个单独的 runloop 并在其 runloop 的上下文中执行我的回调块,而不是主线程使用的相同的主 runloop。
    猜你喜欢
    • 2023-03-04
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2016-11-26
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多