[iOS]RunLoop - iOS界的EventLoop

1.閑扯

一般除了初學者,大部分人了解runloop可能更多的是在面試或者準備面試的時候。 顯然這種技術在平時的開發中,使用的場景是非常低的,但是對這個知識的了解程度可以作為衡量一個iOSer的‘閑散’程度(一般太忙,需求太多的人基本沒時間研究這個東西??)

其實我也不太懂,只是把自己各方偷師學來的整理一下,整理中也希望自己能有個更深入的理解。嗯開始吧:

2.優秀文檔/資料

列舉下已有的比較好的資料
1> 深入理解RunLoop (by ibireme)

屏幕快照 2016-03-30 下午5.35.17.png

polen:
從cocoachina看到的,基本算是非常非常全的了,很詳細的介紹了你所能了解的全部runloop(為什么我這么確定,因為其實關于runloop的文檔并不多,這篇是我看過里面個人認為最全面也最細節的一篇,五星推薦!!!)

2>看過一個視頻:
某度的@sunnyxx的分享

屏幕快照 2016-03-30 下午5.34.12.png

3>以及有個可愛的童鞋對這個視頻做了簡單的整理:
iOS runloop (作者:小白和小黑)

polen:
如果有心的朋友看一下這個視頻 ,大概96min,孫源這哥們講的非常透徹,之前是百度的,在我寫文章這幾天好像剛從百度離職,下面內容有些截圖就是他視頻里面的,希望他不會怪我偷他的圖片??

3.我來整理

當然也有比較懶的,那就看我的總結吧
備注:以下大部分信息非本人原創,只是作為只是整理使用,
原文鏈接上面已經提及,大家可以直接看

3.1runloop的定義

polen:
首先說明下背景:
runloop不是線程,不是GCD,在一個APP里面不是唯一的

runloop就是一個對象,如果把線程比作一條高速公路,我的理解runoop就是這條道路的管理員,沒事了就睡覺,有事了把他叫醒(注意,這里叫醒的實現,一般是其他線程(大部分是main線程)的把他叫醒,可以留一下這里,后面會偽代碼說到這個問題)。

形象理解的話,就是下圖里面,如果線程是個箭頭線,runloop就是那個圈,一圈又一圈...

屏幕快照 2016-03-30 下午5.36.26.png

有人會覺得runloop好虛,如何直觀的看到runloop,這個很簡單,你打開Xcode,run一段cheng程序,然后打個斷點或者暫停一下,看一下堆棧信息,馬上就可以看到,我們的進程從main函數開始,緊接著馬上回喚醒runloop,然后是再調其其他的函數, 應該說除了main函數和幾個基本的函數,大部分都是runloop調用起來的,截圖如下:

runloop.png

所以和runloop有關的都有哪些東西?

屏幕快照 2016-03-30 下午5.42.19.png

當然,專業的說,本質是eventlop,這個不只是在iOS,任何系統或者語言里面都有類似的東西

一般來講,一個線程一次只能執行一個任務,執行完成后線程就會退出。如果我們需要一個機制,讓線程能隨時處理事件但并不退出,通常的代碼邏輯是這樣的:

Crayon Syntax Highlighter v2.7.1
function loop() {
    initialize();
    do {
        var message = get_next_message();
        process_message(message);
    } while (message != quit);
}

這種模型通常被稱作 Event Loop。 Event Loop 在很多系統和框架里都有實現,比如 Node.js 的事件處理,比如 Windows 程序的消息循環,再比如 OSX/iOS 里的 RunLoop。實現這種模型的關鍵點在于:如何管理事件/消息,如何讓線程在沒有處理消息時休眠以避免資源占用、在有消息到來時立刻被喚醒。
所以,RunLoop 實際上就是一個對象,這個對象管理了其需要處理的事件和消息,并提供了一個入口函數來執行上面 Event Loop 的邏輯。線程執行了這個函數后,就會一直處于這個函數內部 "接受消息->等待->處理" 的循環中,直到這個循環結束(比如傳入 quit 的消息),函數返回。

