多線程之4-RunLoop

什么是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):


image.png

RunLoop 的結(jié)構(gòu)如下


image.png

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):

image.png

需要注意的就是黃色區(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)鸭蛙。

image.png

蘋果開發(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)視。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末怯晕,一起剝皮案震驚了整個濱河市潜圃,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌舟茶,老刑警劉巖谭期,帶你破解...
    沈念sama閱讀 218,755評論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件堵第,死亡現(xiàn)場離奇詭異,居然都是意外死亡隧出,警方通過查閱死者的電腦和手機踏志,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,305評論 3 395
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來胀瞪,“玉大人针余,你說我怎么就攤上這事∑嗟” “怎么了圆雁?”我有些...
    開封第一講書人閱讀 165,138評論 0 355
  • 文/不壞的土叔 我叫張陵,是天一觀的道長幔摸。 經(jīng)常有香客問我摸柄,道長,這世上最難降的妖魔是什么既忆? 我笑而不...
    開封第一講書人閱讀 58,791評論 1 295
  • 正文 為了忘掉前任驱负,我火速辦了婚禮,結(jié)果婚禮上患雇,老公的妹妹穿的比我還像新娘跃脊。我一直安慰自己,他們只是感情好苛吱,可當(dāng)我...
    茶點故事閱讀 67,794評論 6 392
  • 文/花漫 我一把揭開白布酪术。 她就那樣靜靜地躺著,像睡著了一般翠储。 火紅的嫁衣襯著肌膚如雪绘雁。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,631評論 1 305
  • 那天援所,我揣著相機與錄音庐舟,去河邊找鬼。 笑死住拭,一個胖子當(dāng)著我的面吹牛挪略,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播滔岳,決...
    沈念sama閱讀 40,362評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼杠娱,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了谱煤?” 一聲冷哼從身側(cè)響起摊求,我...
    開封第一講書人閱讀 39,264評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎趴俘,沒想到半個月后睹簇,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體奏赘,經(jīng)...
    沈念sama閱讀 45,724評論 1 315
  • 正文 獨居荒郊野嶺守林人離奇死亡寥闪,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,900評論 3 336
  • 正文 我和宋清朗相戀三年太惠,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片疲憋。...
    茶點故事閱讀 40,040評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡凿渊,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出缚柳,到底是詐尸還是另有隱情埃脏,我是刑警寧澤,帶...
    沈念sama閱讀 35,742評論 5 346
  • 正文 年R本政府宣布秋忙,位于F島的核電站彩掐,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏灰追。R本人自食惡果不足惜堵幽,卻給世界環(huán)境...
    茶點故事閱讀 41,364評論 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望弹澎。 院中可真熱鬧朴下,春花似錦、人聲如沸苦蒿。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,944評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽佩迟。三九已至团滥,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間报强,已是汗流浹背灸姊。 一陣腳步聲響...
    開封第一講書人閱讀 33,060評論 1 270
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留躺涝,地道東北人厨钻。 一個月前我還...
    沈念sama閱讀 48,247評論 3 371
  • 正文 我出身青樓,卻偏偏與公主長得像坚嗜,于是被迫代替她去往敵國和親夯膀。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,979評論 2 355

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