iOS中定時器(Timer)的那點事

Timer

A timer that fires after a certain time interval has elapsed, sending a specified message to a target object.

簡單來說就是在指定時間過去,定時器會被啟動并發(fā)送消息給目標(biāo)對象去執(zhí)行對應(yīng)的事件

定時器(Timer)的功能是與Runloop相關(guān)聯(lián)的,Runloop會強引用Timer,所以當(dāng)定時器被添加到Runloop之后,我們并沒有必須強引用定時器(Timer

理解Run Loop概念

談到定時器,首先需要了解的一個概念是 RunLoop。一般來講,一個線程一次只能執(zhí)行一個任務(wù),執(zhí)行完成后線程就會退出。如果我們需要一個機制,讓線程能隨時處理事件但并不退出,通常的代碼邏輯是這樣的:

function  loop()  {
    initialize();
    do  {
        var  message  =  get_next_message();
        process_message(message);
    }  while  (message  !=  quit);
}

這種模型通常被稱作 Event Loop。 Event Loop 在很多系統(tǒng)和框架里都有實現(xiàn),比如Windows 程序的消息循環(huán),再比如 OSX/iOS 里的 RunLoop。實現(xiàn)這種模型的關(guān)鍵點在于:如何管理事件/消息,如何讓線程在沒有處理消息時休眠以避免資源占用、在有消息到來時立刻被喚醒。

所以,RunLoop實際上就是一個對象,這個對象管理了其需要處理的事件和消息,并提供了一個入口函數(shù)來執(zhí)行上面 Event Loop 的邏輯。線程執(zhí)行了這個函數(shù)后,就會一直處于這個函數(shù)內(nèi)部 “接受消息->等待->處理” 的循環(huán)中,直到這個循環(huán)結(jié)束(比如傳入 quit 的消息),函數(shù)返回。

OSX/iOS 系統(tǒng)中,提供了兩個這樣的對象:RunLoop 和 CFRunLoopRef。更多詳細的內(nèi)容可以看深入理解RunLoop,也可以參考官方文檔Threading Programming Guide

重復(fù)和非重復(fù)定時器

  • 重復(fù)定時

常用的target-action方式

func addRepeatedTimer() {
   let timer = Timer.scheduledTimer(timeInterval: 1.0,
                                          target: self,
                                        selector: #selector(fireTimer),
                                        userInfo: nil,
                                         repeats: true)
}

@objc func fireTimer() {
    print("fire timer")
}

參數(shù)介紹

  • timeInterval:延時時間,單位為秒,可以是小數(shù)。如果值小于等于0.0的話,系統(tǒng)會默認賦值0.1毫秒
  • target:目標(biāo)對象,一般是self,但是注意timer會強引用target,直到調(diào)用invalidate方法
  • selector: 執(zhí)行方法
  • userInfo: 傳入信息
  • repeats:是否重復(fù)執(zhí)行

使用block方式

func addRepeatedTimerWithClosure() {
    if #available(iOS 10.0, *) { // iOS10之后的API
        let timer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { (timer) in
            print("fire timer")
        }
     } else {
        // Fallback on earlier versions
    }
}

上面兩種方式可以實現(xiàn)重復(fù)定時觸發(fā)事件,但是target-action方式會存在一個問題?那就是對象之間的引用問題導(dǎo)致內(nèi)存泄露,因為定時器強引用了self,而本身又被runloop強引用。所以timerself都得不到釋放,所以定時器一直存在并觸發(fā)事件,這樣就會導(dǎo)致內(nèi)存泄露。

為了避免內(nèi)存泄露,所以需要在不使用定時器的時候,手動執(zhí)行timer.invalidate()方法。而block方式雖然并不會存在循環(huán)引用情況,但是由于本身被runloop強引用,所以也需要執(zhí)行timer.invalidate()方法,否則定時器還是會一直存在。

invalidate方法有2個功能:一是將timerrunloop中移除,二是timer本身也會釋放它持有的資源

