RunLoop,顧名思義就是執行迴圈的意思,是指程式在執行過程中迴圈做一些事情。
RunLoop 簡介
當我們建立一個terminal專案的時候,此時的main函式中並沒有一個RunLoop。所以程式執行完main函式之後就退出了。
而一個iOS的application程式,預設在主執行緒開啟了一個RunLoop,這樣一個App就可以處理一些計時器事件,滑動事件等,不會馬上退出。
在iOS專案中每一條執行緒都對應著一個RunLoop物件,RunLoop存放在一個以執行緒作為key的雜湊表中。
主執行緒在建立的時候預設開啟RunLoop,而其他子執行緒預設不開啟,但是會在第一次獲取RunLoop([NSRunLoop currentRunLoop]或者CFRunLoopGetCurrent()
)的時候建立。
一般情況下RunLoop的生命週期跟隨執行緒,執行緒結束的時候RunLoop也會被銷燬。
iOS中提供了一套Foundation框架的NSRunLoop api和一套基於Core Foundation的CFRunLoopRef api來使用RunLoop。其中NSRunLoop是基於CFRunLoopRef做了一層OC的封裝。
RunLoop 結構
在CFRunLoop的原始碼中RunLoop的基本結構如下:
CFRunLoopRef是一個__CFRunLoop
的結構體,結構體中存放了許多mode相關的成員。
其中_currentMode
是CFRunLoopModeRef
型別的,它是一個__CFRunLoopMode
型別的結構體指標。RunLoop通過它來表徵RunLoop的執行狀態。
一個RunLoop中包含有許多Mode。_commonModes
是一個可變的集合,集合中存放了許多mode。RunLoop在執行的時候只能選擇一個Mode作為當前RunLoop執行的狀態,也就是_currentMode
。
mode是CFRunLoopMode
型別的。而CFRunLoopMode
是通過typedf__CFRunLoopMode
得到的。__CFRunLoopMode
中存放了處理觸控事件的source0
、系統時間捕捉的source1
、處理計時器的timers
、監聽RunLoop狀態的observer
等。
另外,如果RunLoop需要切換執行狀態的時候必須先退出當前的Mode,才能進入新的Mode。如果當前Mode中所有的source
、timer
、observer
的時候RunLoop就會立刻退出。
常見的RunLoopMode有預設的modekCFRunLoopDefaultMode
、跟蹤介面(比如:保證滑動不受其他mode影響)的UITrackingRunLoopMode
。另外在api中還有一個kCFRunLoopCommonModes
但是這並不是一個真正的mode,它不存在於_commonModes
中,它只是一個標記。
RunLoop 的監聽器會監聽RunLoop的一些狀態:
/* Run Loop Observer Activities */
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
kCFRunLoopEntry = (1UL << 0), // 即將進入runloop
kCFRunLoopBeforeTimers = (1UL << 1), // 即將處理計時器
kCFRunLoopBeforeSources = (1UL << 2), // 即將處理source
kCFRunLoopBeforeWaiting = (1UL << 5), // 即將進入休眠
kCFRunLoopAfterWaiting = (1UL << 6), // 即將從休眠中喚醒
kCFRunLoopExit = (1UL << 7), // 即將退出RunLoop
kCFRunLoopAllActivities = 0x0FFFFFFFU // 全部狀態
};
複製程式碼
探究RunLoop的執行流程
我們可以通過Xcode自帶的lldb 通過bt
命令檢視函式呼叫棧找到RunLoop的入口函式
在CFRunLoop.c
檔案中找到該函式,我們發現它通過__CFRunLoopDoObservers(rl, currentMode, kCFRunLoopEntry)
監聽進入RunLoop。接著有一個__CFRunLoopRun
的函式呼叫,該函式中封裝了RunLoop處理事件的邏輯。
我們只關注__CFRunLoopRun
主要程式碼:我們發現該函式中存在著一個do-while()
迴圈,當retVal==0
的時候迴圈持續進行,當retVal != 0
的時候,回返回給函式CFRunLoopRunSpecific
,它呼叫__CFRunLoopDoObservers(rl, currentMode, kCFRunLoopExit);
退出RunLoop。
在__CFRunLoopRun
中主要的流程都在下圖中進行了描述。總結來說:
- 首先會通知監聽者Observers:即將處理Timers
- 通知監聽者Observers:即將處理Sources
- 處理blocks
- 處理source0,如果處理完了會再次處理blocks
- 如果存在source1,則跳到handle_msg處理,如果沒有則通知監聽器即將進入休眠
- 休眠時期等待訊息來喚醒當前執行緒
- 如果有訊息喚醒則進入handle_msg處理計時器,gcd,source1這些資訊
- 再次處理blocks
- 獲取返回值retVal
- 進入
do-while()
,如果retVal == 0
則迴圈持續進行。否則返回給CFRunLoopRunSpecific
函式,退出RunLoop
RunLoop的應用
Timer
當我們使用+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block;
建立一個計時器,並且頁面存在scrollView的時候。滑動scrollView計時器就會停止執行。這是因為一開始runloop存在於NSDefaultRunLoopMode
,當滑動事件響應的時候runloop會進入UITrackingRunLoopMode
模式處理滑動事件,所有timer就會失去處理。
我們可以使用+ (NSTimer *)timerWithTimeInterval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block;
建立timer,然後將它放在一個NSRunLoopCommonModes
標記的模式下進行工作,timer是可以t在_commonModes陣列中存放的模式下工作的。這樣就解決了滑動事件和timer計時器事件衝突的問題。
執行緒保活
當我們建立一條執行緒的時候,這條執行緒並沒有一個RunLoop。當我們第一次獲取RunLoop的時候這條執行緒中才會建立RunLoop。
所以建立一條一直存在的執行緒,我們需要線上程中加入一個不會被回收的RunLoop,也就是讓do-while()
一直存在,也就是RunLoop一直有事情在處理,而retVal
不會為不是0的其他值。
例項程式碼:
#import "SoCPermanentThread.h"
@interface SoCPermanentThread ()
@property (nonatomic, strong) NSThread *thread;
@end
@implementation SoCPermanentThread
- (instancetype)init {
if (self = [super init]) {
self.thread = [[NSThread alloc] initWithBlock:^{
CFRunLoopSourceContext context = {0};
CFRunLoopSourceRef source = CFRunLoopSourceCreate(kCFAllocatorDefault, 0, &context);
CFRunLoopAddSource(CFRunLoopGetCurrent(), source, kCFRunLoopDefaultMode);
CFRelease(source);
CFRunLoopRunInMode(kCFRunLoopDefaultMode, 1.0e10, false);
}];
[self.thread start];
}
return self;
}
- (void)executeTask:(SoCPermenantThreadTask)task {
if (!_thread || !task) return;
[self performSelector:@selector(__task:) onThread:_thread withObject:task waitUntilDone:NO];
}
- (void)stop {
if (!_thread) return;
[self performSelector:@selector(__stop) onThread:_thread withObject:nil waitUntilDone:YES];
}
- (void)__task:(SoCPermenantThreadTask)task {
task();
}
- (void)__stop {
CFRunLoopStop(CFRunLoopGetCurrent());
_thread = nil;
}
- (void)dealloc {
[self stop];
}
@end
複製程式碼
上述程式碼使用Core Foundation
實現的執行緒保活,其中重要的就是首先往RunLoop中新增Source保證RunLoop有事情可以做,另外就是CFRunLoopRunInMode(kCFRunLoopDefaultMode, 1.0e10, false);
這個方法的最後一個引數BOOL引數returnAfterSourceHandled
,其值為flase
代表執行完函式(處理完source)不會返回,而true
則相反,表示執行完函式(處理完source)會立即返回。
總結
本篇主要以Core Foundation
api 為基礎(Core Foundation
開源)簡述了RunLoop的基本概念,和呼叫流程,由於NSRunLoop
是基於CFRunLoop
做的OC封裝,其原理和流程都是一樣的。另外介紹了兩個使用RunLoop的案例。