Timer使用指南

Swift的Timer类(前身是NSTimer)是一种灵活规划未来预定事件的方法,可以仅触发一次或不断循环。在这篇指南中我会提供多种使用它的方式,并带有一些常见问题的解决办法。



注意:

我要首先声明,使用timers会有很大的电力消耗。我们会想办法减少它,但任何类型的timers要想触发,都要从静止状态下唤醒系统,并会有相应的消耗。

创建一个循环timer



从最基础的开始,创建并启用一个循环timer来调用一个方法:

let

 timer = Timer.scheduledTimer(timeInterval: 1.0, target: self, selector: 

#selector(fireTimer), userInfo: nil, repeats: true)





这里我们为了测试用了fireTimer()方法:

@objc 

func

 

fireTimer

()

 {

    

print

(

"Timer fired!"

)

}



尽管我们要求timer每隔1.0秒触发一次,iOS会让timer稍微有点宽容度——可能你的timer很难精确的间隔1.0秒触发。



另一个创建循环timer的常用方法是使用闭包:

let

 timer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: 

true

) { timer 

in



    

print

(

"Timer fired!"

)

}



这些初始化都可以创建timer,不需要把它存在某个属性中,但那样做会比较好,能够方便晚些终止这个timer。因为闭包方法每次代码运行时都要通过timer,你也可以从这方面终止它。

创建一个非循环timer



如果你想代码只运行一次,就把repeats: true改成repeats: false

let

 timer1 = Timer.scheduledTimer(timeInterval: 1.0, target: self, selector: 

#selector(fireTimer), userInfo: nil, repeats: false)





let

 timer2 = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: 

false

) { timer 

in



    

print

(

"Timer fired!"

)

}



其他代码不变

尽管这种方法听上去很完美,我个人还是推荐用GCD来实现

DispatchQueue.main.asyncAfter(deadline: .now() + 1) {

    

print

(

"Timer fired!"

)

}



结束一个timer



你可以调用invalidate()方法来销毁已存在的timer。

例如下面代码创建每秒打印“Timer fired!”1次,共打印3次的timer,之后终止它。

var

 runCount = 

0





Timer.scheduledTimer(withTimeInterval: 

1.0

, repeats: 

true

) { 

timer 

in



    

print

(

"Timer fired!"

)

    runCount +

1





    

if

 runCount == 

3

 {

        timer.invalidate()

    }

}



如果通过一个方法结束一个timer,首先需要声明一个timer和一个runCount属性

var

 timer: Timer?

var

 runCount = 

0





之后规划好timer

timer

 = Timer.scheduledTimer(timeInterval: 

1.0

, target: self, selector: #selector(fireTimer), userInfo: nil, repeats: 

true

)



最后,填写fireTimer()方法

@objc 

func

 

fireTimer

()

 {

    

print

(

"Timer fired!"

)

    runCount += 

1





    

if

 runCount == 

3

 {

        timer?.invalidate()

    }

}



另一种方法是,让fireTimer()接收timer作为其参数,这样就不需要使用timer属性。需要这样重写fireTimer()

@objc 

func

 

fireTimer

(timer: Timer)

 {

    

print

(

"Timer fired!"

)

    runCount += 

1





    

if

 runCount == 

3

 {

        timer.invalidate()

    }

}



附加context



当你创建timer来执行一个方法时,你可以附加一些context,用于存储额外的timer触发条件信息。它是一个字典,可以存任意量的数据——比如触发timer的事件,用户在做些什么,哪个table view被选中等等



比如我们可以让这个字典包含有一个用户名:

let

 context = [

"user"

"

@twostraws

"

]

Timer.scheduledTimer(timeInterval: 

1

.

0

, target: self, selector: 

#selector(fireTimer), userInfo: context, repeats: true)





我们之后可以通过查看timer 参数的userInfo属性来读取fireTimer()

@

objc func 

fireTimer

(

timer: Timer

{

    guard 

let

 context = timer.userInfo 

as

? [String: String] 

else

 { 

return

 }

    

let

 user = context[

"user"

default

"Anonymous"

]



    print(

"Timer fired by \(user)!"

)

    runCount += 

1





    

if

 runCount == 

3

 {

        timer.invalidate()

    }

}



添加一些时间宽容度(tolerance)



给你的timer添加一些时间宽容度可以降低它的电力消耗。它允许你给系统留一些timer执行时间的冗余。“我希望1秒钟运行一次,但是晚个200毫秒我也不介意”。这允许系统协同运行多个timer,把多个timer事件合并到一起,节省电池寿命。



当你指定了时间宽容度,就意味着系统可以在原有时间附加该宽容度内的任意时刻触发timer。例如,如果你要timer 1秒后运行,并有0.5秒的时间宽容度,实际就可能是1秒,1.5秒或1.3秒等。



下例中创建了一个1秒运行一次的timer,并有0.2秒的时间宽容度:

let

 timer = Timer.scheduledTimer(timeInterval: 1.0, target: self, selector: 

#selector(fireTimer), userInfo: nil, repeats: true)



timer.tolerance = 0.2



默认的时间宽容度是0,但是系统会自动添加一个很小的宽容度。

如果一个重复性timer由于设定的时间宽容度推迟了一小会执行,这并不意味着后续的执行都会晚一会。iOS不允许timer总体上的漂移,也就是说下一次触发会快一些。

举例的话,如果一个timer每1秒运行一次,并有0.5秒的时间宽容度,那么实际可能是这样:



  • 1.0秒后timer触发

  • 2.4秒后timer再次触发,晚了0.4秒,但是在时间宽容度内

  • 3.1秒后timer第三次触发,和上一次仅差0.7秒,但每次触发的时间是按原始时间算的。

  • 等等…

  • 与runloops协同使用



    在app中实际使用中,人们经常会遇到timer并没有触发的情况。比如用户用手指触摸屏幕,滚动一个table view的时候,即使设定好条件timer也不会触发。



    这是由于我们默认把timer创建为defaultRunLoopMode,这是我们app的主线程。所以当用户与UI正在互动时会暂停,当用户停下后才再次触发。



    最简单的解决办法是在创建timer时不直接规划它,而是手动把它添加到一个runloop中。本例中,我们选用了.commonModes:即使UI正在使用,它也允许timer触发。

    let

     context = [

    "user"

    "@twostraws"

    ]

    let

     timer = Timer(timeInterval: 

    1.0

    , target: self, selector: 

    #selector(fireTimer), userInfo: context, repeats: true)



    RunLoop.current.

    add

    (timer, forMode: .commonModes)



    把timer与屏幕刷新同步



    一些人,尤其是游戏开发者,会尝试在每帧被绘制之前让timer完成一些工作。

    但这是错误的:timer并不具备这么高的精确度,人们也无法知道上一帧被绘制后过去了多少时间。你也许会设置每秒运行60或120次代码,但实际上在你的timer触发之前可能半数都被跳过了。



    所以如果你想要一些代码在屏幕刷新后立即运行,你要使用CADisplayLink。下面是一些关于CADisplayLink的代码

    let

     displayLink = CADisplayLink(target: self, selector: 

    #selector(fireTimer))



    displayLink.

    add

    (to: .current, forMode: .defaultRunLoopMode)



    别忘了,如果你想要DisplayLink方法在UI被使用时也能触发,请指定.commonModes,而不是用.defaultRunLoopMode。



    相关推荐:

  • Runtime的应用

  • iOS底层原理总结 - Category的本质

  • WKWebView的15条应用指南



  • Timer使用指南