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
數據結構
由開發人員本身發送, 蘋果也提供了一個CFRunLoopSource
來幫助處理, 簡單介紹核心實:架構
runloop
可以處理的樣式,即第一步定義的輸入源。它相似Mach
的功能NSObject類提供了不少方法供咱們使用,這些方法是添加到runloop的,因此若是沒有開啓runloop的話,不會運行app
/// 主線程
- (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
內部的邏輯大體以下:框架
未查看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];
複製代碼
歡迎您掃一掃下面的微信公衆號,訂閱個人博客!