NSRunLoop
是iOS
中的消息處理機制,執行完某個事件後線程不會退出,而是進入休眠狀態,當再次監測到須要出發事件時,線程激活,繼續處理事件,處理完成後再次進入休眠while
循環RunLoop
, 由於cocoa
框架爲咱們建立了一個默認的RunLoop
RunLoop
的主要做用
App
中的各類事件(手勢、定時器、Selector
等)CPU
資源、提升程序性能:該作任務的時候作任務,沒事幹的時候休息RunLoop
和線程的關係
RunLoop
對象RunLoop
保存在一個全局的Dictionary
裏, 線程做爲key
, RunLoop
做爲value
RunLoop
對象, RunLoop
會在第一次獲取線程時建立RunLoop
會在線程結束時銷燬RunLoop
已經自動獲取(建立), 子線程默認沒有開啓RunLoop
iOS
開發中RunLoop
有兩套API
框架, 分別是
Foundation
的NSRunLoop
Core Foundation
的CFRunLoopRef
CFRunLoopRef
是基於C
語言的開源框架, 有興趣的能夠到源碼地址下載源碼, 不過沒有C
語言功底的只怕很難看懂NSRunLoop
是對CFRunLoopRef
的有一層封裝, 是OC
語法的框架RunLoop
對象// 獲取當前線程的RunLoop [NSRunLoop currentRunLoop]; // 獲取主線程的RunLoop [NSRunLoop mainRunLoop]; // 獲取主線程的RunLoop CFRunLoopGetMain(); // 獲取當前線程的RunLoop CFRunLoopGetCurrent(); 複製代碼
NSRunLoop
是不開源的, 可是CFRunLoopRef
倒是開源的, 從源碼地址下載CFRunLoopRef
的源碼Core Foundation
中CFRunLoopRef
有如下5個相關的類
CFRunLoopRef
CFRunLoopModeRef
CFRunLoopSourceRef
CFRunLoopTimerRef
CFRunLoopObserverRef
CFRunLoopRef
CFRunLoopRef
對象的主要核心代碼以下html
typedef struct __CFRunLoop * CFRunLoopRef; struct __CFRunLoop { // 線程對象 pthread_t _pthread; // 無序集合 CFMutableSetRef _commonModes; CFMutableSetRef _commonModeItems; // 當前mode CFRunLoopModeRef _currentMode; CFMutableSetRef _modes; }; 複製代碼
主要屬性介紹前端
CFMutableSetRef
是一個無序的集合, 在上面的代碼中存儲的都是CFRunLoopModeRef
對象_modes
存儲的是全部的mode
對象_currentMode
是指當前的mode
CFRunLoopModeRef
typedef struct __CFRunLoopMode *CFRunLoopModeRef; struct __CFRunLoopMode { // mode名稱 CFStringRef _name; Boolean _stopped; CFMutableSetRef _sources0; CFMutableSetRef _sources1; CFMutableArrayRef _observers; CFMutableArrayRef _timers; }; 複製代碼
_name
: 該__CFRunLoopMode
的名稱_sources0
和_sources1
: 一個無序集合, 存儲的都是CFRunLoopSourceRef
對象_observers
: 一個有序集合數組,存儲的都是CFRunLoopObserverRef
對象_timers
: 一個有序集合數組,存儲的都是CFRunLoopTimerRef
對象CFRunLoopModeRef
表明RunLoop
的運行模式RunLoop
只能對應一個線程, 卻包含若干個Mode
,每一個Mode
又包含若干個Source0/Source1/Timer/Observer
RunLoop
啓動時只能選擇其中一個Mode
,做爲currentMode
一樣只能有一個Mode
,只能退出當前Loop
,再從新選擇一個Mode
進入, 不一樣組的Source0/Source1/Timer/Observer
能分隔開來,互不影響Mode
裏沒有任何Source0/Source1/Timer/Observer
,RunLoop
會立馬退出mode
FOUNDATION_EXPORT NSRunLoopMode const NSDefaultRunLoopMode; FOUNDATION_EXPORT NSRunLoopMode const NSRunLoopCommonModes; UIKIT_EXTERN NSRunLoopMode const UITrackingRunLoopMode; CF_EXPORT const CFRunLoopMode kCFRunLoopDefaultMode; CF_EXPORT const CFRunLoopMode kCFRunLoopCommonModes; 複製代碼
kCFRunLoopDefaultMode
(NSDefaultRunLoopMode
):App
的默認Mode
,一般主線程是在這個Mode
下運行UITrackingRunLoopMode
:界面跟蹤Mode
,用於ScrollView
追蹤觸摸滑動,保證界面滑動時不受其餘Mode
影響kCFRunLoopCommonModes
(NSRunLoopCommonModes
): 並非某一種特定的mode
, 而是通用模式, 包括kCFRunLoopDefaultMode
和UITrackingRunLoopMode
CFRunLoopObserverRef
是觀察者,可以監聽RunLoop
全部的狀態改變。 能夠監聽的時間點有以下幾種:數組
/* Run Loop Observer Activities */ typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) { // 即將進入Loop kCFRunLoopEntry = (1UL << 0), // 即將處理Timer kCFRunLoopBeforeTimers = (1UL << 1), // 即將處理Source kCFRunLoopBeforeSources = (1UL << 2), // 即將進入休眠 kCFRunLoopBeforeWaiting = (1UL << 5), // 剛從休眠中喚醒 kCFRunLoopAfterWaiting = (1UL << 6), // 即將退出Loop kCFRunLoopExit = (1UL << 7), // 全部狀態 kCFRunLoopAllActivities = 0x0FFFFFFFU }; 複製代碼
在主線程監聽全部的狀態bash
// 建立observer CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(kCFAllocatorDefault, kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) { switch (activity) { case kCFRunLoopEntry: NSLog(@"kCFRunLoopEntry"); break; case kCFRunLoopBeforeTimers: NSLog(@"kCFRunLoopBeforeTimers"); break; case kCFRunLoopBeforeSources: NSLog(@"kCFRunLoopBeforeSources"); break; case kCFRunLoopBeforeWaiting: NSLog(@"kCFRunLoopBeforeWaiting"); break; case kCFRunLoopAfterWaiting: NSLog(@"kCFRunLoopAfterWaiting"); break; case kCFRunLoopExit: NSLog(@"kCFRunLoopExit"); break; default: break; } }); // 吧observer添加到RunLoop中 CFRunLoopAddObserver(CFRunLoopGetMain(), observer, kCFRunLoopCommonModes); // 釋放 CFRelease(observer); 複製代碼
從上圖能夠看出消息類型大概能夠分出兩種, 第一種類型又能夠細分爲三種, 這三種都是異步執行的微信
監聽程序的mach ports
,ports
能夠簡單的理解爲:內核經過port
這種方式將信息發送,而mach
則監聽內核發來的port
信息,而後將其整理,打包發給runloop
markdown
由開發人員本身發送, 蘋果也提供了一個CFRunLoopSource
來幫助處理, 簡單介紹核心實:數據結構
runloop
可以處理的樣式,即第一步定義的輸入源。它相似Mach
的功能NSObject類提供了不少方法供咱們使用,這些方法是添加到runloop的,因此若是沒有開啓runloop的話,不會運行架構
/// 主線程 - (void)performSelectorOnMainThread:(SEL)aSelector withObject:(nullable id)arg waitUntilDone:(BOOL)wait modes:(nullable NSArray<NSString *> *)array; - (void)performSelectorOnMainThread:(SEL)aSelector withObject:(nullable id)arg waitUntilDone:(BOOL)wait; /// 指定線程 - (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(nullable id)arg waitUntilDone:(BOOL)wait modes:(nullable NSArray<NSString *> *)array; - (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(nullable id)arg waitUntilDone:(BOOL)wait; /// 針對當前線程, 延遲調用 - (void)performSelector:(SEL)aSelector withObject:(nullable id)anArgument afterDelay:(NSTimeInterval)delay inModes:(NSArray<NSRunLoopMode> *)modes; - (void)performSelector:(SEL)aSelector withObject:(nullable id)anArgument afterDelay:(NSTimeInterval)delay; /// 取消,在當前線程,和上面兩個方法對應 + (void)cancelPreviousPerformRequestsWithTarget:(id)aTarget selector:(SEL)aSelector object:(nullable id)anArgument; + (void)cancelPreviousPerformRequestsWithTarget:(id)aTarget; 複製代碼
aSelector
, 通常狀況下aSelector
會添加到指定線程的runloop
wait
參數設爲YES
,那麼aSelector
會直接在指定線程運行,再也不添加到runloop
;wait
參數設爲YES
, 意味着要等待aSelector
執行完成以後纔回去執行後面的邏輯根據蘋果在文檔裏的說明,RunLoop
內部的邏輯大體以下:app
未查看RunLoop
的執行流程, 咱們能夠新建一個項目, 並簡單加一個觸發事件, 以下所示框架
bt
命令後, 就能看到完整的執行流程了UIApplicationMain
CFRunLoopRunSpecific
__CFRunLoopRun
__CFRunLoopDoSources0
[UIResponder touchesBegan:withEvent:]
觸發函數了CFRunLoop.c
文件, 搜索CFRunLoopRunSpecific
方法, 就是核心代碼了, 一塊兒來看看吧/// RunLoop的實現, 大概在文件的2622行 SInt32 CFRunLoopRunSpecific(CFRunLoopRef rl, CFStringRef modeName, CFTimeInterval seconds, Boolean returnAfterSourceHandled) { { /// 首先根據modeName找到對應mode CFRunLoopModeRef currentMode = __CFRunLoopFindMode(rl, modeName, false); /// 1. 通知 Observers: RunLoop 即將進入 loop。 __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopEntry); /// __CFRunLoopRun中具體要作的事情 result = __CFRunLoopRun(rl, currentMode, seconds, returnAfterSourceHandled, previousMode); /// 11. 通知 Observers: RunLoop 即將退出。 __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopExit); return result; } // __CFRunLoopRun的實現, 進入loop, 大概在文件的2304行 static int32_t __CFRunLoopRun(CFRunLoopRef rl, CFRunLoopModeRef rlm, CFTimeInterval seconds, Boolean stopAfterHandle, CFRunLoopModeRef previousMode) { int32_t retVal = 0; do { // 2. 通知 Observers: RunLoop 即將觸發 Timer 回調。 __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeTimers); // 3. 通知 Observers: RunLoop 即將觸發 Source0 (非port) 回調。 __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeSources); // 4. 處理block __CFRunLoopDoBlocks(rl, rlm); // 5. 處理Source0 Boolean sourceHandledThisLoop = __CFRunLoopDoSources0(rl, rlm, stopAfterHandle); // 若是處理Source0的結果是rrue if (sourceHandledThisLoop) { // 再次處理block __CFRunLoopDoBlocks(rl, rlm); } Boolean poll = sourceHandledThisLoop || (0ULL == timeout_context->termTSR); // 6. 若是有Source1 (基於port) 處於ready狀態,直接處理這個Source1而後跳轉去處理消息。 if (__CFRunLoopWaitForMultipleObjects(NULL, &dispatchPort, 0, 0, &livePort, NULL)) { // 若是有Source1, 就跳轉到handle_msg goto handle_msg; } // 7. 通知 Observers: RunLoop 的線程即將進入休眠(sleep) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeWaiting); __CFRunLoopSetSleeping(rl); // 7. 調用mach_msg等待接受mach_port的消息。線程將進入休眠, 等待別的消息來喚醒當前線程 // 一個基於 port 的Source 的事件。 // 一個 Timer 到時間了 // RunLoop 自身的超時時間到了 // 被其餘什麼調用者手動喚醒 __CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort, poll ? 0 : TIMEOUT_INFINITY, &voucherState, &voucherCopy); __CFRunLoopUnsetSleeping(rl); // 8. 通知Observers: 結束休眠, RunLoop的線程剛剛被喚醒了 __CFRunLoopDoObservers(rl, rlm, kCFRunLoopAfterWaiting); // 收到消息,處理消息。 handle_msg:; if (/* 被timer喚醒 */) { // 01. 處理Timer __CFRunLoopDoTimers(rl, rlm, mach_absolute_time()) } else if (/* 被gcd喚醒 */) { // 02. 處理gcd __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(msg); } else { // 被Source1喚醒 // 處理Source1 sourceHandledThisLoop = __CFRunLoopDoSource1(rl, rlm, rls) || sourceHandledThisLoop; } // 9. 處理Blocks __CFRunLoopDoBlocks(rl, rlm); // 10. 設置返回值, 根據不一樣的結果, 處理不一樣操做 if (sourceHandledThisLoop && stopAfterHandle) { // 進入loop時參數說處理完事件就返回。 retVal = kCFRunLoopRunHandledSource; } else if (timeout_context->termTSR < mach_absolute_time()) { // 超出傳入參數標記的超時時間了 retVal = kCFRunLoopRunTimedOut; } else if (__CFRunLoopIsStopped(rl)) { // 被外部調用者強制中止了 retVal = kCFRunLoopRunStopped; } else if (__CFRunLoopModeIsEmpty(rl, rlm, previousMode)) { // source/timer/observer一個都沒有了 retVal = kCFRunLoopRunFinished; } // 若是沒超時,mode裏沒空,loop也沒被中止,那繼續loop。 } while (0 == retVal); return retVal; } 複製代碼
從上面的代碼能夠看到RunLoop
其內部是一個do-while
循環; 當你調用CFRunLoopRun()
時,線程就會一直停留在這個循環裏;直到超時或被手動中止,該函數纔會返回
RunLoop
的核心是基於mach port
的,其進入休眠時調用的函數是mach_msg()
Mach
自己提供的API
很是有限,並且蘋果也不鼓勵使用Mach
的API
API
很是基礎,若是沒有這些API
的話,其餘任何工做都沒法實施Mach
中,全部的東西都是經過本身的對象實現的,進程、線程和虛擬內存都被稱爲」對象」Mach
的對象間不能直接調用,只能經過消息傳遞的方式實現對象間的通訊。Mach
中最基礎的概念,消息在兩個端口 (port
) 之間傳遞,這就是Mach
的IPC
(進程間通訊) 的核心。Mach
的消息定義是在<mach/message.h>
頭文件的,很簡單:
typedef struct { mach_msg_header_t header; mach_msg_body_t body; } mach_msg_base_t; typedef struct { mach_msg_bits_t msgh_bits; mach_msg_size_t msgh_size; mach_port_t msgh_remote_port; mach_port_t msgh_local_port; mach_port_name_t msgh_voucher_port; mach_msg_id_t msgh_id; } mach_msg_header_t; 複製代碼
Mach
消息實際上就是一個二進制數據包 (BLOB),其頭部定義了當前端口local_port
和目標端口remote_port
API
進行的,其option
標記了消息傳遞的方向:mach_msg_return_t mach_msg(
mach_msg_header_t *msg,
mach_msg_option_t option,
mach_msg_size_t send_size,
mach_msg_size_t rcv_size,
mach_port_name_t rcv_name,
mach_msg_timeout_t timeout,
mach_port_name_t notify
);
複製代碼
mach_msg()
函數其實是調用了一個Mach
陷阱(trap
),即函數mach_msg_trap()
,陷阱這個概念在Mach
中等同於系統調用mach_msg_trap()
時會觸發陷阱機制,切換到內核態;內核態中內核實現的mach_msg()
函數會完成實際的工做mach_msg()
, 若是沒有消息就讓線程休眠,有消息就喚醒線程RunLoop
的核心就是一個mach_msg()
(見上面代碼的第7步),RunLoop
調用這個函數去接收消息,若是沒有別人發送port
消息過來,內核會將線程置於等待狀態iOS
的App
,而後在App
靜止時點擊暫停,你會看到主線程調用棧是停留在mach_msg_trap()
這個地方NSRunLoop
應用實踐解決NSTimer
在滑動時中止工做的問題
CFRunLoopMode
主要使用的通常有三種Mode
DefaultMode
是App
平時所處的狀態,TrackingRunLoopMode
是追蹤ScrollView
滑動時的狀態Timer
並加到DefaultMode
時,Timer
會獲得重複回調,但此時滑動一個TableView
時,RunLoop
會將mode
切換爲TrackingRunLoopMode
,這時Timer
就不會被回調,而且也不會影響到滑動操做#import "ViewController.h" @interface ViewController () // 在xib中添加一個可滾動的UITextView @property (weak, nonatomic) IBOutlet UITextView *textView; @property (assign, nonatomic) NSInteger timerCount; @end @implementation ViewController - (void)viewDidLoad { [super viewDidLoad]; // 添加一個定時器 [NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(timerClick) userInfo:nil repeats:YES]; } - (void)timerClick { NSLog(@"--------%ld", (long)self.timerCount++); } - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event { NSLog(@"touchesBegan"); } @end 複製代碼
scheduledTimerWithTimeInterval
方式添加的NSTimer
會默認被添加到DefaultMode
中UITextView
的時候, 從打印結果能夠看到, 定時器中止執行了, 結束滾動UITextView
的時候, 定時器方法會繼續執行2019-08-20 21:46:01.986843+0800 RunLoop[86811:3484205] --------0
2019-08-20 21:46:02.986723+0800 RunLoop[86811:3484205] --------1
2019-08-20 21:46:03.986040+0800 RunLoop[86811:3484205] --------2
2019-08-20 21:46:04.986274+0800 RunLoop[86811:3484205] --------3
2019-08-20 21:46:05.272525+0800 RunLoop[86811:3484205] touchesBegan
2019-08-20 21:46:12.291035+0800 RunLoop[86811:3484205] --------4
2019-08-20 21:46:12.986318+0800 RunLoop[86811:3484205] --------5
2019-08-20 21:46:13.986197+0800 RunLoop[86811:3484205] --------6
2019-08-20 21:46:14.986735+0800 RunLoop[86811:3484205] --------7
複製代碼
Timer
,在兩個Mode
中都能獲得回調Timer
分別加入這兩個Mode
Timer
加入到頂層的RunLoop
的commonModeItems
中commonModeItems
被RunLoop
自動更新到全部具備Common
屬性的Mode
裏去CommonModes
並非一個真的模式,它只是一個標記NSTimer *timer = [NSTimer timerWithTimeInterval:1 repeats:YES block:^(NSTimer * _Nonnull timer) { NSLog(@"--------%ld", (long)self.timerCount++); }]; [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes]; 複製代碼
歡迎您掃一掃下面的微信公衆號,訂閱個人博客!