在 iOS 开发中,如果遇到需要定时触发一个事件的场景,最常用的就是通过Timer的方式是触发,例如:启动页的倒计时,定时上报日志信息,某个页面的定时网络请求等。最简单的实现方式就是通过 NSTimer 控件:
1 Timer
lazy var timer: Timer? = {
let obj = Timer.scheduledTimer(timeInterval: 1, target: self, selector:
#selector(timerAction), userInfo: nil, repeats: true)
return obj
}()
使用scheduledTimer方式创建timer后,会自动触发任务执行,默认会加入到主线程 RunLoop 中的 defaultMode 中。如果 RunLoop 切换到其他 mode 模式(比如列表滑动时的 UITracingMode )时,defaultMode 中的 timer 就会停止运行。如果需要同时运行 timer, 则需要将 timer 加入到 RunLoop的 commonMode 模式。
将timer加入到 runloop中时,会在 RunLoop 的时间线上注册定时任务,比如timer的 duration 设置为 1s, 则会在 2s、3s、4s... 的时刻,注册timer绑定的任务。所以,被UITracingMode阻碍的任务,当满足执行条件时,会立即执行。如果某任务A被阻塞了 2 次,当切换到defaultMode时,会立即执行 2 次A。
timer作为属性被控制器强引用,同时其又把控制器作为代理对象,强引用控制器,造成了引用的循环,会导致两者在内存中不能释放。一个解决方式是在合适的时机,使 timer 失效,主动打破循环;另一个解决方式是通过中间对象实现弱代理:
class UAWeakTarget: NSObject {
weak var target: AnyObject?
init(_ target: AnyObject) {
self.target = target
}
override func forwardingTarget(for aSelector: Selector!) -> Any? {
return self.target
}
}
//使用方式:
lazy var timer: Timer? = {
let timerT = UAWeakTarget(self)
let obj = Timer.scheduledTimer(timeInterval: 1, target: timerT, selector:
#selector(timerAction), userInfo: nil, repeats: true)
return obj
}()
deinit {
timer?.invalidate()
timer = nil
print("deinit")
}
2 CADisplayLink
通过 CADisplayLink 也可以实现定时任务:
lazy var link: CADisplayLink = {
let obj = CADisplayLink(target: self, selector: #selector(linkAction(_:)))
obj.frameInterval = 60
obj.add(to: RunLoop.current, forMode: .default)
return obj
}()
和timer默认加入到defaultMode中的方式不同,link初始时不会加入到任何 mode 中,需要显式加入到Runloop中,才可以触发link绑定的任务。
和timer一样,加入到defaultMode中的link,其绑定的任务也会在RunLoop切换到UITracingMode时被阻塞。当link绑定的任务被阻塞时,会直接丢弃,等待下一个任务的执行。之所以这样,和 CADispalyLink 运行的机制有关。
CADispalyLink跟随屏幕的刷新频率运行,使用中设置的frameInterval=60的含义是每隔60个屏幕的刷新执行一次任务。我们知道,屏幕刷新的频率为 60次/s, 也就是FPS=60。但这是在屏幕不卡顿的正常情况下,实际中由于页面的复杂程度不同, CPU 和 GPU 执行任务的时间是不确定的。所以link中绑定的任务,要看具体屏幕刷新花费的时间,更准确地说,要看 RunLoop 的执行时间。link被加入到 RunLoop后,当 RunLoop执行了frameInterval 次后,就会调用一次link中的任务。
在一个含有TableView的列表页面中,故意将 Cell 中的元素设置成十分卡顿的方式(各种圆角+透明度),执行link中的方法:
@objc func linkAction(_ link: CADisplayLink) {
print("\(#function): \(link.timestamp)")
}
//运行结果:
/*
linkAction(_:): 341378.114139467
linkAction(_:): 341379.11413948704
linkAction(_:): 341380.114139507
linkAction(_:): 341381.114139527
linkAction(_:): 341382.11413954705
linkAction(_:): 341383.114139567
linkAction(_:): 341384.11413958704
linkAction(_:): 341385.114139607
*/
link.gif
能够肉眼可见页面卡顿,但是link绑定的任务执行并不受影响。是因为页面卡顿由GPU不能及时渲染导致,而CPU运算不受影响,RunLoop能够正常执行。
在link任务中,加入大量计算任务,比如计算10000000字符相加:
@objc func linkAction(_ link: CADisplayLink) {
var sum = ""
for _ in 0...10000000 {
sum += "a"
}
print("\(#function): \(link.timestamp)")
}
//运行结果:
/*
linkAction(_:): 341269.73214639304
linkAction(_:): 341269.74881306
linkAction(_:): 341277.74881322
linkAction(_:): 341285.69881337904
linkAction(_:): 341293.66548020503
*/
能够看出任务执行的时间,大大超过了设定的frameInterval。
CADisplayLink也会导致控制器和link对象的循环引用,在使用时,需要通过主动或者使用中间对象实现弱代理的方式打破循环。
3 DispatchSourceTimer
iOS8.0以后, Apple 加入了DispatchSourceTimer, 可以避免循环引用:
public class UATimer {
public typealias SwiftTimerHandler = (UATimer)->()
public let sourceTimer: DispatchSourceTimer
private var running = false
public init(deadline: DispatchTime = .now(), repeating interval: DispatchTimeInterval = .never,
leeway: DispatchTimeInterval = .nanoseconds(0), handleBlock: SwiftTimerHandler?) {
sourceTimer = DispatchSource.makeTimerSource()
sourceTimer.schedule(deadline: deadline, repeating: interval, leeway: leeway)
sourceTimer.setEventHandler {[weak self] in
guard let self = self else { return }
handleBlock?(self)
}
}
public func start() {
sourceTimer.resume()
running = true
}
deinit {
if !running {
sourceTimer.resume()
}
}
}
lazy var timer_gcd: UATimer = {
let obj = UATimer(repeating: DispatchTimeInterval.seconds(1)) { (swifTimer) in
print("SwiftTimer\(UATimerVC.count) \(Thread.current)")
}
obj.start()
return obj
}()
//运行结果:
/*
SwiftTimer1 <NSThread: 0x600000df55c0>{number = 5, name = (null)}
SwiftTimer2 <NSThread: 0x600000dd7b40>{number = 3, name = (null)}
SwiftTimer3 <NSThread: 0x600000df55c0>{number = 5, name = (null)}
SwiftTimer4 <NSThread: 0x600000df55c0>{number = 5, name = (null)}
SwiftTimer5 <NSThread: 0x600000df55c0>{number = 5, name = (null)}
*/
调用DispatchSourceTimer的resum方法,就会触发handler的执行。从运行结果看,handler中的方法运行在子线程。DispatchSourceTimer 不会和停留页面形成循环引用,其正常销毁需要先调用resum方法,否则会崩溃。
除了以上介绍的 3 种方式,还可通过DispatchAfter的方式,实现一次任务的执行。
总结
Timer和CADisplayLink定时器,会造成循环引用,使用手动或者中间对象弱代理的方式,可以打破该循环。Timer中的定时任务阻塞后,会立即执行;而CADisplayLink中的任务被阻塞会直接丢弃。DispatchSourceTimer定时器不会造成循环引用,其handler中的任务是在子线程执行,销毁之前需要调用resum方法,否则会引起崩溃。完~
参考:
1 iOS的几种定时器及区别
2 打造一个优雅的Timer
3 iOS FPS监测
4 以前的老代码在使用 NSTimer 时出现了内存泄露







网友评论