OSX/iOS 系統中,提供了兩個這樣的對象:NSRunLoop 和 CFRunLoopRef。
CFRunLoopRef 是在 CoreFoundation 框架內的,它提供了純 C 函數的 API,所有這些 API 都是線程安全的。
NSRunLoop 是基于 CFRunLoopRef 的封裝,提供了面向對象的 API,但是這些 API 不是線程安全的。

polen:
這個我有篇內存管理的文章專門提及過,CoreFoundation和Foundation對象在ARC中處理也是不一樣的。
內存優化之ARC和Core Foundation object

CFRunLoopRef 的代碼是開源的,你可以點擊這里 下載到整個 CoreFoundation 的源碼來查看。

Update: Swift 開源后,蘋果又維護了一個跨平臺的 CoreFoundation 版本,這個版本的源碼可能和現有 iOS 系統中的實現略不一樣,但更容易編譯,而且已經適配了 Linux/Windows。

3.2 RunLoop 與線程的關系

CFRunLoop 是基于 pthread 來管理的

蘋果不允許直接創建 RunLoop,它只提供了兩個自動獲取的函數:CFRunLoopGetMain() 和 CFRunLoopGetCurrent()

線程和 RunLoop 之間是一一對應的,其關系是保存在一個全局的 Dictionary 里。線程剛創建時并沒有 RunLoop,如果你不主動獲取,那它一直都不會有。RunLoop 的創建是發生在第一次獲取時,RunLoop 的銷毀是發生在線程結束時。你只能在一個線程的內部獲取其 RunLoop(主線程除外)

polen:
說明下,一個線程只能有唯一對應的runloop;但這個根runloop里可以嵌套子runloops

屏幕快照 2016-03-30 下午5.44.45.png

3 詳細說說潤runloop的內部

屏幕快照 2016-03-30 下午12.03.40.png

一個 RunLoop 包含若干個 Mode,每個 Mode 又包含若干個 Source/Timer/Observer。每次調用 RunLoop 的主函數時,只能指定其中一個 Mode,這個Mode被稱作 CurrentMode。如果需要切換 Mode,只能退出 Loop,再重新指定一個 Mode 進入。這樣做主要是為了分隔開不同組的 Source/Timer/Observer,讓其互不影響。

CFRunLoopSourceRef 是事件產生的地方。Source有兩個版本:Source0 和 Source1。
? Source0 只包含了一個回調(函數指針),它并不能主動觸發事件。使用時,你需要先調用 CFRunLoopSourceSignal(source),將這個 Source 標記為待處理,然后手動調用 CFRunLoopWakeUp(runloop) 來喚醒 RunLoop,讓其處理這個事件。
? Source1 包含了一個 mach_port 和一個回調(函數指針),被用于通過內核和其他線程相互發送消息。這種 Source 能主動喚醒 RunLoop 的線程,其原理在下面會講到。

polen:
簡單理解就是app用到的都是source0, 系統級的調用是source1

【問】:
就是UIButton點擊事件是source0還是source1:
(打印堆棧看的話是從source0調出的)
【答】:
首先是由那個Source1 接收IOHIDEvent,之后在回調 __IOHIDEventSystemClientQueueCallback() 內觸發的 Source0,Source0 再觸發的 _UIApplicationHandleEventQueue()。所以UIButton事件看到是在 Source0 內的。你可以在 __IOHIDEventSystemClientQueueCallback 處下一個 Symbolic Breakpoint 看一下。

CFRunLoopTimerRef 是基于時間的觸發器,它和 NSTimer 是toll-free bridged 的,可以混用。其包含一個時間長度和一個回調(函數指針)。當其加入到 RunLoop 時,RunLoop會注冊對應的時間點,當時間點到時,RunLoop會被喚醒以執行那個回調。

polen:
toll-free bridged :
Core Foundation 和 Foundation 之間交換使用數據類型的技術就叫 Toll-Free Bridging.
這里在ARC體現很明顯,ARC是不處理Core Foundation,解決方案是使用 __bridge, __bridge_retained, __bridge_transfer 等進行指針轉換
詳情可查看:
內存優化之ARC和Core Foundation object
Toll-Free Bridging