因此經(jīng)常會對timer進行引用。

self.timer = timer

失效定時器

timer.invalidate()
timer = nil

具體的循環(huán)引用例子,后面會有

  • 非重復(fù)定時

非重復(fù)定時器只會執(zhí)行一次,執(zhí)行結(jié)束會自動調(diào)用invalidates方法,這樣能夠防止定時器再次啟動。實現(xiàn)很簡單將repeats設(shè)置為false即可

// target-action方式
func addNoRepeatedTimer() {
    let timer = Timer.scheduledTimer(timeInterval: 1.0,
                                           target: self,
                                         selector:  #selector(fireTimer),
                                         userInfo: nil,
                                         repeats: false)
}

// block方式
func addUnRepeatedTimerWithClosure() {
    if #available(iOS 10.0, *) { // iOS10之后的API
        let timer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: false) { (timer) in
            print("fire timer")
        }
     } else {
        // Fallback on earlier versions
    }
}

@objc func fireTimer() {
     print("fire timer")
 }

定時容忍范圍(Timer Tolerance)

iOS7之后,iOS允許我們?yōu)?code>Timer指定Tolerance,這樣會給你的timer添加一些時間寬容度可以降低它的電力消耗以及增加響應(yīng)。好比如:“我希望1秒鐘運行一次,但是晚個200毫秒我也不介意”。

當(dāng)你指定了時間寬容度,就意味著系統(tǒng)可以在原有時間附加該寬容度內(nèi)的任意時刻觸發(fā)timer。例如,如果你要timer1秒后運行,并有0.5秒的時間寬容度,實際就可能是1秒,1.5秒或1.3秒等。

下面是每秒運行一次的timer,并有0.2秒的時間寬容度

 let timer = Timer.scheduledTimer(timeInterval: 1.0,
                                         target: self,
                                         selector: #selector(fireTimer),
                                         userInfo: nil,
                                         repeats: true)
 timer.tolerance = 0.2

默認的時間寬容度是0,如果一個重復(fù)性timer由于設(shè)定的時間寬容度推遲了一小會執(zhí)行,這并不意味著后續(xù)的執(zhí)行都會晚一會。iOS不允許timer總體上的漂移,也就是說下一次觸發(fā)會快一些。

舉例的話,如果一個timer每1秒運行一次,并有0.5秒的時間寬容度,那么實際可能是這樣:

  • 1.0秒后timer觸發(fā)
  • 2.4秒后timer再次觸發(fā),晚了0.4秒,但是在時間寬容度內(nèi)
  • 3.1秒后timer第三次觸發(fā),和上一次僅差0.7秒,但每次觸發(fā)的時間是按原始時間算的。
    等等…

使用userInfo獲取額外信息

   func getTimerUserInfo() {
        let timer = Timer.scheduledTimer(timeInterval: 1.0,
                                         target: self,
                                         selector:  #selector(fireTimer),
                                         userInfo: ["score": 90],
                                         repeats: false)

    }

    @objc func fire(_ timer: Timer) {
        guard let userInfo = timer.userInfo as? [String: Int],
            let score = userInfo["score"] else {
            return
        }
        print("score: \(score)")
    }

與Run Loop協(xié)同工作

當(dāng)使用下列方法創(chuàng)建timer,需要手動添加timerRun Loop并指定運行模型,上面使用的方法都是自動添加到當(dāng)前的Run Loop并在默認模型(default mode)允許

public  init(timeInterval ti: TimeInterval,
             invocation: NSInvocation,
             repeats yesOrNo: Bool)

public init(timeInterval ti: TimeInterval,
            target aTarget: Any,
            selector aSelector: Selector,
            userInfo: Any?,
            repeats yesOrNo: Bool)

public init(fireAt date: Date,
            interval ti: TimeInterval,
            target t: Any,
            selector s: Selector,
            userInfo ui: Any?,
            repeats rep: Bool)

比如創(chuàng)建timer添加到當(dāng)前的Run Loop

