How do I create a NSTimer on a background thread?

前端 未结 10 674
感情败类
感情败类 2020-11-28 02:04

I have a task that needs to be performed every 1 second. Currently I have an NSTimer firing repeatedly every 1 sec. How do I have the timer fire in a background thread (no

10条回答
  •  广开言路
    2020-11-28 02:46

    Today after 6 years, I try to do same thing, here is alternative soltion: GCD or NSThread.

    Timers work in conjunction with run loops, a thread's runloop can be get from the thread only, so the key is that schedule timer in the thread.

    Except main thread's runloop, runloop should start manually; there should be some events to handle in running runloop, like Timer, otherwise runloop will exit, and we can use this to exit a runloop if timer is the only event source: invalidate the timer.

    The following code is Swift 4:

    Solution 0: GCD

    weak var weakTimer: Timer?
    @objc func timerMethod() {
        // vefiry whether timer is fired in background thread
        NSLog("It's called from main thread: \(Thread.isMainThread)")
    }
    
    func scheduleTimerInBackgroundThread(){
        DispatchQueue.global().async(execute: {
            //This method schedules timer to current runloop.
            self.weakTimer = Timer.scheduledTimer(timeInterval: 1, target: self, selector: #selector(timerMethod), userInfo: nil, repeats: true)
            //start runloop manually, otherwise timer won't fire
            //add timer before run, otherwise runloop find there's nothing to do and exit directly.
            RunLoop.current.run()
        })
    }
    

    Timer has strong reference to target, and runloop has strong reference to timer, after timer invalidate, it release target, so keep weak reference to it in target and invalidate it in appropriate time to exit runloop(and then exit thread).

    Note: as an optimization, syncfunction of DispatchQueue invokes the block on the current thread when possible. Actually, you execute above code in main thread, Timer is fired in main thread, so don't use sync function, otherwise timer is not fired at the thread you want.

    You could name thread to track its activity by pausing program executing in Xcode. In GCD, use:

    Thread.current.name = "ThreadWithTimer"
    

    Solution 1: Thread

    We could use NSThread directly. Don't afraid, code is easy.

    func configurateTimerInBackgroundThread(){
        // Don't worry, thread won't be recycled after this method return.
        // Of course, it must be started.
        let thread = Thread.init(target: self, selector: #selector(addTimer), object: nil)
        thread.start()
    }
    
    @objc func addTimer() {
        weakTimer = Timer.scheduledTimer(timeInterval: 1, target: self, selector: #selector(timerMethod), userInfo: nil, repeats: true)
        RunLoop.current.run()
    }
    

    Solution 2: Subclass Thread

    If you want to use Thread subclass:

    class TimerThread: Thread {
        var timer: Timer
        init(timer: Timer) {
            self.timer = timer
            super.init()
        }
    
        override func main() {
            RunLoop.current.add(timer, forMode: .defaultRunLoopMode)
            RunLoop.current.run()
        }
    }
    

    Note: don't add timer in init, otherwise, timer is add to init's caller's thread's runloop, not this thread's runloop, e.g., you run following code in main thread, if TimerThread add timer in init method, timer will be scheduled to main thread's runloop, not timerThread's runloop. You can verify it in timerMethod() log.

    let timer = Timer.init(timeInterval: 1, target: self, selector: #selector(timerMethod), userInfo: nil, repeats: true)
    weakTimer = timer
    let timerThread = TimerThread.init(timer: timer)
    timerThread.start()
    

    P.S About Runloop.current.run(), its document suggest don't call this method if we want runloop to terminate, use run(mode: RunLoopMode, before limitDate: Date), actually run() repeatedly invoke this method in the NSDefaultRunloopMode, what's mode? More details in runloop and thread.

提交回复
热议问题