RunLoop 本質:安全
RunLoop 本質上是一個運行循環,其做用是保持線程的生命,防止線程被銷燬,平常開發中無處不在,爲了感覺到 RunLoop 的存在,舉個簡單的例子,咱們都知道,APP程序的入口在main.m裏面:bash
#import <UIKit/UIKit.h>
#import "AppDelegate.h"
int main(int argc, char * argv[]) {
@autoreleasepool {
return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
}
}
複製代碼
接下來咱們試着感覺一下 RunLoop,點擊查看 UIApplicationMain,其返回值是一個 int 值:網絡
// If nil is specified for principalClassName, the value for NSPrincipalClass from the Info.plist is used. If there is no
// NSPrincipalClass key specified, the UIApplication class is used. The delegate class will be instantiated using init.
UIKIT_EXTERN int UIApplicationMain(int argc, char * _Nullable argv[_Nonnull], NSString * _Nullable principalClassName, NSString * _Nullable delegateClassName);
複製代碼
咱們在 main 函數裏面用一個 int 變量去接收它,而後在返回值先後添加輸出語句:函數
int main(int argc, char * argv[]) {
@autoreleasepool {
NSLog(@"1 %@", [NSThread currentThread]);
int a = UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
NSLog(@"2 %@", [NSThread currentThread]);
return a;
}
}
複製代碼
運行程序,看一下輸出: oop
咱們看到,只執行了第一個輸出,而且當前線程是主線程,第二個輸出語句並無執行,沒有執行的緣由,有兩種可能,第一種:UIApplicationMain 內部阻塞;第二種:UIApplicationMain 函數內部開啓了一個死循環;可是一旦程序發生阻塞,APP 就沒法使用了,那麼說得通的可能性就是第二種,這個函數內部開啓了死循環,就是 RunLoop 循環,它的做用,就是保持主線程的生命,而且幫咱們監聽一些事件,好比觸摸、時鐘、網絡等;ui
Runloop 的兩種形式:spa
OC: Foundation ——> NSRunLoop線程
// NSRunLoop獲取RunLoop
[NSRunloop currentRunloop]; // 得到當前線程的RunLoop的方法
[NSRunloop mainRunloop]; // 得到主線程的RunLoop的方法
複製代碼
C: CoreFoudation ——> CFRunLoopRef指針
// CFRunLoopRef獲取RunLoop
CFRunloopGetCurrent(); // 得到當前線程的RunLoop的方法
CFRunloopGetMain(); // 得到主線程的RunLoop的方法
複製代碼
NSRunLoop 是對 CFRunLoopRef 的進一步封裝,而且 CFRunLoopRef 是線程安全的,而這一點 NSRunLoop 並不能保證。code
RunLoop 與線程的關係:
每一條線程都有一個與之對應的 RunLoop 對象; RunLoop 保存在全局的字典內,是以線程爲 key,RunLoop 爲 value; RunLoop 的建立是發生在第一次獲取時(懶加載, 不獲取是不會建立的),RunLoop 的銷燬是發生在線程結束時,只能在一個線程的內部獲取其 RunLoop(主線程除外)。
RunLoop 對外的接口:
在 CoreFoundation 裏面關於 RunLoop 有5個類:
typedef struct CF_BRIDGED_MUTABLE_TYPE(id) __CFRunLoop * CFRunLoopRef; // CFRunLoopRef
typedef struct CF_BRIDGED_MUTABLE_TYPE(id) __CFRunLoopSource * CFRunLoopSourceRef; // CFRunLoopSourceRef
typedef struct CF_BRIDGED_MUTABLE_TYPE(id) __CFRunLoopObserver * CFRunLoopObserverRef; // CFRunLoopObserverRef
typedef struct CF_BRIDGED_MUTABLE_TYPE(NSTimer) __CFRunLoopTimer * CFRunLoopTimerRef; // CFRunLoopTimerRef
CFRunLoopModeRef
複製代碼
其中 CFRunLoopModeRef 類並無對外暴露,只是經過 CFRunLoopRef 的接口進行了封裝:
一個 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 的線程,其原理在下面會講到。
CFRunLoopTimerRef 是基於時間的觸發器,它和 NSTimer 是 toll-free bridged 的,能夠混用。其包含一個時間長度和一個回調(函數指針)。當其加入到 RunLoop 時,RunLoop會註冊對應的時間點,當時間點到時,RunLoop 會被喚醒以執行那個回調。
CFRunLoopObserverRef 是觀察者,每一個 Observer 都包含了一個回調(函數指針),當 RunLoop 的狀態發生變化時,觀察者就能經過回調接受到這個變化,RunLoop 的狀態以下:
/* Run Loop Observer Activities */
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
kCFRunLoopEntry = (1UL << 0), // 即將進入RunLoop
kCFRunLoopBeforeTimers = (1UL << 1), // 即將處理Timer
kCFRunLoopBeforeSources = (1UL << 2), // 即將處理 Source
kCFRunLoopBeforeWaiting = (1UL << 5), // 即將進入休眠
kCFRunLoopAfterWaiting = (1UL << 6), // 剛從休眠中喚醒
kCFRunLoopExit = (1UL << 7), // 即將退出RunLoop
kCFRunLoopAllActivities = 0x0FFFFFFFU
};
複製代碼
上面的 Source / Timer / Observer 被統稱爲 mode item,一個 item 能夠被同時加入多個 Mode。但一個 item 被重複加入同一個 Mode 時是不會有效果的。若是一個 Mode 中一個 item 都沒有,則 RunLoop 會直接退出,不進入循環。
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
...
};
複製代碼
常見的2種 Mode:
kCFRunloopDefaultMode //程序的默認Mode
UITrackingRunloopMode //界面追蹤Mode
複製代碼
不一樣 Mode 對應的事件類型都不相同,例如咱們平時使用的 Timer:
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
NSTimer *timer = [NSTimer timerWithTimeInterval:1.0f target:self selector:@selector(timerMethod) userInfo:nil repeats:YES];
// timerWithTimeInterval:target:selector:userInfo:repeats: 建立的 Timer 須要手動添加到RunLoop中才能運行
// 兩個 Mode 同時添加,在兩個模式下都會運行
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode]; // 默認模式下運行,但當觸摸事件發生時,就會中止
[[NSRunLoop currentRunLoop] addTimer:timer forMode:UITrackingRunLoopMode]; // 有觸摸事件時纔會運行,可是時間結束後就會中止
// 添加到 NSRunLoopCommonModes 中,在兩個模式下都會運行
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
}
- (void)timerMethod {
NSLog(@"%@", [NSThread currentThread]);
}
複製代碼
經過這種方式建立的 Timer 自己是不會運行的,只有將 Timer 添加到 RunLoop 中才會運行,咱們先將 Mode 設置爲 NSDefaultRunLoopMode,Timer 會運行,而且打印輸出, 這個時候咱們往界面中添加交互控件,例如添加一個ScrollView,當咱們滑動這個控件時, Timer 就會中止,響應完觸摸事件又會恢復運行,這是由於 RunLoop 會優先響應 UITrackingRunloopMode 的事件,相反,若是咱們將 Mode 設置爲 UITrackingRunloopMode,那正常狀況下 Timer 不會運行,當有 UI 事件的時候,纔會運行;這和咱們的初衷相背離,咱們但願在任何狀況下,Timer 都能運行,這個時候,咱們就要將 Timer 同時添加到 NSDefaultRunLoopMod 和 UITrackingRunloopMode 兩個模式下,NSRunLoopCommonModes 就是爲咱們解決這個問題。
注: kCFRunloopCommonModes 默認包括了 kCFRunloopDefaultMode 和 UITrackingRunloopMode; kCFRunloopCommonModes 並非一個真正的模式,只是一個標記,而 kCFRunloopDefaultMode 和 UITrackingRunloopMode 的模式是真正意義的模式,timer 在設置了 kCFRunloopCommonModes 標記的模式下都能運行,而 kCFRunloopDefaultMode 和 UITrackingRunloopMode 這兩個模式都是 kCFRunloopCommonModes 標記的。
RunLoop 運行邏輯:
// 用DefaultMode啓動
void CFRunLoopRun(void) {
CFRunLoopRunSpecific(CFRunLoopGetCurrent(), kCFRunLoopDefaultMode, 1.0e10, false);
}
/// 用指定的Mode啓動,容許設置RunLoop超時時間
int CFRunLoopRunInMode(CFStringRef modeName, CFTimeInterval seconds, Boolean stopAfterHandle) {
return CFRunLoopRunSpecific(CFRunLoopGetCurrent(), modeName, seconds, returnAfterSourceHandled);
}
/// RunLoop的實現
int CFRunLoopRunSpecific(runloop, modeName, seconds, stopAfterHandle) {
/// 首先根據modeName找到對應mode
CFRunLoopModeRef currentMode = __CFRunLoopFindMode(runloop, modeName, false);
/// 若是mode裏沒有source/timer/observer, 直接返回。
if (__CFRunLoopModeIsEmpty(currentMode)) return;
/// 1. 通知 Observers: RunLoop 即將進入 loop。
__CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopEntry);
/// 內部函數,進入loop
__CFRunLoopRun(runloop, currentMode, seconds, returnAfterSourceHandled) {
Boolean sourceHandledThisLoop = NO;
int retVal = 0;
do {
/// 2. 通知 Observers: RunLoop 即將觸發 Timer 回調。
__CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeTimers);
/// 3. 通知 Observers: RunLoop 即將觸發 Source0 (非port) 回調。
__CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeSources);
/// 執行被加入的block
__CFRunLoopDoBlocks(runloop, currentMode);
/// 4. RunLoop 觸發 Source0 (非port) 回調。
sourceHandledThisLoop = __CFRunLoopDoSources0(runloop, currentMode, stopAfterHandle);
/// 執行被加入的block
__CFRunLoopDoBlocks(runloop, currentMode);
/// 5. 若是有 Source1 (基於port) 處於 ready 狀態,直接處理這個 Source1 而後跳轉去處理消息。
if (__Source0DidDispatchPortLastTime) {
Boolean hasMsg = __CFRunLoopServiceMachPort(dispatchPort, &msg)
if (hasMsg) goto handle_msg;
}
/// 通知 Observers: RunLoop 的線程即將進入休眠(sleep)。
if (!sourceHandledThisLoop) {
__CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeWaiting);
}
/// 7. 調用 mach_msg 等待接受 mach_port 的消息。線程將進入休眠, 直到被下面某一個事件喚醒。
/// • 一個基於 port 的Source 的事件。
/// • 一個 Timer 到時間了
/// • RunLoop 自身的超時時間到了
/// • 被其餘什麼調用者手動喚醒
__CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort) {
mach_msg(msg, MACH_RCV_MSG, port); // thread wait for receive msg
}
/// 8. 通知 Observers: RunLoop 的線程剛剛被喚醒了。
__CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopAfterWaiting);
/// 收到消息,處理消息。
handle_msg:
/// 9.1 若是一個 Timer 到時間了,觸發這個Timer的回調。
if (msg_is_timer) {
__CFRunLoopDoTimers(runloop, currentMode, mach_absolute_time())
}
/// 9.2 若是有dispatch到main_queue的block,執行block。
else if (msg_is_dispatch) {
__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(msg);
}
/// 9.3 若是一個 Source1 (基於port) 發出事件了,處理這個事件
else {
CFRunLoopSourceRef source1 = __CFRunLoopModeFindSourceForMachPort(runloop, currentMode, livePort);
sourceHandledThisLoop = __CFRunLoopDoSource1(runloop, currentMode, source1, msg);
if (sourceHandledThisLoop) {
mach_msg(reply, MACH_SEND_MSG, reply);
}
}
/// 執行加入到Loop的block
__CFRunLoopDoBlocks(runloop, currentMode);
if (sourceHandledThisLoop && stopAfterHandle) {
/// 進入loop時參數說處理完事件就返回。
retVal = kCFRunLoopRunHandledSource;
} else if (timeout) {
/// 超出傳入參數標記的超時時間了
retVal = kCFRunLoopRunTimedOut;
} else if (__CFRunLoopIsStopped(runloop)) {
/// 被外部調用者強制中止了
retVal = kCFRunLoopRunStopped;
} else if (__CFRunLoopModeIsEmpty(runloop, currentMode)) {
/// source/timer/observer一個都沒有了
retVal = kCFRunLoopRunFinished;
}
/// 若是沒超時,mode裏沒空,loop也沒被中止,那繼續loop。
} while (retVal == 0);
}
/// 10. 通知 Observers: RunLoop 即將退出。
__CFRunLoopDoObservers(rl, currentMode, kCFRunLoopExit);
}
複製代碼
能夠看到,實際上 RunLoop 就是這樣一個函數,其內部是一個 do-while 循環。當你調用 CFRunLoopRun() 時,線程就會一直停留在這個循環裏,直到超時或被手動中止,該函數纔會返回。