// 手動添加到runloop,指定模型
func addTimerToRunloop() {
    let timer = Timer(timeInterval: 1.0,
                            target: self,
                          selector: #selector(fireTimer),
                          userInfo: nil,
                           repeats: true)
        
   RunLoop.current.add(timer, forMode: .common)
}

iOS開發(fā)中經(jīng)常遇到的場景,tableView上有定時器,當(dāng)用戶用手指觸摸屏幕,定時器會停止執(zhí)行,滾動停止才會恢復(fù)定時。但是這并不是我們所想要的?為什么會出現(xiàn)呢?

主線程的RunLoop里有兩個預(yù)置的 ModekCFRunLoopDefaultModeUITrackingRunLoopMode

這兩個Mode都已經(jīng)被標(biāo)記為”Common”屬性。DefaultModeApp平時所處的狀態(tài),TrackingRunLoopMode是追蹤 ScrollView滑動時的狀態(tài)。當(dāng)你創(chuàng)建一個 Timer并加到 DefaultMode 時,Timer 會得到重復(fù)回調(diào),但此時滑動一個TableView時,RunLoop會將 mode 切換為 TrackingRunLoopMode,這時 Timer就不會被回調(diào),并且也不會影響到滑動操作。

所以當(dāng)你需要一個Timer,在兩個 Mode 中都能得到回調(diào),有如下方法

  • 1、將這個Timer分別加入這兩個 Mode
  RunLoop.current.add(timer, forMode: .default)
  RunLoop.current.add(timer, forMode: .tracking)
  • 2、將 Timer加入到頂層的 RunLoopcommon模式中
RunLoop.current.add(timer, forMode: .common)
  • 3、在子線程中進行Timer的操作,再在主線程中修改UI界面

實際場景

1、利用Timer簡單實現(xiàn)倒計時功能

class TimerViewController: BaseViewController {

    var timer: Timer?
    var timeLeft = 60
    lazy var timeLabel: UILabel = {
        let label = UILabel(frame: CGRect(x: 0, y: 0, width: 60, height: 60))
        label.backgroundColor = UIColor.orange
        label.textColor = UIColor.white
        label.text = "60 s"
        return label
    }()

    override func viewDidLoad() {
        super.viewDidLoad()
        addTimeLabel()
        countDownTimer()
    }

    func addTimeLabel() {
        view.addSubview(timeLabel)
        timeLabel.center = view.center
    }

    func countDownTimer() {
        timer = Timer.scheduledTimer(timeInterval: 1.0,
                                     target: self,
                                     selector: #selector(countTime),
                                     userInfo: nil,
                                     repeats: true)

    }

    @objc func countTime() {
        timeLeft -= 1
        timeLabel.text = "\(timeLeft) s"

        if timeLeft <= 0 {
            timer?.invalidate()
            timer = nil
        }
    }
}

2、定時器的循環(huán)引用

常見的場景:

有兩個控制器ViewControllerAViewControllerBViewControllerA 跳轉(zhuǎn)到ViewControllerB中,ViewControllerB開啟定時器,但是當(dāng)返回ViewControllerA界面時,定時器依然還在走,控制器也并沒有執(zhí)行deinit方法銷毀掉

為何會出現(xiàn)循環(huán)引用的情況呢?原因是:定時器對控制器 (self) 進行了強引用,定時器被runloop引用,定時器得不到釋放,所以控制器也不會被釋放

具體代碼

TimerViewController是第二個界面,實現(xiàn)很簡單,也是初學(xué)者經(jīng)常做的事情,僅僅是啟動一個定時器,在TimerViewController被釋放的時候,釋放定時器

class TimerViewController: BaseViewController {

    var timer: Timer?

    override func viewDidLoad() {
        super.viewDidLoad()
        addRepeatedTimer()
    }

    func addRepeatedTimer() {
        let timer = Timer.scheduledTimer(timeInterval: 1.0,
                                         target: self,
                                         selector: #selector(fireTimer),
                                         userInfo: nil,
                                         repeats: true)
        self.timer = timer
    }

