對,我就是RunLoop(代碼也會講故事系列)

#以下是RunLoop和iOS搬磚程序員的訪談#

"請問你是?"

"不用請問,我就是RunLoop"

“你好,我是iOS開發者,我聽說過你,不過抱歉,對你的名聲我早有耳聞,只是不很熟悉。”

”嗯,不難理解。畢竟我在幕後,你在臺前,我是說句不妄言的話,沒有我,你們就別想玩的轉。“

”哦 ? 這麼說的話,我確實很好奇,請問你能不能介紹下你自己!

RunLoop的概念

人如其名,我就是RunLoooooooooooooooop,像是一個死循環,不停的跑圈,永不懈怠。除非程序不啓動,或者你們代碼寫的太差,以至於crash,我纔不得不停止了。

可是程序啓動與否和你有什麼關係?

程序啓動伊始,有一段代碼:

int main(int argc, char * argv[]) {
     @autoreleasepool {
        return UIApplicationMain(argc, argv, nil, NSStringFromClass([YourAppDelegate class]));
    }
}

這段代碼永遠不會執行結束,不然程序也停止了。所以我伴隨着程序的啓動,一直存在。
當然,我不可能無意義的瞎跑。我貫穿整個程序,在奔跑的過程中幫忙處理各種事情。


我主觀上聽明白了你的意思,但冒昧的說一句,除了瞎跑我還真不知道你到底做了啥?

我理解,我理解,畢竟你們花費大量的時間和UIKit和Foundation的各種類打交道,絲毫不顧及我的存在感。但你有沒有好奇過,你的事件響應,各種手勢識別,定時器都是怎麼傳遞的啊?

抱歉,你這麼一問,我確實欠考慮了。

對啊,所以這就是我從幕後走向臺前的目的。寫代碼不能只看表面,還要挖挖本質。話說回來,官方點說呢,我就是消息機制的處理模式,從線程start到線程end,一直在循環檢測,檢測inputSource(如點擊,雙擊等操作)同步事件,檢測timeSource同步事件,檢測到輸入源後會執行處理函數,首先會產生通知,CoreFunction向線程添加runLoop Observers來監聽事件,意在監聽事件發生時來做處理。

RunLoop和線程之間的關係

麻煩停一下,我只知道很多事情是線程來做的,比如頁面刷新交給主線程,異步線程來下載。但聽你的口氣,這些功勞都是你的了?

對,從表面看來你們都是在操作線程,但這只是表面。我和線程是綁定在一起的。每個線程都有一個對應的 Runloop 對象。當然不同線程的RunLoop是有區別的,主線程(也是你們常說的UI線程)的 Runloop 會在應用啓動時完成啓動,其他線程的 Runloop 默認並不會啓動,只是在需要使用時,你們手動啓動。

RunLoop不同的Mode

你的意思是,每一個線程都有一個RunLoop,但默認情況下只有主線程的纔會開啓。

對,不僅每個線程都有RunLoop,而且每一個 RunLoop都包含若干個 Mode,這麼給你解釋吧,你肯定玩過LOL吧,知道里面有鞋子的裝備吧。

這個倒忘不掉。

那就好說了,正常來說,每個鞋子都有自己偏重的功能屬性,不同英雄都只會買一種鞋子對不對。我也一樣,我雖然有多個Mode,但就像穿鞋一樣,每次調用 RunLoop 的主函數時,只能指定其中一個 Mode,這個Mode被稱作 CurrentMode。如果需要切換 Mode,只能退出 RunLoop,再重新指定一個 Mode 進入。這就像換鞋子一樣,必須先脫掉舊鞋才能穿上新鞋。

當然,鞋子不同,有的偏重法術,有的偏重攻速,你所謂的Mode是怎麼區分不同的?

其實每個 Mode 包含若干個 Source/Timer/Observer,這些都屬於Mode的item,item不同,Mode也不同,RunLoop分爲五類。

NSDefaultRunLoopMode    //大多數工作中默認的運行方式
NSConnectionReplyMode    //使用這個Mode去監聽NSConnection對象的狀態,我們很少需要自己使用這個Mode
NSModalPanelRunLoopMode    //在Model Panel情況下去區分事件(OS X開發中會遇到)
NSEventTrackingRunLoopMode    //跟蹤來自用戶交互的事件(比如UITableView上下滑動)
NSRunLoopCommonModes    //這是一個僞模式,其爲一組run loop mode的集合

雖然模式很多,但iOS中公開暴露出來的只有 NSDefaultRunLoopMode 和 NSRunLoopCommonModes。 NSRunLoopCommonModes 實際上是一個 Mode 的集合,默認包括 NSDefaultRunLoopMode 和 NSEventTrackingRunLoopMode。

這裏就有點抽象了,這麼多Mode你是怎麼選擇的呢?

主線程的 RunLoop 裏有兩個預置的 Mode:NSDefaultRunLoopMode和 NSEventTrackingRunLoopMode。這兩個 Mode 都已經被標記爲"Common"屬性。DefaultMode 是 App 平時所處的狀態,TrackingRunLoopMode 是追蹤 ScrollView 滑動時的狀態。當你創建一個 Timer 並加到 DefaultMode 時,Timer 會得到重複回調,但此時滑動一個TableView時,RunLoop 會將 mode 切換爲 TrackingRunLoopMode,這時 Timer 就不會被回調,並且也不會影響到滑動操作。

但如果我想滑動ScrollView時,不影響Timer咋辦?

