RunLoop深度探究(一)

原文鏈接:http://yangchao0033.github.io/blog/2016/01/06/runloopshen-du-tan-jiu/

RunLoop的概念

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

function loop() {
    initialize();
    do {
        var message = get_next_message();
        process_message(message);
    } while (message != quit);
}

這種模型通常被稱作 Event Loop。 Event Loop 在很多系統(tǒng)和框架里都有實現浙巫,比如 Node.js 的事件處理金蜀,比如 Windows 程序的消息循環(huán)刷后,再比如 OSX/iOS 里的 RunLoop。實現這種模型的關鍵點在于:如何管理事件/消息渊抄,如何讓線程在沒有處理消息時休眠以避免資源占用尝胆、在有消息到來時立刻被喚醒。

所以护桦,RunLoop 實際上就是一個對象含衔,這個對象管理了其需要處理的事件和消息,并提供了一個入口函數來執(zhí)行上面 Event Loop 的邏輯二庵。線程執(zhí)行了這個函數后贪染,就會一直處于這個函數內部 "接受消息->等待->處理" 的循環(huán)中,直到這個循環(huán)結束(比如傳入 quit 的消息)催享,函數返回杭隙。

OSX/iOS 系統(tǒng)中,提供了兩個這樣的對象:NSRunLoop 和 CFRunLoopRef因妙。
CFRunLoopRef 是在 CoreFoundation 框架內的痰憎,它提供了純 C 函數的 API,所有這些 API 都是線程安全的攀涵。
NSRunLoop 是基于 CFRunLoopRef 的封裝信殊,提供了面向對象的 API,但是這些 API 不是線程安全的汁果。

CFRunLoopRef 的代碼是開源的,你可以在這里 http://opensource.apple.com/tarballs/CF/CF-855.17.tar.gz 下載到整個 CoreFoundation 的源碼玲躯。為了方便跟蹤和查看据德,你可以新建一個 Xcode 工程,把這堆源碼拖進去看跷车。

RunLoop與線程的關系

首先棘利,iOS 開發(fā)中能遇到兩個線程對象: pthread_t 和 NSThread。過去蘋果有份文檔標明了 NSThread 只是 pthread_t 的封裝朽缴,但那份文檔已經失效了善玫,現在它們也有可能都是直接包裝自最底層的 mach thread。蘋果并沒有提供這兩個對象相互轉換的接口密强,但不管怎么樣茅郎,可以肯定的是 pthread_t 和 NSThread 是一一對應的。比如或渤,你可以通過 pthread_main_thread_np() 或 [NSThread mainThread] 來獲取主線程系冗;也可以通過 pthread_self() 或 [NSThread currentThread] 來獲取當前線程。CFRunLoop 是基于 pthread 來管理的薪鹦。

蘋果不允許直接創(chuàng)建 RunLoop掌敬,它只提供了兩個自動獲取的函數:CFRunLoopGetMain() 和 CFRunLoopGetCurrent()惯豆。 這兩個函數內部的邏輯大概是下面這樣:

/// 全局的Dictionary,key 是 pthread_t奔害, value 是 CFRunLoopRef
static CFMutableDictionaryRef loopsDic;
/// 訪問 loopsDic 時的鎖
static CFSpinLock_t loopsLock;
 
/// 獲取一個 pthread 對應的 RunLoop楷兽。
CFRunLoopRef _CFRunLoopGet(pthread_t thread) {
    OSSpinLockLock(&loopsLock);
    
    if (!loopsDic) {
        // 第一次進入時,初始化全局Dic华临,并先為主線程創(chuàng)建一個 RunLoop芯杀。
        loopsDic = CFDictionaryCreateMutable();
        CFRunLoopRef mainLoop = _CFRunLoopCreate();
        CFDictionarySetValue(loopsDic, pthread_main_thread_np(), mainLoop);
    }
    
    /// 直接從 Dictionary 里獲取。
    CFRunLoopRef loop = CFDictionaryGetValue(loopsDic, thread));
    
    if (!loop) {
        /// 取不到時银舱,創(chuàng)建一個
        loop = _CFRunLoopCreate();
        CFDictionarySetValue(loopsDic, thread, loop);
        /// 注冊一個回調瘪匿,當線程銷毀時,順便也銷毀其對應的 RunLoop寻馏。
        _CFSetTSD(..., thread, loop, __CFFinalizeRunLoop);
    }
    
    OSSpinLockUnLock(&loopsLock);
    return loop;
}
 