    @objc func fireTimer() {
        print("fire timer")
    }

    func cancelTimer() {
        timer?.invalidate()
        timer = nil
    }

    deinit {
        cancelTimer()
        print("deinit timerviewcontroller")
    }
}

運行程序之后,可以看到進入該視圖控制頁面,定時器正常執(zhí)行,返回上級頁面,定時器仍然執(zhí)行,而且視圖控制也沒有得到釋放。為了解決這個問題,有兩種方法

方式1:

蘋果官方為了給我們解決對象引用的問題,提供了一個新的定時器方法,利用block來解決與視圖控制器的引用循環(huán),但是只適用于iOS10和更高版本:

func addRepeatedTimerWithClosure() {
    if #available(iOS 10.0, *) {
        weak var weakSelf = self
        let timer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { (timer) in
            weakSelf?.doSomething()
        }
        self.timer = timer
     } else {
            // Fallback on earlier versions
    }
}

func doSomething() {
   print("fire timer")
}

方式2:

既然Apple為我們提供了block方式解決循環(huán)引用問題,我們也可以模仿Apple使用block來解決,擴展Timer添加一個新方法來創(chuàng)建Timer

extension Timer {
    class func sh_scheduledTimer(withTimeInterval interval: TimeInterval, repeats: Bool, block: @escaping (Timer) -> Void) -> Timer {
        if #available(iOS 10.0, *) {
           return Timer.scheduledTimer(withTimeInterval: interval,
                                       repeats: repeats,
                                       block: block)
        } else {
            return Timer.scheduledTimer(timeInterval: interval,
                                        target: self,
                                        selector: #selector(timerAction(_:)),
                                        userInfo: block,
                                        repeats: repeats)
        }
    }

    @objc class func timerAction(_ timer: Timer) {
        guard let block = timer.userInfo as? ((Timer) -> Void) else {
            return
        }
        block(timer)
    }
}

由上可知很簡單iOS10還是使用官方API,iOS10以前也是使用的官方API,只不過將target變成了Timer自己,然后將block作為userInfo的參數(shù)傳入,當(dāng)定時器啟動的時候,獲取block,并執(zhí)行。

簡單使用一下

func addNewMethodOfTimer() {
     weak var weakSelf = self
     let timer = Timer.sh_scheduledTimer(withTimeInterval: 1.0, repeats: true) { (timer) in
         weakSelf?.doSomething()
     }
    self.timer = timer
}

運行程序可以看到,controllertimer都得到了釋放

當(dāng)然,除了擴展Timer,也可以創(chuàng)建一個新的類,實現(xiàn)都大同小異,通過中間類切斷強引用。

final class WeakTimer {

    fileprivate weak var timer: Timer?
    fileprivate weak var target: AnyObject?
    fileprivate let action: (Timer) -> Void

    fileprivate init(timeInterval: TimeInterval,
                     target: AnyObject,
                     repeats: Bool,
                     action: @escaping (Timer) -> Void) {
        self.target = target
        self.action = action
        self.timer = Timer.scheduledTimer(timeInterval: timeInterval,
                                          target: self,
                                          selector: #selector(fire),
                                          userInfo: nil,
                                          repeats: repeats)
    }

    class func scheduledTimer(timeInterval: TimeInterval,
                              target: AnyObject,
                              repeats: Bool,
                              action: @escaping (Timer) -> Void) -> Timer {
        return WeakTimer(timeInterval: timeInterval,
                         target: target,
                         repeats: repeats,
                         action: action).timer!
    }

    @objc fileprivate func fire(timer: Timer) {
        if target != nil {
            action(timer)
        } else {
            timer.invalidate()
        }
    }
}

更多詳情可以看 Weak Reference to NSTimer Target To Prevent Retain Cycle

3、定時器的精確

