什么是RunLoop
顧名思義,RunLoop就是在‘跑圈’蝙斜,其本質(zhì)是一個do
while循環(huán)孕荠。RunLoop提供了這么一種機制稚伍,當(dāng)有任務(wù)處理時个曙,線程的RunLoop會保持忙碌垦搬,而在沒有任何任務(wù)處理時悼沿,會讓線程休眠糟趾,從而讓出CPU义郑。當(dāng)再次有任務(wù)需要處理時非驮,RunLoop會被喚醒劫笙,來處理事件填大,直到任務(wù)處理完畢允华,再次進入休眠磷蜀。
為什么會有這樣一種機制呢褐隆?
- 大家都知道妓灌,一個線程的生命周期分為創(chuàng)建虫埂、就緒掉伏、運行斧散、阻塞和死亡鸡捐,當(dāng)一個線程上的任務(wù)執(zhí)行完畢箍镜,這個線程就會死亡色迂,所占的資源也就會被回收歇僧,當(dāng)頻繁開啟異步操作的時候诈悍,就意味著頻繁創(chuàng)建和銷毀線程兽埃,創(chuàng)建和銷毀線程是要消耗一些性能的慕趴,所以RunLoop 可以在一定程度上解決這個問題冕房。
- 按常理來講耙册,線程中的任務(wù)執(zhí)行完畢详拙,線程要被釋放蔓同,但是卻有一個特例斑粱,Thread.main矿微,主線程好像一直都存在尚揣,不論我們什么時候使用主線程執(zhí)行代碼快骗,主線程都在那里等著你思灌。其實很好理解恭取,如果主線程死亡了耗跛,APP 不就沒了嗎调塌?所以這里也有runLoop 的功勞羔砾。
如果繼續(xù)問:RunLoop是怎么實現(xiàn)休眠機制的姜凄?RunLoop都可以處理哪些任務(wù),又是怎么處理的呢董虱?RunLoop在iOS系統(tǒng)中都有什么應(yīng)用呢?這些東西在寫代碼過程中可能不會直接用到捐友,但是了解他的原理,對我們平時開發(fā)避坑是有一定作用的撮慨。
RunLoop 的結(jié)構(gòu)組成
RunLoop位于蘋果的Core Foundation庫中砌溺,而Core Foundation庫則位于iOS架構(gòu)分層的Core Service層中(值得注意的是规伐,Core Foundation是一個跨平臺的通用庫,不僅支持Mac肌厨,iOS吵护,同時也支持Windows):
RunLoop 的結(jié)構(gòu)如下
RunLoop提供了如下功能(括號中CF**表明了在CF庫中對應(yīng)的數(shù)據(jù)結(jié)構(gòu)名稱):
RunLoop(CFRunLoop)使你的線程保持忙碌(有事干時)或休眠狀態(tài)(沒事干時)間切換(由于休眠狀態(tài)的存在,使你的線程不至于意外退出)瓮恭。
RunLoop提供了處理事件源(source0屯蹦,source1)機制(CFRunLoopSource)登澜。
RunLoop提供了對Timer的支持(CFRunLoopTimer)帖渠。
RunLoop自身會在多種狀態(tài)間切換(run,sleep,exit等),在狀態(tài)切換時狞甚,RunLoop會通知所注冊的Observer(CFRunLoopObserver)哼审,使得系統(tǒng)可以在特定的時機執(zhí)行對應(yīng)的操作涩盾。相關(guān)的如AutoreleasePool 的Pop/Push春霍,手勢識別等叶眉。
RunLoop在run時衅疙,會進入如下圖所示的do while循環(huán):
需要注意的就是黃色區(qū)域的消息處理中并不包含source0喧伞,因為它在循環(huán)開始之初就會處理,整個流程其實就是一種Event Loop的實現(xiàn)次舌,其他平臺均有類似的實現(xiàn)彼念,只是這里叫做Runloop哲思。但是既然RunLoop是一個消息循環(huán)棚赔,誰來管理和運行Runloop?那么它接收什么類型的消息徘郭?休眠過程是怎么樣的靠益?如何保證休眠時不占用系統(tǒng)資源?如何處理這些消息以及何時退出循環(huán)残揉?
盡管CFRunLoopPerformBlock在上圖中作為喚醒機制有所體現(xiàn)胧后,但事實上執(zhí)行CFRunLoopPerformBlock只是入隊,下次RunLoop運行才會執(zhí)行抱环,而如果需要立即執(zhí)行則必須調(diào)用CFRunLoopWakeUp壳快。
RunLoopMode
Runloop總是運行在某種特定的CFRunLoopModeRef下(每次運行__CFRunLoopRun()函數(shù)時必須指定Mode)。而通過CFRunloopRef對應(yīng)結(jié)構(gòu)體的定義可以很容易知道每種Runloop都可以包含若干個Mode镇草,每個Mode又包含Source/Timer/Observer。每次調(diào)用Runloop的主函數(shù)__CFRunLoopRun()時必須指定一種Mode,這個Mode稱為** _currentMode**,當(dāng)切換Mode時必須退出當(dāng)前Mode,然后重新進入Runloop以保證不同Mode的Source/Timer/Observer互不影響。
有些難以理解?來看一下創(chuàng)建Timer 的代碼撩笆,類比一下
let timer = Timer(timeInterval: 1.0, target: self, selector: #selector(timerRun(_:)), userInfo: nil, repeats: true)
RunLoop.main.add(timer, forMode: .common)
這個就是在主線程的runLoop里面加了個timer,設(shè)置mode為 common。
系統(tǒng)默認提供的Run Loop Modes有kCFRunLoopDefaultMode(NSDefaultRunLoopMode)和UITrackingRunLoopMode,需要切換到對應(yīng)的Mode時只需要傳入對應(yīng)的名稱即可颠悬。前者是系統(tǒng)默認的Runloop Mode灾票,例如進入iOS程序默認不做任何操作就處于這種Mode中正什,此時滑動UIScrollView主经,主線程就切換Runloop到到UITrackingRunLoopMode鉴腻,不再接受其他事件操作(除非你將其他Source/Timer設(shè)置到UITrackingRunLoopMode下)。
但是對于開發(fā)者而言經(jīng)常用到的Mode還有一個kCFRunLoopCommonModes(NSRunLoopCommonModes),其實這個并不是某種具體的Mode,而是一種模式組合志鞍,在iOS系統(tǒng)中默認包含了
NSDefaultRunLoopMode和 UITrackingRunLoopMode(注意:并不是說Runloop會運行在kCFRunLoopCommonModes這種模式下此洲,而是相當(dāng)于分別注冊了 NSDefaultRunLoopMode和 UITrackingRunLoopMode衷畦。當(dāng)然你也可以通過調(diào)用CFRunLoopAddCommonMode()方法將自定義Mode放到 kCFRunLoopCommonModes**組合)。
RunLoop和線程的關(guān)系
Runloop是基于pthread進行管理的纹磺,pthread是基于c的跨平臺多線程操作底層API。它是mach thread的上層封裝(可以參見Kernel Programming Guide)瞬痘,和NSThread一一對應(yīng)(而NSThread是一套面向?qū)ο蟮腁PI,所以在iOS開發(fā)中我們也幾乎不直接使用pthread)鸭蛙。
蘋果開發(fā)的接口中并沒有直接創(chuàng)建Runloop的接口红符,如果需要使用Runloop通常CFRunLoopGetMain()和CFRunLoopGetCurrent()兩個方法來獲取糜芳。
只有當(dāng)我們使用線程的方法主動get Runloop時才會在第一次創(chuàng)建該線程的Runloop,同時將它保存在全局的Dictionary中(線程和Runloop二者一一對應(yīng)),默認情況下線程并不會創(chuàng)建Runloop(主線程的Runloop比較特殊郁竟,任何線程創(chuàng)建之前都會保證主線程已經(jīng)存在Runloop)瘸彤,同時在線程結(jié)束的時候也會銷毀對應(yīng)的Runloop。
iOS開發(fā)過程中對于開發(fā)者而言更多的使用的是Runloop,它默認提供了三個常用的run方法:
open func run()
open func run(until limitDate: Date)
open func run(mode: RunLoop.Mode, before limitDate: Date) -> Bool
- run方法對應(yīng)上面CFRunloopRef中的CFRunLoopRun并不會退出枝秤,除非調(diào)用CFRunLoopStop();通常如果想要永遠不會退出RunLoop才會使用此方法,否則可以使用runUntilDate。
- runMode:beforeDate:則對應(yīng)CFRunLoopRunInMode(mode,limiteDate,true)方法,只執(zhí)行一次觉壶,執(zhí)行完就退出;通常用于手動控制RunLoop(例如在while循環(huán)中)蹬碧。
- runUntilDate:方法其實是CFRunLoopRunInMode(kCFRunLoopDefaultMode,limiteDate,false)翔始,執(zhí)行完并不會退出,繼續(xù)下一次RunLoop直到timeout.
RunLoop應(yīng)用
Timer
前面提到Timer Source作為事件源,事實上它的上層對應(yīng)就是Timer(其實就是CFRunloopTimerRef)這個開發(fā)者經(jīng)常用到的定時器(底層基于使用mk_timer實現(xiàn)),甚至很多開發(fā)者接觸RunLoop還是從Timer開始的窿撬。其實Timer定時器的觸發(fā)正是基于RunLoop運行的,所以使用Timer之前必須注冊到RunLoop色洞,但是RunLoop為了節(jié)省資源并不會在非常準(zhǔn)確的時間點調(diào)用定時器媚污,如果一個任務(wù)執(zhí)行時間較長,那么當(dāng)錯過一個時間點后只能等到下一個時間點執(zhí)行廷雅,并不會延后執(zhí)行(Timer提供了一個tolerance屬性用于設(shè)置寬容度耗美,如果確實想要使用Timer并且希望盡可能的準(zhǔn)確,則可以設(shè)置此屬性)航缀。
Timer的創(chuàng)建通常有兩種方式商架,盡管都是類方法,一種是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
二者最大的區(qū)別就是后者除了創(chuàng)建一個定時器外會自動以NSDefaultRunLoopModeMode添加到當(dāng)前線程RunLoop中,不添加到RunLoop中的Timer是無法正常工作的灿巧。例如下面的代碼中如果timer2不加入到RunLoop中是無法正常工作的赶袄。同時注意如果滾動UIScrollView(UITableView、UICollectionview是類似的)二者是無法正常工作的抠藕,但是如果將RunLoop.Mode.default改為RunLoop.Mode.common則可以正常工作饿肺,這也解釋了前面介紹的Mode內(nèi)容。
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盾似,居然發(fā)現(xiàn)敬辣,deinit 語句并沒有輸出,到底是人性的缺失還是道德的淪喪颜说?
對于普通的對象而言购岗,執(zhí)行完viewDidLoad方法之后(準(zhǔn)確的說應(yīng)該是執(zhí)行完viewDidLoad方法后的的一個RunLoop運行結(jié)束)timer2應(yīng)該會被釋放,但事實上timer2并沒有被釋放门粪。原因是:為了確保定時器正常運轉(zhuǎn),當(dāng)加入到RunLoop以后系統(tǒng)會對Timer執(zhí)行一次retain操作烹困。
在創(chuàng)建Timer1 和 timer2時指定了target為self玄妈,這樣一來造成了timer1和timer2對ViewController1有一個強引用。解決這個問題的方法通常有兩種:一種是將target分離出來獨立成一個對象(在這個對象中創(chuàng)建NSTimer并將對象本身作為Timer的target),控制器通過這個對象間接使用Timer拟蜻;另一種方式的思路仍然是轉(zhuǎn)移target绎签,只是可以直接增加Timer擴展(分類),讓Timer自身做為target酝锅,同時可以將操作selector封裝到block中诡必。后者相對優(yōu)雅,也是目前使用較多的方案(目前有大量類似的封裝搔扁,例如:NSTimer+Block)爸舒。顯然Apple也認識到了這個問題,如果你可以確保代碼只在iOS 10下運行就可以使用iOS 10新增的系統(tǒng)級block方案(上面的代碼中已經(jīng)貼出這種方法)稿蹲。
當(dāng)然使用上面第二種方法可以解決控制器無法釋放的問題扭勉,但是會發(fā)現(xiàn)即使控制器被釋放了兩個定時器仍然正常運行,要解決這個問題就需要調(diào)用Timer的invalidate方法(注意:無論是重復(fù)執(zhí)行的定時器還是一次性的定時器只要調(diào)用invalidate方法則會變得無效苛聘,只是一次性的定時器執(zhí)行完操作后會自動調(diào)用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的關(guān)系
在RunLoop的源代碼中可以看到用到了GCD的相關(guān)內(nèi)容,但是RunLoop本身和GCD并沒有直接的關(guān)系设哗。當(dāng)調(diào)用了dispatch_async(dispatch_get_main_queue(), <#^(void)block#>)時libDispatch會向主線程RunLoop發(fā)送消息喚醒RunLoop唱捣,RunLoop從消息中獲取block,并且在CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE回調(diào)里執(zhí)行這個block网梢。不過這個操作僅限于主線程爷光,其他線程dispatch操作是全部由libDispatch驅(qū)動的。
RunLoop 的其他使用
RunLoop包含多個Mode澎粟,而它的Mode又是可以自定義的蛀序,這么推斷下來其實無論是Source1、Timer還是Observer開發(fā)者都可以利用活烙,但是通常情況下不會自定義Timer徐裸,更不會自定義一個完整的Mode,利用更多的其實是Observer和Mode的切換啸盏。
例如很多人都熟悉的使用perfromSelector在默認模式下設(shè)置圖片重贺,防止UITableView滾動卡頓([[UIImageView allocinitWithFrame:CGRectMake(0, 0, 100, 100)] performSelector:@selector(setImage:) withObject:myImage afterDelay:0.0 inModes:@NSDefaultRunLoopMode])。還有sunnyxx的UITableView+FDTemplateLayoutCell利用Observer在界面空閑狀態(tài)下計算出UITableViewCell的高度并進行緩存回懦。再有老譚的PerformanceMonitor關(guān)于iOS實時卡頓監(jiān)控气笙,同樣是利用Observer對RunLoop進行監(jiān)視。