CFRunLoopObserverRef 是觀察者,每個 Observer 都包含了一個回調(函數指針),當 RunLoop 的狀態發生變化時,觀察者就能通過回調接受到這個變化。可以觀測的時間點有以下幾個:

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
};

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
};

上面的 Source/Timer/Observer 被統稱為 mode item,一個 item 可以被同時加入多個 mode。但一個 item 被重復加入同一個 mode 時是不會有效果的。如果一個 mode 中一個 item 都沒有,則 RunLoop 會直接退出,不進入循環。

這里有個概念叫 "CommonModes":一個 Mode 可以將自己標記為"Common"屬性(通過將其 ModeName 添加到 RunLoop 的 "commonModes" 中)。每當 RunLoop 的內容發生變化時,RunLoop 都會自動將 _commonModeItems 里的 Source/Observer/Timer 同步到具有 "Common" 標記的所有Mode里。

應用場景舉例:
主線程的 RunLoop 里有兩個預置的 Mode:kCFRunLoopDefaultMode 和 UITrackingRunLoopMode。這兩個 Mode 都已經被標記為"Common"屬性。DefaultMode 是 App 平時所處的狀態,TrackingRunLoopMode 是追蹤 ScrollView 滑動時的狀態。當你創建一個 Timer 并加到 DefaultMode 時,Timer 會得到重復回調,但此時滑動一個TableView時,RunLoop 會將 mode 切換為 TrackingRunLoopMode,這時 Timer 就不會被回調,并且也不會影響到滑動操作。
有時你需要一個 Timer,在兩個 Mode 中都能得到回調,一種辦法就是將這個 Timer 分別加入這兩個 Mode。還有一種方式,就是將 Timer 加入到頂層的 RunLoop 的 "commonModeItems" 中。"commonModeItems" 被 RunLoop 自動更新到所有具有"Common"屬性的 Mode 里去。

屏幕快照 2016-03-30 下午6.00.59.png

3.3RunLoop 的內部邏輯

屏幕快照 2016-03-30 下午6.03.48.png

可以看到,實際上 RunLoop 就是這樣一個函數,其內部是一個 do-w
hile 循環。當你調用 CFRunLoopRun() 時,線程就會一直停留在這個循環里;直到超時或被手動停止,該函數才會返回。

屏幕快照 2016-03-30 下午6.21.59.png

polen:
解釋一下這個圖:

SetupThisRunLoopRunTimoutTimer(); //這個是設置一個過期時間,防止runloop無止境的跑下去,由CGD的timer實現,用于檢測這次runloop跑了多久
do {}里面:
首先
__CFRunLoopOoObservers(...timers);//告訴observer:我要跑timer了(通知觀察者任何即將要開始的定時器)
__CFRunLoopOoObservers(...Sources);//告訴observer:我要跑source了(通知觀察者任何即將啟動的非基于端口的源)

__CFRunLoopOoBlocks();
__CFRoopLoopOoSource0(); //遍歷source0跑

CheckIfExistMessagesInMainDispatchQweue();//詢問GCD是否有主線程的東西,需要我runloop去跑

之后告訴observer我要開始睡,Zzzz...

...

直到它被喚醒 received mach_msg,wake up 

//喚醒的場景:
# a>. 某一事件到達基于端口的源
# b>. 定時器啟動
# c>. Run loop設置的時間已經超時
# d>. Run loop被顯式喚醒


__CFRunLoopOoObservers(kCFRunLoopAfterWaiting) //告訴observer我醒了

接著
if(){
}else if (){
}else{
}...
根據喚醒的端口來處理事務:
1.如果用戶定義的定時器啟動,處理定時器事件并重啟run loop。再次進入__CFRunLoopOoObservers(...timers);
2.如果是被GCD喚醒,則調用GCD的事件
3.其他場景是由source1 (基于port事件)觸發,做對應的事件處理(比如網絡等等) 


總結下:
這里就是如果在do里面睡眠,就一直睡;
如果沒有睡眠,同時沒有超時(說明被喚醒了),就開始在while里執行各種runloop的東西