一般情況下使用Timer是沒什么問題,但是對于精確到要求較高可以使用CADisplayLink(做動畫)和GCD,對于CADisplayLink不了解,可以看CADisplayLink的介紹,對于定時器之間的比較,可以看更可靠和高精度的 iOS 定時器

定時器不準(zhǔn)時的原因

  • 定時器計算下一個觸發(fā)時間是根據(jù)初始觸發(fā)時間計算的,下一次觸發(fā)時間是定時器的整數(shù)倍+容差tolerance
  • 定時器是添加到runloop中的,如果runloop阻塞了,調(diào)用或執(zhí)行方法所花費的時間長于指定的時間間隔(第1點計算得到的時間,就會推遲到下一個runloop周期。
  • 定時器是不會嘗試補償在調(diào)用或執(zhí)行指定方法時可能發(fā)生的任何錯過的觸發(fā)
  • runloop的模式影響

高精度的 iOS 定時器

提高調(diào)度優(yōu)先級:

#include <mach/mach.h>
#include <mach/mach_time.h>
#include <pthread.h>

void move_pthread_to_realtime_scheduling_class(pthread_t pthread) {
    mach_timebase_info_data_t timebase_info;
    mach_timebase_info(&timebase_info);

    const uint64_t NANOS_PER_MSEC = 1000000ULL;
    double clock2abs = ((double)timebase_info.denom / (double)timebase_info.numer) * NANOS_PER_MSEC;

    thread_time_constraint_policy_data_t policy;
    policy.period      = 0;
    policy.computation = (uint32_t)(5 * clock2abs); // 5 ms of work
    policy.constraint  = (uint32_t)(10 * clock2abs);
    policy.preemptible = FALSE;

    int kr = thread_policy_set(pthread_mach_thread_np(pthread_self()),
                   THREAD_TIME_CONSTRAINT_POLICY,
                   (thread_policy_t)&policy,
                   THREAD_TIME_CONSTRAINT_POLICY_COUNT);
    if (kr != KERN_SUCCESS) {
        mach_error("thread_policy_set:", kr);
        exit(1);
    }
}

精確延時:

#include <mach/mach.h>
#include <mach/mach_time.h>

static const uint64_t NANOS_PER_USEC = 1000ULL;
static const uint64_t NANOS_PER_MILLISEC = 1000ULL * NANOS_PER_USEC;
static const uint64_t NANOS_PER_SEC = 1000ULL * NANOS_PER_MILLISEC;

static mach_timebase_info_data_t timebase_info;

static uint64_t abs_to_nanos(uint64_t abs) {
    return abs * timebase_info.numer  / timebase_info.denom;
}

static uint64_t nanos_to_abs(uint64_t nanos) {
    return nanos * timebase_info.denom / timebase_info.numer;
}

void example_mach_wait_until(int argc, const char * argv[]) {
    mach_timebase_info(&timebase_info);
    uint64_t time_to_wait = nanos_to_abs(10ULL * NANOS_PER_SEC);
    uint64_t now = mach_absolute_time();
    mach_wait_until(now + time_to_wait);
}

High Precision Timers in iOS / OS X

利用GCD實現(xiàn)一個好的定時器

而眾所周知的是,NSTimer有不少需要注意的地方。

  1. 循環(huán)引用問題

    NSTimer會強引用target,同時RunLoop會強引用未invalidate的NSTimer實例。 容易導(dǎo)致內(nèi)存泄露。
    (關(guān)于NSTimer引起的內(nèi)存泄露可閱讀iOS夯實:ARC時代的內(nèi)存管理 NSTimer一節(jié))

  2. RunLoop問題

    因為NSTimer依賴于RunLoop機制進行工作,因此需要注意RunLoop相關(guān)的問題。NSTimer默認運行于RunLoop的default mode中。
    而ScrollView在用戶滑動時,主線程RunLoop會轉(zhuǎn)到UITrackingRunLoopMode。而這個時候,Timer就不會運行,方法得不到fire。如果想要在ScrollView滾動的時候Timer不失效,需要注意將Timer設(shè)置運行于NSRunLoopCommonModes

  3. 線程問題

    NSTimer無法在子線程中使用。如果我們想要在子線程中執(zhí)行定時任務(wù),必須激活和自己管理子線程的RunLoop。否則NSTimer是失效的。

  4. 不支持動態(tài)修改時間間隔

    NSTimer無法動態(tài)修改時間間隔,如果我們想要增加或減少NSTimer的時間間隔。只能invalidate之前的NSTimer,再重新生成一個NSTimer設(shè)定新的時間間隔。

  5. 不支持閉包。

    NSTimer只支持調(diào)用selector,不支持更現(xiàn)代的閉包語法。

利用DispatchSource來解決上述問題,基于DispatchSource構(gòu)建Timer

class SwiftTimer {
    
    private let internalTimer: DispatchSourceTimer
    
    init(interval: DispatchTimeInterval, repeats: Bool = false, queue: DispatchQueue = .main , handler: () -> Void) {
        
        internalTimer = DispatchSource.makeTimerSource(queue: queue)
        internalTimer.setEventHandler(handler: handler)
        if repeats {
            internalTimer.scheduleRepeating(deadline: .now() + interval, interval: interval)
        } else {
            internalTimer.scheduleOneshot(deadline: .now() + interval)
        }
    }
    
    deinit() {
        //事實上,不需要手動cancel. DispatchSourceTimer在銷毀時也會自動cancel。
        internalTimer.cancel()
    }
    
    func rescheduleRepeating(interval: DispatchTimeInterval) {
        internalTimer.scheduleRepeating(deadline: .now() + interval, interval: interval)
    }
}

原文內(nèi)容

4、后臺定時器繼續(xù)運行

蘋果上面的App一般都是不允許在后臺運行的,比如說:定時器計時,當(dāng)用戶切換到后臺,定時器就被被掛起,等回到App之后,才會Resume

但是任何的app都能夠使用 UIApplication background tasks在后臺運行一小段時間,除此之外沒有其他的辦法。

在后臺運行定時器需要注意:

  • You need to opt into background execution with beginBackgroundTaskWithExpirationHandler.
  • Either create the Timer on the main thread, OR you will need to add it to the mainRunLoop manually withRunLoop.current.add(timer, forMode: .default)

實現(xiàn)如下

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {

    var window: UIWindow?
    var backgroundUpdateTask: UIBackgroundTaskIdentifier = UIBackgroundTaskIdentifier(rawValue: 0)

    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        // Override point for customization after application launch.
        return true
    }

    func applicationDidEnterBackground(_ application: UIApplication) {
        let application = UIApplication.shared

        self.backgroundUpdateTask = application.beginBackgroundTask {
            self.endBackgroundUpdateTask()
        }

        DispatchQueue.global().async {
            let timer = Timer.scheduledTimer(timeInterval: 2, target: self, selector: #selector(self.methodRunAfterBackground), userInfo: nil, repeats: true)
            RunLoop.current.add(timer, forMode: .default)
            RunLoop.current.run()
        }
    }

    @objc func methodRunAfterBackground() {
        print("methodRunAfterBackground")
    }

    func endBackgroundUpdateTask() {
        UIApplication.shared.endBackgroundTask(self.backgroundUpdateTask)
        self.backgroundUpdateTask = UIBackgroundTaskIdentifier.invalid
    }


    func applicationWillEnterForeground(_ application: UIApplication) {
          self.endBackgroundUpdateTask()
    }
}

注意:

  • Apps only get ~ 10 mins (~3 mins as of iOS 7) of background execution - after this the timer will stop firing.
  • As of iOS 7 when the device is locked it will suspend the foreground app almost instantly. The timer will not fire after an iOS 7 app is locked.

內(nèi)容參考Scheduled NSTimer when app is in background,如果想了解后臺任務(wù)Background Modes Tutorial: Getting Started

參考

Timer

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

推薦閱讀更多精彩內(nèi)容