既然在DefaultMode下,不影響Timer,TrackingRunLoopMode下能滑動,兩者都不影響,就是兩種模式都要就ok了,iOS中的commonModeItems就是就是將DefaultMode和TrackingRunLoopMode組合在一起了,因此只用切換到commonModeItems就ok了。


RunLoop的內部邏輯

哈哈,果真我對你瞭解的太不到位了,原來你是超神的存在。

不不不,超神不至於,其實我也只是給系統跑腿罷了。但我的一舉一動也要接受管理,不能隨便亂來。

誰還能管的了你啊?

怎麼管不了,能力大,責任大。我要時時刻刻接受系統的監督。你們對我的瞭解可能從NSRunLoop開始的,但這實際只是OC對我簡單的封裝,我的底層是C語言庫CFRunLoop,這裏面有一個叫CFRunLoopObserverRef的觀察者,也就是前面我給你提到的Observer,當我的狀態發生改變時,觀察者就會記錄我的變化。

typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
    kCFRunLoopEntry         = (1UL << 0), // 即將進入Loop
    kCFRunLoopBeforeTimers  = (1UL << 1), // 即將處理 Timer
    kCFRunLoopBeforeSources = (1UL << 2), // 即將處理 Source
    kCFRunLoopBeforeWaiting = (1UL << 5), // 即將進入休眠
    kCFRunLoopAfterWaiting  = (1UL << 6), // 剛從休眠中喚醒
    kCFRunLoopExit          = (1UL << 7), // 即將退出Loop
};

這個倒不難理解,畢竟UIView,UIController,UIApplication都有類似的管理。

RunLoop與NSTimer的準時觸發

可是RunLoop與NSTimer有什麼關係呢?

NSTimer其實是一種資源,但它要想起作用必須添加到RunLoop中。

NSTimer會是準時觸發事件嗎?

timer不是一種實時的機制,會存在延遲,而且延遲的程度跟當前線程的執行情況有關。

這個怎麼理解?

正常情況下,你指定一個事件2秒之後觸發,但若是此時恰好有一個大規模的連續耗時運算,那timer的執行必然要等到該連續事件處理結束纔會開始執行,此時你就無法保證NSTimer的準時觸發了。當然這只是針對於一次執行的timer,

[NSTimer scheduledTimerWithTimeInterval:1 target:testObject2 selector:@selector(timerAction:) userInfo:nil repeats:YES];

對於重複性事件,情況也不一樣。比如一個程序,你設置了週期性1秒觸發,但是有個耗時事件用時兩秒,此時就無法準確觸發,並且以後會隨着這個延遲繼續延遲。

RunLoop的相關實戰

說了那麼多,我大約感受到你的神奇魔力了,但還是過於抽象和偏理論,有沒有具體的實例來彰顯你的存在感啊!

那是必然,那我虎軀一震,抖一抖我的黑魔法。給你說幾個具體的場景吧。

AutoreleasePool的真諦

問你個問題:從MRC的手動管理內存,到ARC的自動管理,其關鍵因素是什麼?

因爲多了AutoreleasePool,自動釋放池。只是我也不知道AutoreleasePool背後到底幫助我們做了什麼?

哈哈,其實這和我RunLoop有很大的關係,在App啓動後,會在主線程度的RunLoop幫我們創建兩個Observer。
第一個 Observer 只監視了一個事件:監聽事件在Entry(即將進入Loop)期間,其回調內會調用 _objc_autoreleasePoolPush() 創建自動釋放池。
第二個 Observer 監視了兩個事件:在BeforeWaiting(準備進入休眠) 時調用_objc_autoreleasePoolPop() 和 _objc_autoreleasePoolPush() 釋放舊的池並創建新池;在主線程執行的代碼,通常是寫在諸如事件回調、Timer回調內的,被這些inputSource和timeSource包裹。這些回調會被 RunLoop 創建好的 AutoreleasePool 環繞着,所以不會出現內存泄漏,開發者也不必顯示創建 Pool 了。

你這麼一說我就理解了,難怪之前有人告訴我,內存的釋放是在每次RunLoop結束之後呢。

這算是一個場景,當然還有其它的。

關於performSelecter:afterDelay:方法

好的,你接着說。

你有沒有遇到過這樣的場景,我在子線程中執行performSelecter:,一切ok,但加了延時,執行performSelecter:afterDelay:方法時,卻愣是沒反應?

對對對,後來別人告訴我在子線程開了RunLoop就ok了,至於爲何,我現在還是雲裏霧裏的。

其實這和NSTimer有關,當調用 NSObject 的 performSelecter:afterDelay: 後,實際上其內部會創建一個 Timer 並添加到當前線程的 RunLoop 中。而子線程默認沒有開啓RunLoop,就無法執行timer事件,自然就不執行。

原來如此。

GCD

既然聊到了線程問題,我想問下線程之間的通信問題。比如我在異步子線程執行了網絡請求,想把請求回來的結果通過異步主線程dispatch_async(dispatch_get_main_queue(), block),的方式將block回調給主線程,這應該也很你們有關係吧。

對的,對於子線程和主線程之間的通信。當調用 dispatch_async(dispatch_get_main_queue(), block) 時,libDispatch 會向主線程的 RunLoop 發送消息,RunLoop會被喚醒,並從消息中取得這個 block,並在回調 CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE() 裏執行這個 block。但這個邏輯僅限於 dispatch 到主線程,dispatch 到其他線程仍然是由 libDispatch 處理的。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章