RunLoop的掛起與喚醒

1.制定用于喚醒的mach_port端口
2.調用mach_msg監聽喚醒端口,被喚醒前,系統內核將這個線程掛起,停留在mach_msg_trap
3.由另外一個線程(或另一個進程中的某個線程)向內核發送這個端口的msg后,trap狀態被喚醒,RunLoop繼續開始干活
屏幕快照 2016-03-30 下午6.20.48.png

|

3.4RunLoop 的底層實現

OSX/iOS 的系統架構和Darwin 這個核心的架構如下:


屏幕快照 2016-03-30 下午6.06.58.png

從上面代碼可以看到,RunLoop 的核心是基于 mach port 的,其進入休眠時調用的函數是 mach_msg()。為了解釋這個邏輯,下面稍微介紹一下 OSX/iOS 的系統架構。

可以看到,系統默認注冊了5個Mode:

1. kCFRunLoopDefaultMode: App的默認 Mode,通常主線程是在這個 Mode 下運行的。
2. UITrackingRunLoopMode: 界面跟蹤 Mode,用于 ScrollView 追蹤觸摸滑動,保證界面滑動時不受其他 Mode 影響。
3. UIInitializationRunLoopMode: 在剛啟動 App 時第進入的第一個 Mode,啟動完成后就不再使用。
4: GSEventReceiveRunLoopMode: 接受系統事件的內部 Mode,通常用不到。
5: kCFRunLoopCommonModes: 這是一個占位的 Mode,沒有實際作用。

4. 補充一些雜項

4.1. autorelease究竟是在什么時候釋放

答:
UIKit通過RunLoopObserver在RunLoop兩次Sleep間對AutoreleasePool進行pop和push,將這次Loop中產生的Autorelease對象釋放

屏幕快照 2016-03-30 下午6.00.00.png
runloop + autorelease流程圖

這個圖來自一位大牛-微博@iOS程序犭袁
對這個圖的解釋點擊這里

4.2 一個TableView延遲加載圖片的新思路

[self.avatarImageView performSelector:@s;elector(serImage:)    
                        withObjetc:downloadedImage
                        afterDelay:0
                        inModes:@[NSDefaultRunLoopMode]];

4.3 考一個問題:

有這么一個場景,我們要做一個SDK, 這個函數不能使用回調,直接在接口里面return 結果,但是這個函數又必須先彈出一個登錄框,讓用戶輸入用戶名密碼后,SDK再返回結果,請問如何實現:
polen:
說明下,sdk的接口一般都是會單獨有個線程去做自己的事情,但是彈出登錄框,這個行為必然需要在主線程里面去做(main Thread), 但是題目要求直接return結果,言外之意是,放棄block相關的想法,那么如何實現呢?

知道的同學可以在評論里回復哈.

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,702評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,143評論 3 415
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,553評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,620評論 1 307
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,416評論 6 405
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,940評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,024評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,170評論 0 287
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,709評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,597評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,784評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,291評論 5 357
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,029評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,407評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,663評論 1 280
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,403評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,746評論 2 370

推薦閱讀更多精彩內容

  • Runloop是iOS和OSX開發中非常基礎的一個概念,從概念開始學習。 RunLoop的概念 -般說,一個線程一...
    小貓仔閱讀 1,007評論 0 1
  • 轉載:http://www.cocoachina.com/ios/20150601/11970.html RunL...
    Gatling閱讀 1,453評論 0 13
  • 轉自http://blog.ibireme.com/2015/05/18/runloop 深入理解RunLoop ...
    飄金閱讀 995評論 0 4
  • 深入理解RunLoop 由ibireme| 2015-05-18 |iOS,技術 RunLoop 是 iOS 和 ...
    橙娃閱讀 871評論 1 2
  • 靜靜的感悟人到中年 文 ——柔情似水 2017/09/09 人生是一筆滄桑,紅塵是一場過往,花開花落,斗轉星移,時...
    擷一朵鏡中花閱讀 336評論 5 2