CFRunLoopRef CFRunLoopGetMain() {
    return _CFRunLoopGet(pthread_main_thread_np());
}
 
CFRunLoopRef CFRunLoopGetCurrent() {
    return _CFRunLoopGet(pthread_self());
}

綜上:線程和RunLoop之間是一一對應的棋弥,并且關系是保存在一個全局的字典中的,并且線程剛創(chuàng)建時是沒有RunLoop的诚欠,如果你不獲取它顽染,他一直都不會有,RunLoop的創(chuàng)建發(fā)生在第一次獲取的時候轰绵,RunLoop的銷毀發(fā)生在線程結束時粉寞。并且只能在線程內部獲取runloop(主線程除外)。

RunLoop 對外的接口

在 CoreFoundation 里面關于 RunLoop 有5個類:

CFRunLoopRef
CFRunLoopModeRef
CFRunLoopSourceRef
CFRunLoopTimerRef
CFRunLoopObserverRef

其中 CFRunLoopModeRef 類并沒有對外暴露左腔,只是通過 CFRunLoopRef 的接口進行了封裝唧垦。他們的關系如下:


image
image

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

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

image
image

CFRunLoopTimerRef是基于時間的觸發(fā)器审洞,他和NSTimer是toll-free bridged的莱睁,可以混用。其包含一個時間長度和一個回調(函數指針)芒澜。

image
image

CFRunLoopObserverRef是觀察者仰剿,每個Observer都包含了一個回調(函數指針),當RunLoop的狀態(tài)發(fā)生變法時痴晦,觀察者就能通過回調接受這個變化南吮。可以觀測的時間點有以下幾個:

image
image

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

RunLoop的Mode

CFRunLoopMode和CFRunLoop的結構大致如下:

struct __CFRunLoopMode {
    CFStringRef _name;            // Mode Name, 例如 @"kCFRunLoopDefaultMode"
    CFMutableSetRef _sources0;    // Set
    CFMutableSetRef _sources1;    // Set
    CFMutableArrayRef _observers; // Array
    CFMutableArrayRef _timers;    // Array
    ...
};
struct __CFRunLoop {
    CFMutableSetRef _commonModes;     // Set
    CFMutableSetRef _commonModeItems; // Set<Source/Observer/Timer>
    CFRunLoopModeRef _currentMode;    // Current Runloop Mode
    CFMutableSetRef _modes;           // Set
    ...
};

這里有個概念叫:“CommonModes”一個Mode可以將自己標記為“Common”屬性(通過將其ModeName添加到RunLoop得“commonModes”中)誊酌。每當RnuLoop的內容發(fā)生變化時部凑,RunLoop的都會自動將_commonModeItems里的Source/Timer/Observer同步到具有“Common”標記的所有的Mode。
應用舉例:主線程 RunLoop 默認會預制兩個 Mode :kCFRunLoopDefaultMode 和 UITrackingRunLoopMode碧浊。這兩個Mode都已經被標記為“Common”屬性涂邀。DefaultMode是App平時所處的狀態(tài),TrackingRunLoop是為了追蹤ScrollView滑動時的狀態(tài)箱锐。當你創(chuàng)建一個Timer并加到DefaultMode時比勉,Timer會得到重復回調。此時滑動TableView時驹止,RunLoop會將mode切換為TrackingRunLoopMode敷搪,這時Timer就不會被回調,也不會影響滑動的操作了幢哨。具體用例類似于在tableView中加入滾動廣告欄,當你在操作tableView時會回調自動滾動欄的Timer嫂便,造成滾動欄的滑動出現卡頓捞镰。

有時你需要一個Timer,在兩個Mode中都能得到回調毙替,一種辦法就是講這個Timer加入兩個Mode岸售。還有一種就是講Timer加入到頂層的RunLoop的“commonModeItems”中〕Щ“commonModeItems”被RunLoop自動更新到所有具有“Common”屬性的Mode里去凸丸。

