你如何安排一个块在下一次运行循环迭代中运行?

Posted

技术标签:

【中文标题】你如何安排一个块在下一次运行循环迭代中运行?【英文标题】:How do you schedule a block to run on the next run loop iteration? 【发布时间】:2013-02-16 04:08:41 【问题描述】:

我希望能够在下一次运行循环迭代中执行block。它是在下一个运行循环的开始还是结束时执行并不那么重要,只是延迟执行,直到当前运行循环中的所有代码都执行完。

我知道以下内容不起作用,因为它与主运行循环交错,因此我的代码可能会在下一个运行循环中执行,但可能不会。

dispatch_async(dispatch_get_main_queue(),^
    //my code
);

我认为以下内容与上述问题相同:

dispatch_after(DISPATCH_TIME_NOW, dispatch_get_main_queue(), ^(void)
    //my code
);

现在我相信以下内容会起作用,因为它被放置在当前运行循环的末尾(如果我错了,请纠正我),这真的有效吗?

[self performSelector:@selector(myMethod) withObject:nil afterDelay:0];

有一个0 间隔的计时器呢?文档指出:If seconds is less than or equal to 0.0, this method chooses the nonnegative value of 0.1 milliseconds instead. 这是否转化为保证在下一次运行循环迭代时执行?

[NSTimer scheduledTimerWithTimeInterval:0 target:self selector:@selector(myMethod) userInfo:nil repeats:NO];

这就是我能想到的所有选项,但我仍然没有更接近在下一次运行循环迭代中执行一个块(而不是调用一个方法),并保证它不会更早。

【问题讨论】:

【参考方案1】:

我不相信有任何 API 可以让您保证代码在下一个事件循环轮次中运行。我也很好奇为什么你需要保证没有其他东西在循环中运行,特别是主要的。

我还可以确认,使用 perforSelector:withObject:afterDelay 确实使用了基于 runloop 的计时器,并且在功能上与 dispatch_get_main_queue() 上的 dispatch_async'ing 行为相似。

编辑:

实际上,在重新阅读您的问题后,听起来您只需要 current 运行循环即可完成。如果这是真的,那么 dispatch_async 正是你所需要的。事实上,上面的所有代码确实保证了 current runloop 循环将完成。

【讨论】:

我不确定这是不是真的。文档状态:The main dispatch queue is a globally available serial queue that executes tasks on the application’s main thread. This queue works with the application’s run loop (if one is present) to interleave the execution of queued tasks with the execution of other event sources attached to the run loop. 见developer.apple.com/library/mac/#documentation/General/… 人们可能希望将一个块安排在它排队的运行循环周期之后执行的一个原因是为了保证事件的有序序列。创建一个事务或添加一系列原本不相关的事务需要考虑顺序。一个运行循环周期是一个事务。在顺序很重要的地方,它是一个不可分割的考虑因素。在提出这个问题时,它可能并未被视为一种常见需求;今天,整个编程范式都基于此:请参阅函数响应式编程 (www.manning.com) 的第 1 章。【参考方案2】:

您可能不知道运行循环在每次迭代中所做的一切。 (在我研究这个答案之前,我没有!)碰巧,CFRunLoop 是open-source CoreFoundation package 的一部分,所以我们可以看看它到底意味着什么。运行循环大致如下:

while (true) 
    Call kCFRunLoopBeforeTimers observer callbacks;
    Call kCFRunLoopBeforeSources observer callbacks;
    Perform blocks queued by CFRunLoopPerformBlock;
    Call the callback of each version 0 CFRunLoopSource that has been signalled;
    if (any version 0 source callbacks were called) 
        Perform blocks newly queued by CFRunLoopPerformBlock;
    
    if (I didn't drain the main queue on the last iteration
        AND the main queue has any blocks waiting)
    
        while (main queue has blocks) 
            perform the next block on the main queue
        
     else 
        Call kCFRunLoopBeforeWaiting observer callbacks;
        Wait for a CFRunLoopSource to be signalled
          OR for a timer to fire
          OR for a block to be added to the main queue;
        Call kCFRunLoopAfterWaiting observer callbacks;
        if (the event was a timer) 
            call CFRunLoopTimer callbacks for timers that should have fired by now
         else if (event was a block arriving on the main queue) 
            while (main queue has blocks) 
                perform the next block on the main queue
            
         else 
            look up the version 1 CFRunLoopSource for the event
            if (I found a version 1 source) 
                call the source's callback
            
        
    
    Perform blocks queued by CFRunLoopPerformBlock;

