RunLoop學習筆記

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

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

這種模型通常被稱為Event Loop。Event Loop在iOS里的實現就是RunLoop。實現這種模型的關鍵點在于:如何管理事件/消息;如何讓線程在沒有消息處理時處于休眠以避免資源占用、在有消息到來時被喚醒來處理消息。
所以,RunLoop實際上就是一個對象,這個對象管理了其需要處理的事件和消息,并提供了一個入口函數來執行上面的Event Loop的邏輯。
iOS系統中提供了兩個這樣的對象:NSRunLoop和CFRunLoopRef。

  • CFRunLoopRef是在CoreFoundation框架內的,它提供了純C函數的API,所有這些API都是線程安全的;
  • NSRunLoop是基于CFRunLoopRef的封裝,提供了面向對象的API,但這些對象不是線程安全的。
    CFRunLoopRef是開源的,在這里可以下載到整個CoreFoundation。

RunLoop與線程的關系

iOS里有兩個線程對象:NSThread和pthread_t。CFRunloop是基于pthread來管理的。

蘋果不允許直接創建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,并先為主線程創建一個 RunLoop。
        loopsDic = CFDictionaryCreateMutable();
        CFRunLoopRef mainLoop = _CFRunLoopCreate();
        CFDictionarySetValue(loopsDic, pthread_main_thread_np(), mainLoop);
    }
    
    /// 直接從 Dictionary 里獲取。
    CFRunLoopRef loop = CFDictionaryGetValue(loopsDic, thread));
    
    if (!loop) {
        /// 取不到時,創建一個
        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之間是一一對應的,線程作為key、CFRunLoopRef作為value保存到了全局的字典里。線程剛創建時沒有對應的RunLoop,RunLoop的創建是發生在第一次獲取時,RunLoop的銷毀是發生在線程結束時。你只能在一個線程的內部獲取RunLoop(主線程除外)。
我們的應用程序不需要自己創建RunLoop,而是在合適的時間來啟動RunLoop??梢酝ㄟ^ [NSRunLoop currentRunLoop] 或 [NSRunLoop mainRunLoop]來獲取RunLoop。

RunLoop對外的接口

在CoreFoundation里面關于RunLoop有5各類:
CFRunLoopRef
CFRunLoopModeRef
CFRunLoopSourceRef
CFRunLoopTimerRef
CFRunLoopObserverRef

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

RunLoop_0.png

RunLoop的Mode

CFRunLoopMode和CFRunLoop的結構大致如下:

struct __CFRunLoopMode {
    CFStringRef _name;            // Mode Name, 例如 @"kCFRunLoopDefaultMode",@"kCFRunLoopCommonModes"
    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
    ...
};

滑動ScrollView時NSTimer失效的問題

主線程的RunLoop里有兩個預制的mode:
KCFRunLoopDefaultMode和UITrackingRunLoopMode,這兩個Mode都已經被標記為“Common”屬性。DefaultMode是App平時所處的狀態,TrackingRunLoopMode是追蹤scrollView滑動是的狀態。NSTimer初始化后默認是KCFRunLoopDefaultMode的狀態。
要想讓timer在兩個狀態中都能正常使用,一種方法是將timer分別加入兩個mode中,還有一種,就是將timer加入到頂層的RunLoop的“commonModeItems”中?!癱ommonModeItems”被更新到所有具有“Common”屬性的Mode里去。

開啟一個NSTimer實際上就是在當前的RunLoop中注冊了一個新的事件源,當scrollView滑動的過程中,當前的RunLoop會處于UITrackingRunLoopMode的模式下,在這個模式下,是不會處理NSDefaultRunLoopMode的消息。簡單地說就是NSTimer不會開啟新的進程,只是在RunLoop里注冊了一下,RunLoop每次loop時都會檢測這個timer,看是否可以觸發。當RunLoop處于A Mode中,而timer注冊在B Mode時就無法檢測到這個timer,所以需要把timer也注冊到A Mode,這樣就可以檢測到。
解決方法:

[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];

如果定時器所在的runloop沒有運行,或者runloop所處的mode和定時器的不一致,都會導致定時器失效。

NSRunLoopMode

typedef NSString *NSRunLoopMode;
NSRunLoopCommonMode:被添加到RunLoop里的對象使用這個mode會被那些已經描述作為“common”Modes的所有的RunLoop所監測到。
NSDefaultRunLoopMode:這個model處理輸入源除了NSConnection對象。
NSEventTrackingRunLoopMode
NSModalPanelRunLoopMode
UITrackingRunLoopMode

NSThread

NSThread * th = [[NSThread alloc] initWithTarget:self selector:@selector(test) object:nil];
[th start];

start方法會異步地生成一個新的線程并且在線程中調用NSThread對象的main方法。
start方法只能使用一次,自此調用會引起crash


屏幕快照 2017-05-04 下午4.52.36.png

如果想多次使用這個線程怎么處理呢:

- (void)viewDidLoad {
    [super viewDidLoad];
    
    _thread = [[NSThread alloc] initWithTarget:self selector:@selector(startThread) object:nil];
    [_thread start];
    [self useThread];
    [self useThread];
}
- (void)startThread{
    NSLog(@"start");
}
- (void)useThread{
    [self performSelector:@selector(log) onThread:_thread withObject:nil waitUntilDone:NO];
}jiu
- (void)log{
    NSLog(@"log");
}

運行之后發現,log方法并沒有調用,原因是線程在執行完startThread方法后,便退出了。為了讓線程能夠再次使用,可以讓線程對應的runLoop運行起來。我們把上面的startThread方法修改一下:

- (void)startThread{
    NSRunLoop * runloop = [NSRunLoop currentRunLoop];
    [runloop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
    [runloop run];
}

然后運行可以看到打印的log。
即使RunLoop開始運行,如果RunLoop中的modes為空,或者要執行的mode里沒有item,那么RunLoop會直接在當前loop中返回,并進入睡眠狀態。
如果把上面的[runloop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];給去掉,也是沒有log的。
當在另外一個線程執行selector的時候,這個線程必須有一個運行的runloop。

如果在主線程里使用 initWithRequest:delegate:startImmediately:創建了一個NSURLConnetion,當調用star方法時,這個NSURLConnetion會被安排到當前的runloop里,并且是默認的mode。如果此時滑動uiscrollview,由于主線程的runloop的mode被切換到UITrackingRunLoopMode,導致NSURLConnetion的代理方法無法回調。
所以需要用到scheduleInRunLoop: scheduleInRunLoop

 NSURL * url = [NSURL URLWithString:@"https://www.baidu.com"];
 NSURLRequest *request = [[NSURLRequest alloc] initWithURL:url cachePolicy:NSURLRequestReloadIgnoringLocalCacheData timeoutInterval:15];
NSURLConnection *connection = [[NSURLConnection alloc] initWithRequest:request delegate:self startImmediately:NO];
 [connection scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes];
 [connection start];

RunLoop的管理不是全自動的,你仍然需要開啟運行runLoop并且在適當的時候處里到來的事件。應用的每一個線程都有對應的RunLoop對象,只有除了主線程之外的線程需要啟動運行它們的RunLoop,app的框架會自動的配置和運行主線程的RunLoop。

RunLoop從兩個不同類型的源里接收事件。Input sources 異步地傳遞事件,這些事件通常來源于另外一個線程或應用。Timer Source同步地傳遞事件,通常發生在預訂的時間或重復間隔。

Input Source異步地把事件提交到相應的handles并且調用了runUntilDate:(與線程相關聯的RunLoop對象調用這個方法)來退出。Timer Source提供事件到handles但不會引起RunLoop退出。
除了處理Input Source,RunLoop也會生成關于RunLoop運行行為的通知。注冊成為RunLoop的觀察者可以收到那些通知并且使用它們在線程上做一些額外的處理。
RunLoop的Mode是一個集合,它包括了輸入源,timers,observers(會受到通知)。每次你運行了你的RunLoop,你需要為RunLoop指定一個mode。
你可以自定義一個mode,給mode的name設置一個自定義的字符串。你必須要給自定義的mode添加source,timers和observes。

mode Name Description
Default NSDefaultRunLoopMode 大部分情況下,你需要使用這個mode來開啟你的RunLoop并且配置你的輸入源
Common modes NSRunLoopCommonModes 這是一組可配置的常用模式,將輸入源于此模式相關聯還將其與組中的每種模式向關聯。默認情況下,此設置包括了默認模式和事件追蹤模式。

Input Source

Input Source異步地傳遞事件到線程里。Input Source通常有兩類:一種是基于Port的input source,用于檢測應用程序的Mach ports;一種是自定義的input source,用于監測自定義的事件源。這兩個源唯一的區別是如何發出信號的,基于Port的源由內核自動發出信號,自定義源必須從另一個線程手動發出信號。
當你創建一個intput source,你可以把它分配到runloop的多個mode中。如果一個intput source不在runloop當下的監測到的mode中,他所生成的任何事件都會被掛起知道runloop切換到正確的mode中。

什么時候使用runloop

應用的主線程是默認開啟了runloop。當我們使用自定義的線程時,如果想要向和線程進行更多的交互,想要線程處于活躍狀態,就要考慮使用runloop。
如果使用到了以下操作,需要使用runloop:

  • 使用了port或自定義的inputsource來與其他線程通信;
  • 在線程中使用了定時器;
  • 在線程中使用了performSelector:
  • 保持線程執行周期性的任務。

RunLoop對象

在run一個runloop之前,必須要至少添加一個inputsource,否則run后runloop會立即退出。
除了添加inputsource,也可以添加observes來監測runloop的運行狀態。你可以使用CFRunLoopObserveRef來創建對象并使用CFRunLoopAddObserve函數來添加。RunLoop Observe必須使用Core Foundation框架來創建。

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

推薦閱讀更多精彩內容

  • 先貼下 apple doc, 本文基本是對照 doc 的翻譯:https://developer.apple.co...
    brownfeng閱讀 6,877評論 8 111
  • 一、什么是 RunLoop 從字面上來說,RunLoop 可以被翻譯為跑圈,但通常狀況下我們都稱之為運行循環或者消...
    玉米安愛吃甜玉米閱讀 256評論 0 0
  • 參考深入理解RunLoop深入研究 Runloop 與線程?;頡unLoop分享by孫源 RunLoop的概念 R...
    簞食豆羹閱讀 1,094評論 7 15
  • 什么是Runloop 運行循環 跑圈 內部類似一個 do-while 循環, 在循環內部不斷處理各種任務 (Sou...
    aLonelyRoot3閱讀 1,354評論 4 34
  • 落日余暉撒滿天 , 游人匯聚棗博園, 彩虹飛架顯天邊。 老人席地話變遷, 孩童追逐頤少年, 靈州繪就祥和篇
    夕陽在山閱讀 273評論 4 3