CFRunLoop對外暴漏的管理Mode的接口只有下面兩個:

CFRunLoopAddCommonMode(CFRunLoopRef runloop, CFStringRef modeName); // 給RunLoop添加到CommonMode中
CFRunLoopRunInMode(CFStringRef modeName, ...); // 返回當前線程中指定mode的CFRunLoop對象

Mode暴露的管理mode item的接口有下面幾個

CFRunLoopAddSource(CFRunLoopRef rl, CFRunLoopSourceRef source, CFStringRef modeName);// 添加一個CFRunLoopSource對象到一個run loop mode中(如果添加的Source是source0的話,這個方法將會調用 schedule 回調在source的上下文結構(context structure)的指定方法)袱院。一個runloop source 可以同時被注冊到多個 runloop 和 runloop modes 中屎慢。當source被發(fā)出信號瞭稼,無論哪一個被注冊的 runloop 都會開始檢測第一個發(fā)出信號的 source 。 如過rl的mode中已經包含source時腻惠,這個方法將不會做任何事环肘。
CFRunLoopAddObserver(CFRunLoopRef rl, CFRunLoopObserverRef observer, CFStringRef modeName); // 添加CFRunLoopObserver對象到一個run loop mode中去。 討論:一個 runloop 觀察者只能被同時注冊在一個 runloop 中集灌,盡管它可以被通過他的tunloop添加到多個runloop modes中悔雹。 如果rl已經在 mode中 包含 obsever 中,這個方法將不會做任何事欣喧。
CFRunLoopAddTimer(CFRunLoopRef rl, CFRunLoopTimerRef timer, CFStringRef mode);
CFRunLoopRemoveSource(CFRunLoopRef rl, CFRunLoopSourceRef source, CFStringRef modeName); // 添加CFRunLoopTimer 對象到一個runloop mode中 討論:一個runloop timer 在同一時刻只能注冊在一個run loop腌零,盡管它可以被通過他的tunloop添加到多個runloop modes中。 如果rl已經在 mode中 包含 obsever 中唆阿,這個方法將不會做任何事
CFRunLoopRemoveObserver(CFRunLoopRef rl, CFRunLoopObserverRef observer, CFStringRef modeName); // 從run loop mode 移除 Observer 對象益涧,如果 rl 沒有包含參數中的Observer,則該函數不做任何處理
CFRunLoopRemoveTimer(CFRunLoopRef rl, CFRunLoopTimerRef timer, CFStringRef mode); // 從run loop mode 移除 timer 對象酷鸦,如果 rl 沒有包含參數中的timer饰躲,則該函數不做任何處理

以上接口可以看出,只能通過mode name操作內部mode臼隔,當你傳入一個新的mode name但runloop內部沒有對應的mode時嘹裂,runloop會自動幫你創(chuàng)建對應的CFRunloopModeRef。并且官方文檔明確指出摔握,對于runloop來說寄狼,其內部的mode只能增加不能刪除。

蘋果官方公開的內部mode有兩個:CFRunLoopDefaultMode(NSDefaultRunLoopMode)和UITrackingRunLoopMode氨淌,你可以用這兩個 Mode Name來操作對應的 Mode泊愧。
同時蘋果還提出了一個操作Common標記的字符串:kCFRunLoopCommonModes(NSRunLoopCommonModes),你可以用這個字符串來操作Common Items盛正,或標記一個Mode為“Common”删咱。使用時注意區(qū)分該字符串與其他mode name。

特別致謝:

http://blog.ibireme.com/
2015/05/18/runloop/#more-41710

參考文章:

深入理解RunLoop:

http://blog.ibireme.com/2015/05/18/runloop/#more-41710

Apple Document:

