UIScrollView 暂停 NSTimer 直到滚动完成

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

有什么办法可以解决这个问题吗? 线程? 优先级设置? 任何东西?

19101 次浏览

You have to run another thread and another run loop if you want timers to fire while scrolling; since timers are processed as part of the event loop, if you're busy processing scrolling your view, you never get around to the timers. Though the perf/battery penalty of running timers on other threads might not be worth handling this case.

Yes, Paul is right, this is a run loop issue. Specifically, you need to make use of the NSRunLoop method:

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

An easy & simple to implement solution is to do:

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

This is the swift version.

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

For anyone using Swift 3

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




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

for anyone use Swift 4:

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

tl;dr the runloop is handing scroll related events. It can't handle any more events — unless you manually change the timer's config so the timer can be processed while runloop is handling touch events. OR try an alternate solution and use GCD


A must read for any iOS developer. Lots of things are ultimately executed through RunLoop.

Derived from Apple's docs.

What is a Run Loop?

A run loop is very much like its name sounds. It is a loop your thread enters and uses to run event handlers in response to incoming events

How delivery of events are disrupted?

Because timers and other periodic events are delivered when you run the run loop, circumventing that loop disrupts the delivery of those events. The typical example of this behavior occurs whenever you implement a mouse-tracking routine by entering a loop and repeatedly requesting events from the application. Because your code is grabbing events directly, rather than letting the application dispatch those events normally, active timers would be unable to fire until after your mouse-tracking routine exited and returned control to the application.

What happens if timer is fired when run loop is in the middle of execution?

This happens A LOT OF TIMES, without us ever noticing. I mean we set the timer to fire at 10:10:10:00, but the runloop is executing an event which takes till 10:10:10:05, hence the timer is fired 10:10:10:06

Similarly, if a timer fires when the run loop is in the middle of executing a handler routine, the timer waits until the next time through the run loop to invoke its handler routine. If the run loop is not running at all, the timer never fires.

Would scrolling or anything that keeps the runloop busy shift all the times my timer is going to fire?

You can configure timers to generate events only once or repeatedly. A repeating timer reschedules itself automatically based on the scheduled firing time, not the actual firing time. For example, if a timer is scheduled to fire at a particular time and every 5 seconds after that, the scheduled firing time will always fall on the original 5 second time intervals, even if the actual firing time gets delayed. If the firing time is delayed so much that it misses one or more of the scheduled firing times, the timer is fired only once for the missed time period. After firing for the missed period, the timer is rescheduled for the next scheduled firing time.

How can I change the RunLoops's mode?

You can't. The OS just changes itself for you. e.g. when user taps, then the mode switches to eventTracking. When the user taps are finished, the mode goes back to default. If you want something to be run in a specific mode, then it's up to you make sure that happens.


Solution:

When user is scrolling the the Run Loop Mode becomes tracking. The RunLoop is designed to shifts gears. Once the mode is set to eventTracking, then it gives priority (remember we have limited CPU cores) to touch events. This is an architectural design by the OS designers.

By default timers are NOT scheduled on the tracking mode. They are scheduled on:

Creates a timer and schedules it on the current run loop in the default mode.

The scheduledTimer underneath does this:

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

If you want your timer to work when scrolling then you must do either:

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

Or just do:

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

Ultimately doing one of the above means your thread is not blocked by touch events. which is equivalent to:

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.

Alternative solution:

You may consider using GCD for your timer which will help you to "shield" your code from run loop management issues.

For non-repeating just use:

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

For repeating timers use:

See how to use DispatchSourceTimer


Digging deeper from a discussion I had with Daniel Jalkut:

Question: how does GCD (background threads) e.g. a asyncAfter on a background thread get executed outside of the RunLoop? My understanding from this is that everything is to be executed within a RunLoop

Not necessarily - every thread has at most one run loop, but can have zero if there's no reason to coordinate execution "ownership" of the thread.

Threads are an OS level affordance that gives your process the ability to split up its functionality across multiple parallel execution contexts. Run loops are a framework-level affordance that allows you to further split up a single thread so it can be shared efficiently by multiple code paths.

Typically if you dispatch something that gets run on a thread, it probably won't have a runloop unless something calls [NSRunLoop currentRunLoop] which would implicitly create one.

In a nutshell, modes are basically a filter mechanism for inputs and timers

Tested in swift 5

var myTimer: Timer?


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

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