您可以看到有多种方法可以挂接到运行循环。您可以创建一个CFRunLoopObserver,以便为您想要的任何“活动”调用。您可以创建版本 0 CFRunLoopSource 并立即发出信号。您可以创建一对连接的CFMessagePorts,将其中一个包装在版本 1 CFRunLoopSource 中,然后向其发送消息。您可以创建一个CFRunLoopTimer。您可以使用dispatch_get_main_queueCFRunLoopPerformBlock 对块进行排队。

您需要根据调度块的时间以及需要调用它的时间来决定使用哪些 API。

例如,触摸是在版本 1 源中处理的,但如果您通过更新屏幕来处理触摸,则在提交核心动画事务之前不会实际执行更新,这发生在 kCFRunLoopBeforeWaiting 观察者中。

现在假设您想在处理触摸时安排块,但您希望它在事务提交后执行。

您可以为kCFRunLoopBeforeWaiting 活动添加自己的CFRunLoopObserver,但此观察者可能在Core Animation 的观察者之前或之后运行,具体取决于您指定的顺序和Core Animation 指定的顺序。 (Core Animation 当前指定了 2000000 的顺序,但没有记录,因此可能会更改。)

为了确保您的块在 Core Animation 的观察者之后运行,即使您的观察者在 Core Animation 的观察者之前运行,也不要在观察者的回调中直接调用该块。相反,此时使用dispatch_async 将块添加到主队列。将块放入主队列将强制运行循环立即从其“等待”中唤醒。它将运行任何kCFRunLoopAfterWaiting 观察者,然后它将耗尽主队列,此时它将运行您的块。

【讨论】:

这是对运行循环功能的一个很好的总结。对于 OP 的问题,我想说CFRunLoopPerformBlock() 将是确保块在运行循环的下一个循环上执行的最合理的方式。文档没有明确说明如果在循环中间添加块将不会被执行,但他们确实说:“此方法仅将块入队并且不会自动唤醒指定的运行循环。因此,该块的执行发生在下次运行循环唤醒以处理另一个输入源时。”您可以通过在观察者回调中添加块来检查。 嗨@rob市长,这是一个非常古老的好答案,但是:触摸不是在版本0源中处理吗?当我在-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event 之类的方法中添加断点时,堆栈显示«CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION_ »。我并没有真正区分 source0 和 source1 ......即使在阅读 developer.apple.com/library/content/documentation/Cocoa/… Apple 可能改变了触摸的处理方式,或者在模拟器上的处理方式与在设备上的处理方式不同。我现在没有时间调查。【参考方案3】:

mainQueue 上的dispatch_async 是一个很好的建议,但它不会在下一个运行循环中运行,而是插入到循环中的当前运行中。

要获得你想要的行为,你需要求助于传统方式:

[self performSelector:@selector(myMethod) withObject:nil afterDelay:0];

这还有一个额外的好处是可以使用 NSObject 的 cancelPreviousPerforms 取消它。

【讨论】:

【参考方案4】:

我给自己写了一个NSObject category,它接受一个基于another *** question 的可变延迟值。通过传递零值,您可以有效地使代码在下一次可用的 runloop 迭代中运行。

【讨论】:

【参考方案5】:

Rob 的回答很棒而且内容丰富。我不想替换它。

刚刚看了UIView documentation,我发现:

完成

动画序列时要执行的块对象 结束。这个块没有返回值,只接受一个布尔值 指示动画是否实际的参数 在调用完成处理程序之前完成。如果持续时间 动画为0,此块在开始时执行 下一个运行循环周期。该参数可能为NULL。

所以一个简单的解决方案是:

UIView.animate(withDuration: 0) 
    // anything

【讨论】:

以上是关于你如何安排一个块在下一次运行循环迭代中运行?的主要内容,如果未能解决你的问题,请参考以下文章

如何安排一个函数在 Flask 上每小时运行一次?

python asyncio运行一次事件循环?

如何创建一个循环来检查每次迭代时是不是有新的 IBAction 输入?

我可以触发Zeppelin在命令中运行特定块吗?

4_while循环结构和break&continue

你如何使用WaitGroup确保goroutine在for循环中完成?