什么是RunLoop
顾名思义,RunLoop就是在‘跑圈’,其本质是一个do
while循环。RunLoop提供了这么一种机制,当有任务处理时,线程的RunLoop会保持忙碌,而在没有任何任务处理时,会让线程休眠,从而让出CPU。当再次有任务需要处理时,RunLoop会被唤醒,来处理事件,直到任务处理完毕,再次进入休眠。
为什么会有这样一种机制呢?
- 大家都知道,一个线程的生命周期分为创建、就绪、运行、阻塞和死亡,当一个线程上的任务执行完毕,这个线程就会死亡,所占的资源也就会被回收,当频繁开启异步操作的时候,就意味着频繁创建和销毁线程,创建和销毁线程是要消耗一些性能的,所以RunLoop 可以在一定程度上解决这个问题。
- 按常理来讲,线程中的任务执行完毕,线程要被释放,但是却有一个特例,Thread.main,主线程好像一直都存在,不论我们什么时候使用主线程执行代码,主线程都在那里等着你。其实很好理解,如果主线程死亡了,APP 不就没了吗?所以这里也有runLoop 的功劳。
如果继续问:RunLoop是怎么实现休眠机制的?RunLoop都可以处理哪些任务,又是怎么处理的呢?RunLoop在iOS系统中都有什么应用呢?这些东西在写代码过程中可能不会直接用到,但是了解他的原理,对我们平时开发避坑是有一定作用的。
RunLoop 的结构组成
RunLoop位于苹果的Core Foundation库中,而Core Foundation库则位于iOS架构分层的Core Service层中(值得注意的是,Core Foundation是一个跨平台的通用库,不仅支持Mac,iOS,同时也支持Windows):
RunLoop 的结构如下
RunLoop提供了如下功能(括号中CF**表明了在CF库中对应的数据结构名称):
RunLoop(CFRunLoop)使你的线程保持忙碌(有事干时)或休眠状态(没事干时)间切换(由于休眠状态的存在,使你的线程不至于意外退出)。
RunLoop提供了处理事件源(source0,source1)机制(CFRunLoopSource)。
RunLoop提供了对Timer的支持(CFRunLoopTimer)。
RunLoop自身会在多种状态间切换(run,sleep,exit等),在状态切换时,RunLoop会通知所注册的Observer(CFRunLoopObserver),使得系统可以在特定的时机执行对应的操作。相关的如AutoreleasePool 的Pop/Push,手势识别等。
RunLoop在run时,会进入如下图所示的do while循环:
需要注意的就是黄色区域的消息处理中并不包含source0,因为它在循环开始之初就会处理,整个流程其实就是一种Event Loop的实现,其他平台均有类似的实现,只是这里叫做Runloop。但是既然RunLoop是一个消息循环,谁来管理和运行Runloop?那么它接收什么类型的消息?休眠过程是怎么样的?如何保证休眠时不占用系统资源?如何处理这些消息以及何时退出循环?
尽管CFRunLoopPerformBlock在上图中作为唤醒机制有所体现,但事实上执行CFRunLoopPerformBlock只是入队,下次RunLoop运行才会执行,而如果需要立即执行则必须调用CFRunLoopWakeUp。
RunLoopMode
Runloop总是运行在某种特定的CFRunLoopModeRef下(每次运行__CFRunLoopRun()函数时必须指定Mode)。而通过CFRunloopRef对应结构体的定义可以很容易知道每种Runloop都可以包含若干个Mode,每个Mode又包含Source/Timer/Observer。每次调用Runloop的主函数__CFRunLoopRun()时必须指定一种Mode,这个Mode称为** _currentMode**,当切换Mode时必须退出当前Mode,然后重新进入Runloop以保证不同Mode的Source/Timer/Observer互不影响。
有些难以理解?来看一下创建Timer 的代码,类比一下
let timer = Timer(timeInterval: 1.0, target: self, selector: #selector(timerRun(_:)), userInfo: nil, repeats: true)
RunLoop.main.add(timer, forMode: .common)
这个就是在主线程的runLoop里面加了个timer,设置mode为 common。
系统默认提供的Run Loop Modes有kCFRunLoopDefaultMode(NSDefaultRunLoopMode)和UITrackingRunLoopMode,需要切换到对应的Mode时只需要传入对应的名称即可。前者是系统默认的Runloop Mode,例如进入iOS程序默认不做任何操作就处于这种Mode中,此时滑动UIScrollView,主线程就切换Runloop到到UITrackingRunLoopMode,不再接受其他事件操作(除非你将其他Source/Timer设置到UITrackingRunLoopMode下)。
但是对于开发者而言经常用到的Mode还有一个kCFRunLoopCommonModes(NSRunLoopCommonModes),其实这个并不是某种具体的Mode,而是一种模式组合,在iOS系统中默认包含了
NSDefaultRunLoopMode和 UITrackingRunLoopMode(注意:并不是说Runloop会运行在kCFRunLoopCommonModes这种模式下,而是相当于分别注册了 NSDefaultRunLoopMode和 UITrackingRunLoopMode。当然你也可以通过调用CFRunLoopAddCommonMode()方法将自定义Mode放到 kCFRunLoopCommonModes**组合)。
RunLoop和线程的关系
Runloop是基于pthread进行管理的,pthread是基于c的跨平台多线程操作底层API。它是mach thread的上层封装(可以参见Kernel Programming Guide),和NSThread一一对应(而NSThread是一套面向对象的API,所以在iOS开发中我们也几乎不直接使用pthread)。
苹果开发的接口中并没有直接创建Runloop的接口,如果需要使用Runloop通常CFRunLoopGetMain()和CFRunLoopGetCurrent()两个方法来获取。
只有当我们使用线程的方法主动get Runloop时才会在第一次创建该线程的Runloop,同时将它保存在全局的Dictionary中(线程和Runloop二者一一对应),默认情况下线程并不会创建Runloop(主线程的Runloop比较特殊,任何线程创建之前都会保证主线程已经存在Runloop),同时在线程结束的时候也会销毁对应的Runloop。
iOS开发过程中对于开发者而言更多的使用的是Runloop,它默认提供了三个常用的run方法:
open func run()
open func run(until limitDate: Date)
open func run(mode: RunLoop.Mode, before limitDate: Date) -> Bool
- run方法对应上面CFRunloopRef中的CFRunLoopRun并不会退出,除非调用CFRunLoopStop();通常如果想要永远不会退出RunLoop才会使用此方法,否则可以使用runUntilDate。
- runMode:beforeDate:则对应CFRunLoopRunInMode(mode,limiteDate,true)方法,只执行一次,执行完就退出;通常用于手动控制RunLoop(例如在while循环中)。
- runUntilDate:方法其实是CFRunLoopRunInMode(kCFRunLoopDefaultMode,limiteDate,false),执行完并不会退出,继续下一次RunLoop直到timeout.
RunLoop应用
Timer
前面提到Timer Source作为事件源,事实上它的上层对应就是Timer(其实就是CFRunloopTimerRef)这个开发者经常用到的定时器(底层基于使用mk_timer实现),甚至很多开发者接触RunLoop还是从Timer开始的。其实Timer定时器的触发正是基于RunLoop运行的,所以使用Timer之前必须注册到RunLoop,但是RunLoop为了节省资源并不会在非常准确的时间点调用定时器,如果一个任务执行时间较长,那么当错过一个时间点后只能等到下一个时间点执行,并不会延后执行(Timer提供了一个tolerance属性用于设置宽容度,如果确实想要使用Timer并且希望尽可能的准确,则可以设置此属性)。
Timer的创建通常有两种方式,尽管都是类方法,一种是init(xxxxx),另一种scheduedTimer(XXX)。
public /*not inherited*/ init(timeInterval ti: TimeInterval, invocation: NSInvocation, repeats yesOrNo: Bool)
open class func scheduledTimer(timeInterval ti: TimeInterval, invocation: NSInvocation, repeats yesOrNo: Bool) -> Timer
public /*not inherited*/ init(timeInterval ti: TimeInterval, target aTarget: Any, selector aSelector: Selector, userInfo: Any?, repeats yesOrNo: Bool)
open class func scheduledTimer(timeInterval ti: TimeInterval, target aTarget: Any, selector aSelector: Selector, userInfo: Any?, repeats yesOrNo: Bool) -> Timer
/// Creates and returns a new NSTimer object initialized with the specified block object. This timer needs to be scheduled on a run loop (via -[NSRunLoop addTimer:]) before it will fire.
/// - parameter: timeInterval The number of seconds between firings of the timer. If seconds is less than or equal to 0.0, this method chooses the nonnegative value of 0.1 milliseconds instead
/// - parameter: repeats If YES, the timer will repeatedly reschedule itself until invalidated. If NO, the timer will be invalidated after it fires.
/// - parameter: block The execution body of the timer; the timer itself is passed as the parameter to this block when executed to aid in avoiding cyclical references
@available(iOS 10.0, *)
public /*not inherited*/ init(timeInterval interval: TimeInterval, repeats: Bool, block: @escaping (Timer) -> Void)
/// Creates and returns a new NSTimer object initialized with the specified block object and schedules it on the current run loop in the default mode.
/// - parameter: ti The number of seconds between firings of the timer. If seconds is less than or equal to 0.0, this method chooses the nonnegative value of 0.1 milliseconds instead
/// - parameter: repeats If YES, the timer will repeatedly reschedule itself until invalidated. If NO, the timer will be invalidated after it fires.
/// - parameter: block The execution body of the timer; the timer itself is passed as the parameter to this block when executed to aid in avoiding cyclical references
@available(iOS 10.0, *)
open class func scheduledTimer(withTimeInterval interval: TimeInterval, repeats: Bool, block: @escaping (Timer) -> Void) -> Timer
二者最大的区别就是后者除了创建一个定时器外会自动以NSDefaultRunLoopModeMode添加到当前线程RunLoop中,不添加到RunLoop中的Timer是无法正常工作的。例如下面的代码中如果timer2不加入到RunLoop中是无法正常工作的。同时注意如果滚动UIScrollView(UITableView、UICollectionview是类似的)二者是无法正常工作的,但是如果将RunLoop.Mode.default改为RunLoop.Mode.common则可以正常工作,这也解释了前面介绍的Mode内容。
class ViewController1: UIViewController {
var counter = 1
var timer1: Timer?
override func viewDidLoad() {
self.view.backgroundColor = UIColor.white
self.timer1 = Timer.scheduledTimer(timeInterval: 1.0, target: self, selector: #selector(timerRun(_:)), userInfo: nil, repeats: true)
let timer2 = Timer(timeInterval: 1.0, target: self, selector: #selector(timerRun(_:)), userInfo: nil, repeats: true)
RunLoop.main.add(timer2, forMode: .default)
}
@objc func timerRun(_ timer: Timer) {
if timer == timer1 {
print("timer1 print: \(counter)")
} else {
print("timer2 runing\(counter)")
}
counter += 1
}
deinit {
debugPrint("\(self.classForCoder) deinit!!!!")
}
}
然后我们退出这个vc,居然发现,deinit 语句并没有输出,到底是人性的缺失还是道德的沦丧?
对于普通的对象而言,执行完viewDidLoad方法之后(准确的说应该是执行完viewDidLoad方法后的的一个RunLoop运行结束)timer2应该会被释放,但事实上timer2并没有被释放。原因是:为了确保定时器正常运转,当加入到RunLoop以后系统会对Timer执行一次retain操作。
在创建Timer1 和 timer2时指定了target为self,这样一来造成了timer1和timer2对ViewController1有一个强引用。解决这个问题的方法通常有两种:一种是将target分离出来独立成一个对象(在这个对象中创建NSTimer并将对象本身作为Timer的target),控制器通过这个对象间接使用Timer;另一种方式的思路仍然是转移target,只是可以直接增加Timer扩展(分类),让Timer自身做为target,同时可以将操作selector封装到block中。后者相对优雅,也是目前使用较多的方案(目前有大量类似的封装,例如:NSTimer+Block)。显然Apple也认识到了这个问题,如果你可以确保代码只在iOS 10下运行就可以使用iOS 10新增的系统级block方案(上面的代码中已经贴出这种方法)。
当然使用上面第二种方法可以解决控制器无法释放的问题,但是会发现即使控制器被释放了两个定时器仍然正常运行,要解决这个问题就需要调用Timer的invalidate方法(注意:无论是重复执行的定时器还是一次性的定时器只要调用invalidate方法则会变得无效,只是一次性的定时器执行完操作后会自动调用invalidate方法)。修改后的代码如下:
class ViewController1: UIViewController {
var counter = 1
var timer1: Timer?
var timer2: Timer?
override func viewDidDisappear(_ animated: Bool) {
timer1?.invalidate()
timer2?.invalidate()
}
override func viewDidLoad() {
self.view.backgroundColor = UIColor.white
self.timer1 = Timer.scheduledTimer(timeInterval: 1.0, target: self, selector: #selector(timerRun(_:)), userInfo: nil, repeats: true)
let timer2 = Timer(timeInterval: 1.0, target: self, selector: #selector(timerRun(_:)), userInfo: nil, repeats: true)
RunLoop.main.add(timer2, forMode: .default)
self.timer2 = timer2
}
@objc func timerRun(_ timer: Timer) {
if timer == timer1 {
print("timer1 print: \(counter)")
} else {
print("timer2 runing\(counter)")
}
counter += 1
}
deinit {
debugPrint("\(self.classForCoder) deinit!!!!")
}
}
timer1 print: 1
timer2 runing2
timer1 print: 3
timer2 runing4
timer1 print: 5
timer2 runing6
timer1 print: 7
timer2 runing8
"ViewController1 deinit!!!!"
NSURLSession
(pending 待更新)
GCD和RunLoop的关系
在RunLoop的源代码中可以看到用到了GCD的相关内容,但是RunLoop本身和GCD并没有直接的关系。当调用了dispatch_async(dispatch_get_main_queue(), <#^(void)block#>)时libDispatch会向主线程RunLoop发送消息唤醒RunLoop,RunLoop从消息中获取block,并且在CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE回调里执行这个block。不过这个操作仅限于主线程,其他线程dispatch操作是全部由libDispatch驱动的。
RunLoop 的其他使用
RunLoop包含多个Mode,而它的Mode又是可以自定义的,这么推断下来其实无论是Source1、Timer还是Observer开发者都可以利用,但是通常情况下不会自定义Timer,更不会自定义一个完整的Mode,利用更多的其实是Observer和Mode的切换。
例如很多人都熟悉的使用perfromSelector在默认模式下设置图片,防止UITableView滚动卡顿([[UIImageView allocinitWithFrame:CGRectMake(0, 0, 100, 100)] performSelector:@selector(setImage:) withObject:myImage afterDelay:0.0 inModes:@NSDefaultRunLoopMode])。还有sunnyxx的UITableView+FDTemplateLayoutCell利用Observer在界面空闲状态下计算出UITableViewCell的高度并进行缓存。再有老谭的PerformanceMonitor关于iOS实时卡顿监控,同样是利用Observer对RunLoop进行监视。