https://developer.apple.com/library/ios/documentation/Cocoa/Conceptual/Multithreading/RunLoopManagement/RunLoopManagement.html

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末豪筝,一起剝皮案震驚了整個濱河市痰滋,隨后出現的幾起案子,更是在濱河造成了極大的恐慌续崖,老刑警劉巖敲街,帶你破解...
    沈念sama閱讀 212,383評論 6 493
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現場離奇詭異严望,居然都是意外死亡多艇,警方通過查閱死者的電腦和手機,發(fā)現死者居然都...
    沈念sama閱讀 90,522評論 3 385
  • 文/潘曉璐 我一進店門像吻,熙熙樓的掌柜王于貴愁眉苦臉地迎上來峻黍,“玉大人复隆,你說我怎么就攤上這事〖榕” “怎么了昏名?”我有些...
    開封第一講書人閱讀 157,852評論 0 348
  • 文/不壞的土叔 我叫張陵,是天一觀的道長阵面。 經常有香客問我轻局,道長,這世上最難降的妖魔是什么样刷? 我笑而不...
    開封第一講書人閱讀 56,621評論 1 284
  • 正文 為了忘掉前任仑扑,我火速辦了婚禮,結果婚禮上置鼻,老公的妹妹穿的比我還像新娘镇饮。我一直安慰自己,他們只是感情好箕母,可當我...
    茶點故事閱讀 65,741評論 6 386
  • 文/花漫 我一把揭開白布储藐。 她就那樣靜靜地躺著,像睡著了一般嘶是。 火紅的嫁衣襯著肌膚如雪钙勃。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,929評論 1 290
  • 那天聂喇,我揣著相機與錄音辖源,去河邊找鬼。 笑死希太,一個胖子當著我的面吹牛克饶,可吹牛的內容都是我干的。 我是一名探鬼主播誊辉,決...
    沈念sama閱讀 39,076評論 3 410
  • 文/蒼蘭香墨 我猛地睜開眼矾湃,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了堕澄?” 一聲冷哼從身側響起邀跃,我...
    開封第一講書人閱讀 37,803評論 0 268
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎奈偏,沒想到半個月后,有當地人在樹林里發(fā)現了一具尸體躯护,經...
    沈念sama閱讀 44,265評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡惊来,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 36,582評論 2 327
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現自己被綠了棺滞。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片裁蚁。...
    茶點故事閱讀 38,716評論 1 341
  • 序言:一個原本活蹦亂跳的男人離奇死亡矢渊,死狀恐怖,靈堂內的尸體忽然破棺而出枉证,到底是詐尸還是另有隱情矮男,我是刑警寧澤,帶...
    沈念sama閱讀 34,395評論 4 333
  • 正文 年R本政府宣布室谚,位于F島的核電站毡鉴,受9級特大地震影響,放射性物質發(fā)生泄漏秒赤。R本人自食惡果不足惜猪瞬,卻給世界環(huán)境...
    茶點故事閱讀 40,039評論 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望入篮。 院中可真熱鬧陈瘦,春花似錦、人聲如沸潮售。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,798評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽酥诽。三九已至鞍泉,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間盆均,已是汗流浹背塞弊。 一陣腳步聲響...
    開封第一講書人閱讀 32,027評論 1 266
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留泪姨,地道東北人游沿。 一個月前我還...
    沈念sama閱讀 46,488評論 2 361
  • 正文 我出身青樓,卻偏偏與公主長得像肮砾,于是被迫代替她去往敵國和親诀黍。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 43,612評論 2 350

推薦閱讀更多精彩內容

  • 轉載:http://www.cocoachina.com/ios/20150601/11970.html RunL...
    Gatling閱讀 1,436評論 0 13
  • RunLoop的概念 一般來講仗处,一個線程一次只能執(zhí)行一個任務眯勾,執(zhí)行完成后線程就會退出。如果我們需要一個機制婆誓,讓線程...
    IOS學渣閱讀 455評論 1 4
  • 原文地址:http://blog.ibireme.com/2015/05/18/runloop/ RunLoop ...
    大餅炒雞蛋閱讀 1,152評論 0 6
  • 深入理解RunLoop 由ibireme| 2015-05-18 |iOS,技術 RunLoop 是 iOS 和 ...
    橙娃閱讀 849評論 1 2
  • 2016-09-01 杜建華 中醫(yī)書友會 中醫(yī)書友會第572期(853期回顧) 每天一期吃环,陪伴中醫(yī)人成長 作者/杜...
    花葉不見閱讀 790評論 1 1