1.閑扯
一般除了初學者,大部分人了解runloop可能更多的是在面試或者準備面試的時候。 顯然這種技術在平時的開發中,使用的場景是非常低的,但是對這個知識的了解程度可以作為衡量一個iOSer的‘閑散’程度(一般太忙,需求太多的人基本沒時間研究這個東西??)
其實我也不太懂,只是把自己各方偷師學來的整理一下,整理中也希望自己能有個更深入的理解。嗯開始吧:
2.優秀文檔/資料
列舉下已有的比較好的資料
1> 深入理解RunLoop (by ibireme)
polen:
從cocoachina看到的,基本算是非常非常全的了,很詳細的介紹了你所能了解的全部runloop(為什么我這么確定,因為其實關于runloop的文檔并不多,這篇是我看過里面個人認為最全面也最細節的一篇,五星推薦!!!)
2>看過一個視頻:
某度的@sunnyxx的分享
3>以及有個可愛的童鞋對這個視頻做了簡單的整理:
iOS runloop (作者:小白和小黑)
polen:
如果有心的朋友看一下這個視頻 ,大概96min,孫源這哥們講的非常透徹,之前是百度的,在我寫文章這幾天好像剛從百度離職,下面內容有些截圖就是他視頻里面的,希望他不會怪我偷他的圖片??
3.我來整理
當然也有比較懶的,那就看我的總結吧
備注:以下大部分信息非本人原創,只是作為只是整理使用,
原文鏈接上面已經提及,大家可以直接看
3.1runloop的定義
polen:
首先說明下背景:
runloop不是線程,不是GCD,在一個APP里面不是唯一的
runloop就是一個對象,如果把線程比作一條高速公路,我的理解runoop就是這條道路的管理員,沒事了就睡覺,有事了把他叫醒(注意,這里叫醒的實現,一般是其他線程(大部分是main線程)的把他叫醒,可以留一下這里,后面會偽代碼說到這個問題)。
形象理解的話,就是下圖里面,如果線程是個箭頭線,runloop就是那個圈,一圈又一圈...
有人會覺得runloop好虛,如何直觀的看到runloop,這個很簡單,你打開Xcode,run一段cheng程序,然后打個斷點或者暫停一下,看一下堆棧信息,馬上就可以看到,我們的進程從main函數開始,緊接著馬上回喚醒runloop,然后是再調其其他的函數, 應該說除了main函數和幾個基本的函數,大部分都是runloop調用起來的,截圖如下:
所以和runloop有關的都有哪些東西?
當然,專業的說,本質是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
3 詳細說說潤runloop的內部
一個 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 里去。
3.3RunLoop 的內部邏輯
可以看到,實際上 RunLoop 就是這樣一個函數,其內部是一個 do-w
hile 循環。當你調用 CFRunLoopRun() 時,線程就會一直停留在這個循環里;直到超時或被手動停止,該函數才會返回。
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繼續開始干活
|
3.4RunLoop 的底層實現
OSX/iOS 的系統架構和Darwin 這個核心的架構如下:
從上面代碼可以看到,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對象釋放
這個圖來自一位大牛-微博@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相關的想法,那么如何實現呢?
知道的同學可以在評論里回復哈.