UIScrollView 暂停 NSTimer 直到滚动完成

Posted

技术标签:

【中文标题】UIScrollView 暂停 NSTimer 直到滚动完成【英文标题】:UIScrollView pauses NSTimer until scrolling finishes 【发布时间】:2010-10-10 22:34:06 【问题描述】:

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

有没有办法解决这个问题?线程?优先级设置?有什么事吗?

【问题讨论】:

Why does UIScrollView pause my CADisplayLink?的可能重复 七年后...***.com/a/12625429/294884 【参考方案1】:

tl;dr 运行循环正在处理与滚动相关的事件。它无法处理更多事件——除非您手动更改计时器的配置,以便在运行循环处理 touch 事件时处理 timer。或者尝试其他解决方案并使用 GCD


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

源自Apple's docs。

什么是运行循环?

运行循环很像它的名字。这是一个循环你的线程 进入并用于运行事件处理程序以响应传入事件

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

因为运行时会传递计时器和其他周期性事件 运行循环,绕过该循环会破坏那些的交付 事件。每当您 通过进入循环并重复执行鼠标跟踪例程 从应用程序请求事件。因为你的代码正在抓取 直接事件,而不是让应用程序调度那些 事件正常,活动计时器将无法触发,直到之后 您的鼠标跟踪例程已退出并将控制权返回给 应用。

如果在运行循环执行过程中触发计时器会发生什么?

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

类似地,如果一个计时器在运行循环处于中间时触发 执行处理程序例程,计时器一直等到下一次 通过运行循环调用其处理程序。如果运行循环是 根本不运行,计时器永远不会触发。

在我的计时器将要触发时,滚动或任何使 runloop 保持忙碌的东西会一直变化吗?

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

如何更改 RunLoops 的模式?

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


解决方案:

当用户滚动时,运行循环模式变为tracking。 RunLoop 设计用于换档。一旦模式设置为eventTracking,它就会优先处理触摸事件(请记住我们的 CPU 内核有限)。 This is an architectural design by the OS designers.

默认情况下,不会在tracking 模式下安排计时器。他们安排在:

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

下面的scheduledTimer 是这样做的:

RunLoop.main.add(timer, forMode: .default)

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

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

或者干脆做:

RunLoop.main.add(timer, forMode: .common)

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

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.

替代方案:

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

对于非重复使用:

DispatchQueue.main.asyncAfter(deadline: .now() + 5) 
    // your code here

对于重复计时器,请使用:

见how to use DispatchSourceTimer


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

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

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

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

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

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

【讨论】:

【参考方案2】:

在 swift 5 中测试

var myTimer: Timer?

self.myTimer= Timer.scheduledTimer(withTimeInterval: 1, repeats: true)  timer in
      //your code

    
RunLoop.main.add(self.myTimer!, forMode: .common)

【讨论】:

【参考方案3】:

任何人都可以使用 Swift 4:

    timer = Timer(timeInterval: 1, target: self, selector: #selector(timerUpdated), userInfo: nil, repeats: true)
    RunLoop.main.add(timer, forMode: .common)

【讨论】:

【参考方案4】:

对于任何使用 Swift 3 的人

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


RunLoop.main.add(timer, forMode: RunLoopMode.commonModes)

【讨论】:

最好调用timer = Timer(timeInterval: 0.1, target: self, selector: aSelector, userInfo: nil, repeats: true)作为第一个命令而不是Timer.scheduleTimer(),因为scheduleTimer()在runloop中添加了计时器,下一次调用是另一个添加到同一个runloop但模式不同的命令。不要重复做同样的工作。【参考方案5】:

这是快速版本。

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

【讨论】:

【参考方案6】:

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

NSTimer *timer = [NSTimer timerWithTimeInterval:... 
                                         target:...
                                       selector:....
                                       userInfo:...
                                        repeats:...];
[[NSRunLoop mainRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];

【讨论】:

UITrackingRunLoopMode 是模式 - 以及它的公共模式 - 您可以使用它来创建不同的计时器行为。 喜欢它,它也像 GLKViewController 的魅力一样工作。只需在 UIScrollView 出现时将 controller.paused 设置为 YES 并按照说明启动您自己的计时器。当滚动视图被关闭时反转它,就是这样!我的更新/渲染循环不是很昂贵,所以这可能会有所帮助。 太棒了!当我使计时器无效时,我是否必须删除它?谢谢 这适用于滚动,但当应用程序进入后台时会停止计时器。有什么解决方案可以同时实现吗?【参考方案7】:

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

- (void)addTimer:(NSTimer *)aTimer forMode:(NSString *)mode

【讨论】:

【参考方案8】:

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

【讨论】:

它不需要另一个线程,请参阅来自 Kashif 的最新 answer。我试过了,它不需要额外的线程。 用大炮射击麻雀。而不是线程,只需将计时器安排在正确的运行循环模式。 我认为 Kashif 下面的答案是最好的答案,因为它只需要你添加 1 行代码来解决这个问题。

以上是关于UIScrollView 暂停 NSTimer 直到滚动完成的主要内容,如果未能解决你的问题,请参考以下文章

发生 uiscrollview 事件时未触发 NSTimer

发生 uiscrollview 事件时未触发 NSTimer

如何暂停/播放 NSTimer?

如何在 iphone 中暂停和恢复 NSTimer

使用 NSTimer 的秒表错误地在显示中包含暂停时间

Apple Watch 显示器睡眠暂停 NSTimer,但 WKInterfaceTimer 一直倒计时