UIScrollView暂停NSTimer直到滚动完成

mcc*_*ean 82 uiscrollview nstimer ios

当一个UIScrollView(或其派生类)滚动时,似乎所有NSTimers正在运行的都会暂停,直到滚动完成.

有办法解决这个问题吗?主题?优先级设置?什么?

Kas*_*sam 198

一个简单易用的解决方案是:

NSTimer *timer = [NSTimer timerWithTimeInterval:... 
                                         target:...
                                       selector:....
                                       userInfo:...
                                        repeats:...];
[[NSRunLoop mainRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
Run Code Online (Sandbox Code Playgroud)

  • UITrackingRunLoopMode 是一种模式 - 及其公共 - 您可以用来创建不同的计时器行为。 (2认同)
  • 太棒了!使计时器无效时是否必须删除计时器?谢谢 (2认同)

Isa*_*aac 23

对于任何使用Swift 3的人

timer = Timer.scheduledTimer(timeInterval: 0.1,
                            target: self,
                            selector: aSelector,
                            userInfo: nil,
                            repeats: true)


RunLoop.main.add(timer, forMode: RunLoopMode.commonModes)
Run Code Online (Sandbox Code Playgroud)

  • 最好将`timer = Timer(timeInterval:0.1,target:self,selector:aSelector,userInfo:nil,repeats:true)`作为第一个命令而不是`Timer.scheduleTimer()`,因为`scheduleTimer() `将计时器添加到runloop,下一个调用是另一个添加到同一个runloop但具有不同的模式.不要做两次同样的工作. (5认同)

Aug*_*ust 8

是的,Paul是对的,这是一个运行循环问题.具体来说,您需要使用NSRunLoop方法:

- (void)addTimer:(NSTimer *)aTimer forMode:(NSString *)mode
Run Code Online (Sandbox Code Playgroud)


小智 7

这是快速版本.

timer = NSTimer.scheduledTimerWithTimeInterval(0.01, target: self, selector: aSelector, userInfo: nil, repeats: true)
            NSRunLoop.mainRunLoop().addTimer(timer, forMode: NSRunLoopCommonModes)
Run Code Online (Sandbox Code Playgroud)


Hon*_*ney 6

tl; dr runloop 正在处理与滚动相关的事件。它无法处理更多事件——除非您手动更改计时器的配置,以便可以在 runloop 处理触摸事件时处理计时器。或尝试替代解决方案并使用 GCD


任何 iOS 开发者的必读之书。很多事情最终都是通过 RunLoop 来执行的。

源自Apple 的文档

什么是运行循环?

运行循环非常像它的名字。它是您的线程进入并用于运行事件处理程序以响应传入事件的循环

事件的传递是如何中断的?

因为在运行运行循环时会传递计时器和其他周期性事件,所以绕过该循环会中断这些事件的传递。每当您通过进入循环并重复从应用程序请求事件来实现鼠标跟踪例程时,就会出现此行为的典型示例。因为您的代码是直接获取事件,而不是让应用程序正常调度这些事件,所以活动计时器将无法触发,直到您的鼠标跟踪例程退出并将控制权返回给应用程序之后。

如果在 run loop 执行过程中触发 timer 会发生什么?

这种情况发生了很多次,我们从未注意到。我的意思是我们将计时器设置为在 10:10:10:00 触发,但是 runloop 正在执行一个事件,该事件持续到 10:10:10:05,因此计时器在 10:10:10:06 触发

类似地,如果在 run loop 正在执行处理程序例程时触发计时器,则计时器将等待直到下一次通过 run loop 调用其处理程序例程。如果 run loop 根本没有运行,则计时器永远不会触发。

滚动或任何使 runloop 忙碌的东西会在我的计时器将要触发的所有时间转移吗?

您可以将计时器配置为仅生成一次或重复生成事件。重复计时器根据计划的触发时间而不是实际的触发时间自动重新安排自身。例如,如果计时器计划在特定时间触发,并且在此之后每 5 秒触发一次,则计划触发时间将始终落在原始的 5 秒时间间隔内,即使实际触发时间被延迟。如果触发时间延迟太多以至于错过了一个或多个预定的触发时间,则计时器在错过的时间段内仅触发一次。在错过的时间段触发后,定时器被重新安排到下一个预定的触发时间。

如何更改 RunLoops 的模式?

你不能。操作系统只是为你改变自己。例如,当用户点击时,模式会切换到eventTracking。当用户点击完成时,模式返回到default。如果您想在特定模式下运行某些东西,则由您来确保它发生。


解决方案:

当用户滚动时,运行循环模式变为tracking。RunLoop 旨在换档。一旦模式设置为eventTracking,它就会优先(记住我们的 CPU 内核有限)触摸事件。这是操作系统设计者的架构设计

默认情况下,定时器不安排在tracking模式上。他们被安排在:

创建一个计时器并在默认模式下将其安排在当前运行循环上 。

scheduledTimer 下做到这一点:

RunLoop.main.add(timer, forMode: .default)
Run Code Online (Sandbox Code Playgroud)

如果您希望您的计时器在滚动时工作,那么您必须执行以下任一操作:

let timer = Timer.scheduledTimer(timeInterval: 1.0, target: self,
 selector: #selector(fireTimer), userInfo: nil, repeats: true) // sets it on `.default` mode

RunLoop.main.add(timer, forMode: .tracking) // AND Do this
Run Code Online (Sandbox Code Playgroud)

或者只是这样做:

RunLoop.main.add(timer, forMode: .common)
Run Code Online (Sandbox Code Playgroud)

最终执行上述其中一项意味着您的线程不会被触摸事件阻塞。这相当于:

RunLoop.main.add(timer, forMode: .default)
RunLoop.main.add(timer, forMode: .eventTracking)
RunLoop.main.add(timer, forMode: .modal) // This is more of a macOS thing for when you have a modal panel showing.
Run Code Online (Sandbox Code Playgroud)

替代解决方案:

您可以考虑为计时器使用 GCD,这将帮助您“屏蔽”您的代码免受运行循环管理问题的影响。

对于非重复只需使用:

DispatchQueue.main.asyncAfter(deadline: .now() + 5) {
    // your code here
}
Run Code Online (Sandbox Code Playgroud)

对于重复计时器使用:

查看如何使用 DispatchSourceTimer


从我与 Daniel Jalkut 的讨论中深入挖掘:

问题:如何在 RunLoop 之外执行 GCD(后台线程),例如后台线程上的 asyncAfter?我的理解是,一切都将在 RunLoop 中执行

不一定 - 每个线程最多有一个运行循环,但如果没有理由协调线程的执行“所有权”,则可以有零。

线程是一种操作系统级别的可供性,它使您的进程能够在多个并行执行上下文中拆分其功能。运行循环是一种框架级别的可供性,它允许您进一步拆分单个线程,以便多个代码路径有效地共享它。

通常,如果您调度一些在线程上运行的东西,它可能不会有一个 runloop,除非某些调用[NSRunLoop currentRunLoop]会隐式地创建一个runloop 。

简而言之,模式基本上是输入和计时器的过滤机制


Ana*_*tts 5

如果你想在滚动时触发定时器,你必须运行另一个线程和另一个运行循环; 由于计时器是作为事件循环的一部分处理的,如果您正在忙于处理滚动视图,那么您永远不会到达计时器.虽然在其他线程上运行计时器的性能/电池损失可能不值得处理这种情况.

  • 它不需要另一个线程,请参阅Kashif的最新[回答](http://stackoverflow.com/questions/605027/uiscrollview-pauses-nstimer-until-scrolling-finishes/2742275#2742275).我尝试过它并且无需额外的线程. (11认同)
  • 不需要另一个线程.卡希夫的回答要好得多. (3认同)
  • 在麻雀射击大炮.而不是线程,只需在正确的运行循环模式下安排计时器. (3认同)
  • 我认为Kashif的答案是迄今为止最好的答案,因为它只需要添加1行代码来解决这个问题. (2认同)