RunLoop實際上是一個事件處理循環,被用做工做調度而且協調傳入事件的接收。通常狀況下,單條線程一次只能執行一個任務,執行完成以後線程就會退出,若是咱們但願線程可以隨時的處理事件而且不會退出,那麼就在線程中開啓一個RunLoop,RunLoop其實就是一個運行循環,它的主要目的是可以讓線程在有工做的時候保持忙碌,在沒有工做的時候進入休眠,這樣作的好處就是讓線程進入休眠以後避免資源佔用。html
RunLoop的結構以下安全
RunLoop在循環過程當中處理定時器事件、performSelector事件、自定義源以及端口事件等等。bash
RunLoop的整個運行邏輯其實能夠概括爲如下代碼app
function loop() {
initialize();
do {
//休眠中等待消息
var message = get_next_message();
//處理消息
process_message(message);
} while (message != quit);
}
複製代碼
因而可知,其實RunLoop就是一個對象,它管理了須要進行處理的事件和消息,而且爲線程提供了一個如上的入口函數,當線程執行了這個入口函數以後,就會進入一個「接收消息->休眠等待->處理消息」的循環之中,一直到循環結束。框架
Cocoa和Core Foundation框架都提供了運行循環對象來幫助咱們配置和管理線程的運行循環:NSRunLoop和CFRunLoopRef異步
RunLoop的主要做用能夠總結爲如下幾點:函數
運行循環其實不是徹底自動的,當咱們建立一個線程的時候須要在適當的時間啓動運行循環而且響應傳入的事件,上文中說到Cocoa和Core Foundation框架都提供了運行循環對象來幫助咱們配置和管理線程的運行循環,所以咱們不須要顯式的建立運行循環對象,程序中的每一個線程,包括應用程序的主線程都有一個關聯的RunLoop對象。可是,只有子線程須要顯式的開啓它們的運行循環。而主線程做爲應用程序啓動過程的一部分,應用程序框架會在主線程上自動設置而且運行RunLoop。oop
蘋果的API爲咱們提供了兩種方法獲取RunLoop對象性能
閱讀CF源碼,咱們能夠獲得獲取RunLoop函數的內部邏輯以下學習
//獲取線程對應的RunLoop,若是t=0表示獲取主線程的RunLoop
CF_EXPORT CFRunLoopRef _CFRunLoopGet0(pthread_t t) {
//若是當前線程爲nil
if (pthread_equal(t, kNilPthreadT)) {
//獲取主線程
t = pthread_main_thread_np();
}
__CFLock(&loopsLock);
if (!__CFRunLoops) {
__CFUnlock(&loopsLock);
//第一次進入時,全局字典__CFRunLoops爲nil,因此須要初始化全局dic
CFMutableDictionaryRef dict = CFDictionaryCreateMutable(kCFAllocatorSystemDefault, 0, NULL, &kCFTypeDictionaryValueCallBacks);
//首先爲主線程建立一個RunLoop
CFRunLoopRef mainLoop = __CFRunLoopCreate(pthread_main_thread_np());
//將主線程RunLoop存入全局字典中
CFDictionarySetValue(dict, pthreadPointer(pthread_main_thread_np()), mainLoop);
if (!OSAtomicCompareAndSwapPtrBarrier(NULL, dict, (void * volatile *)&__CFRunLoops)) {
CFRelease(dict);
}
CFRelease(mainLoop);
__CFLock(&loopsLock);
}
//根據線程t到全局字典__CFRunLoops中獲取對應的RunLoop
CFRunLoopRef loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
__CFUnlock(&loopsLock);
if (!loop) {
//若是不存在對應RunLoop,則爲當前線程t新建一個RunLoop
CFRunLoopRef newLoop = __CFRunLoopCreate(t);
__CFLock(&loopsLock);
loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
if (!loop) {
//而且將新建的RunLoop保存到全局字典中去
CFDictionarySetValue(__CFRunLoops, pthreadPointer(t), newLoop);
loop = newLoop;
}
__CFUnlock(&loopsLock);
CFRelease(newLoop);
}
if (pthread_equal(t, pthread_self())) {
_CFSetTSD(__CFTSDKeyRunLoop, (void *)loop, NULL);
if (0 == _CFGetTSD(__CFTSDKeyRunLoopCntr)) {
// 註冊一個回調,當線程銷燬時,順便也銷燬其對應的 RunLoop。
_CFSetTSD(__CFTSDKeyRunLoopCntr, (void *)(PTHREAD_DESTRUCTOR_ITERATIONS-1), (void (*)(void *))__CFFinalizeRunLoop);
}
}
return loop;
}
複製代碼
因而可知線程和RunLoop的關係以下
在Core Foundation框架中提供了關於RunLoop的5個類
它們之間的關係以下:
在一個RunLoop對象當中包含了多個Mode,而每一個Mode又包含了若干個Source0/Source1/Timer/Observer,RunLoop啓動的時候只能指定其中的一個Mode,這個Mode被稱爲CurrentMode。若是須要切換Mode,只能先退出當前Loop,而後從新指定Mode再進入Loop。這樣作的主要目的就是爲了分割不一樣組的Source0/Source1/Timer/Observer,讓它們互不影響。
若是Mode中沒有任何的Source0/Source1/Timer/Observer,那麼RunLoop會當即退出。這裏的Source0/Source1/Timer/Observer統稱爲一個Mode item。
CFRunLoopRef其實就是Core Foundation框架提供的RunLoop對象。
CFRunLoopModeRef其實就是就是多個Source0/Source1/Timer/Observer的集合。每次運行RunLoop循環時,都要指定特定的模式,在RunLoop循環過程中,只監視與該模式相關聯的源,而且容許它們進行事件交付。相似的,也只有關聯了該模式的觀察者才能監聽到RunLoop的狀態變化。CFRunLoopModeRef的大體結構以下
struct __CFRunLoopMode {
CFStringRef _name; //Mode名稱
Boolean _stopped;
char _padding[3];
CFMutableSetRef _sources0; //Source0
CFMutableSetRef _sources1; //Source1
CFMutableArrayRef _observers; //觀察者集合
CFMutableArrayRef _timers; //定時器集合
......
}
struct __CFRunLoop {
__CFPort _wakeUpPort; // used for CFRunLoopWakeUp
Boolean _unused;
volatile _per_run_data *_perRunData; // reset for runs of the run loop
CFMutableSetRef _commonModes; // 全部標記爲Common的Mode的name集合
CFMutableSetRef _commonModeItems; // 全部被標記爲Common的Source/Observer/Timer
CFRunLoopModeRef _currentMode; // 當前RunLoop指定的Mode
CFMutableSetRef _modes; // 全部Mode集合
......
};
複製代碼
Mode中主要包含如下幾種元素:
蘋果提供了兩種公開的Mode,kCFRunLoopDefaultMode (NSDefaultRunLoopMode) 和UITrackingRunLoopMode
而在RunLoop對象中,集合_modes包含了全部RunLoop支持的Mode。同時,RunLoop還支持一種「CommonModes」的概念,每一個Mode都能將其標記爲「Common」屬性(具體是將Mode的name添加到RunLoop的_commonModes集合當中),主線程的RunLoop兩個預置的Mode:kCFRunLoopDefaultMode (NSDefaultRunLoopMode) 和UITrackingRunLoopMode都已經被標記爲了「Common」。
RunLoop中的_commonModeItems集合就是用來存放被標記爲common的Source/Observer/Timer,當RunLoop狀態發生變化時,會將_commonModeItems中的全部的Source/Observer/Timer同步到具備common標識的全部Mode中。
以定時器舉例,咱們在主線程中添加一個定時器,而且添加到NSDefaultRunLoopMode當中,定時器會正常回調。此時若是界面上存在ScrollView,而且滑動ScrollView,RunLoop就會切換到UITrackingRunLoopMode模式下,此時,因爲定時器只存在於NSDefaultRunLoopMode模式中,一旦切換到UITrackingRunLoopMode模式,定時器便會中止,等待RunLoop從新切換到DefaultMode時恢復運行。若是想要定時器在兩種模式下都可以正常運行,能夠將定時器同時添加到兩種Mode中,還有一種方式,就是將定時器標記爲「common」,其實也就是將定時器加入到RunLoop對象的_commonModeItems中去。此時RunLoop會自動將定時器同步到具備Common標記的Mode中去。代碼以下:
NSTimer *timer = [NSTimer timerWithTimeInterval:1.0 repeats:YES block:^(NSTimer * _Nonnull timer) {
NSLog(@"定時器任務");
}];
//方法一
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
[[NSRunLoop currentRunLoop] addTimer:timer forMode:UITrackingRunLoopMode];
//方法二
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
複製代碼
除了使用蘋果公開的Mode外,咱們還能夠建立自定義Mode,具體接口以下
//指定RunLoop建立Mode,主要是經過mode name來建立,若是RunLoop內部根據mode name沒有發現對應的mode,RunLoop則會自動建立CFRunLoopModeRef
CFRunLoopAddCommonMode(CFRunLoopRef runloop, CFStringRef modeName);
CFRunLoopRunInMode(CFStringRef modeName, ...);
複製代碼
同時,Mode中也提供了對Mode Item的操做函數,以下
CFRunLoopAddSource(CFRunLoopRef rl, CFRunLoopSourceRef source, CFStringRef modeName);
CFRunLoopAddObserver(CFRunLoopRef rl, CFRunLoopObserverRef observer, CFStringRef modeName);
CFRunLoopAddTimer(CFRunLoopRef rl, CFRunLoopTimerRef timer, CFStringRef mode);
CFRunLoopRemoveSource(CFRunLoopRef rl, CFRunLoopSourceRef source, CFStringRef modeName);
CFRunLoopRemoveObserver(CFRunLoopRef rl, CFRunLoopObserverRef observer, CFStringRef modeName);
CFRunLoopRemoveTimer(CFRunLoopRef rl, CFRunLoopTimerRef timer, CFStringRef mode);
複製代碼
CFRunLoopSourceRef又稱之爲輸入源,它是事件產生的地方。輸入源是將事件異步傳遞到線程中。事件的源則取決於輸入源的類型,而輸入源通常分爲兩類:第一類是自定義輸入源,監視自定義事件源,又稱爲Source0。第二類是基於端口的輸入源,監視應用程序的Mach端口,又稱爲Source1
舉一個簡單的例子:當App在前臺靜止時,若是咱們點擊App的頁面,此時咱們首先接觸的是手機屏幕,此時,觸摸屏幕的事件會被包裝成Event傳遞給source1,而後source1主動喚醒RunLoop,以後將事件傳遞給Source0來進行處理。
CFRunLoopTimerRef是定時器源,它會在未來某個預設的時間點將事件同步發送到線程。定時器是線程通知本身作某件事的一種方式。定時器生成基於時間的通知,可是它並非實時的。與輸入源同樣,計時器和運行循環的特定mode相關聯。
咱們能夠配置定時器來生成一次或者屢次事件,重複定時器會根據預約的觸發時間(而不是實際的觸發時間)自動從新調度本身。例如一個定時器在一個特定的時間觸發,而且在以後每5s觸發一次,那麼計劃的觸發時間將始終落在最初的5s間隔上。若是此時RunLoop正在處理一個耗時的任務,那麼定時器的觸發時間會被延時。假設RunLoop執行的耗時任務爲12s,那麼在RunLoop執行完完耗時任務以後,定時器會當即觸發一次,而後定時器會從新安排下一次預約的觸發時間。也就是說在耗時的12s內,只會觸發一次定時器。
CFRunLoopObserverRef是觀察者,每個Observer都包含一個回調,當RunLoop狀態發生變化時,觀察者就可以經過回調接收到變化狀態。RunLoop有如下幾種狀態能被觀測
/* Run Loop Observer Activities */
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
kCFRunLoopEntry = (1UL << 0), //即將進入loop
kCFRunLoopBeforeTimers = (1UL << 1), //即將處理Timer
kCFRunLoopBeforeSources = (1UL << 2), //即將處理Source
kCFRunLoopBeforeWaiting = (1UL << 5), //即將進入休眠
kCFRunLoopAfterWaiting = (1UL << 6), //從休眠中喚醒
kCFRunLoopExit = (1UL << 7), //即將退出loop
kCFRunLoopAllActivities = 0x0FFFFFFFU
};
複製代碼
在代碼中,咱們可使用如下方式來監聽RunLoop的狀態變化
CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(kCFAllocatorDefault, kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
switch (activity) {
case kCFRunLoopEntry:{
NSLog(@"即將進入loop");
break;
}
case kCFRunLoopBeforeTimers:{
NSLog(@"即將處理Timer");
break;
}
case kCFRunLoopBeforeSources:{
NSLog(@"即將處理Source");
break;
}
case kCFRunLoopBeforeWaiting:{
NSLog(@"即將進入休眠");
break;
}
case kCFRunLoopAfterWaiting:{
NSLog(@"從休眠中喚醒");
break;
}
case kCFRunLoopExit:{
NSLog(@"即將退出loop");
break;
}
default:
break;
}
});
CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopCommonModes);
CFRelease(observer);
複製代碼
RunLoop運行邏輯流程圖以下:
此處是對照官方文檔的流程,其中忽略了對block的處理
想要查看RunLoop源碼,首先須要知道RunLoop的入口函數,方法很簡單,新建項目,在項目啓動後在任意處斷點,而後經過LLDB指令bt能夠獲得調用棧以下:
其中CFRunLoopRunSpecific函數就是RunLoop的入口函數,以下(此處只保留部分主要代碼):
SInt32 CFRunLoopRunSpecific(CFRunLoopRef rl, CFStringRef modeName, CFTimeInterval seconds, Boolean returnAfterSourceHandled) { /* DOES CALLOUT */
//第一步、通知Observers:即將進入loop
__CFRunLoopDoObservers(rl, currentMode, kCFRunLoopEntry);
//內部函數,進入loop
result = __CFRunLoopRun(rl, currentMode, seconds, returnAfterSourceHandled, previousMode);
//第十步、通知Observers:退出loop
__CFRunLoopDoObservers(rl, currentMode, kCFRunLoopExit);
return result;
}
複製代碼
loop的真正核心就是__CFRunLoopRun函數,源碼以下:
static int32_t __CFRunLoopRun(CFRunLoopRef rl, CFRunLoopModeRef rlm, CFTimeInterval seconds, Boolean stopAfterHandle, CFRunLoopModeRef previousMode) {
Boolean didDispatchPortLastTime = true;
int32_t retVal = 0;
do {
//第二步、通知Observers:即將處理Timers
__CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeTimers);
//第三步、通知Observers:即將處理Source0(非port)
__CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeSources);
//執行被加入的block
__CFRunLoopDoBlocks(rl, rlm);
//第四步、處理Source0,觸發Source0回調
Boolean sourceHandledThisLoop = __CFRunLoopDoSources0(rl, rlm, stopAfterHandle);
if (sourceHandledThisLoop) {
//再次執行執行被加入的block
__CFRunLoopDoBlocks(rl, rlm);
}
Boolean poll = sourceHandledThisLoop || (0ULL == timeout_context->termTSR);
//第五步、判斷是否有Source1須要進行處理,若是有,跳轉到handle_msg標記處執行
if (MACH_PORT_NULL != dispatchPort && !didDispatchPortLastTime) {
msg = (mach_msg_header_t *)msg_buffer;
if (__CFRunLoopServiceMachPort(dispatchPort, &msg, sizeof(msg_buffer), &livePort, 0, &voucherState, NULL)) {
goto handle_msg;
}
}
didDispatchPortLastTime = false;
//第六步、通知Observers,RunLoop所在線程即將進入休眠
if (!poll && (rlm->_observerMask & kCFRunLoopBeforeWaiting)) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeWaiting);
//第七步、調用內核函數mach_msg,讓線程進入休眠,直到被如下事件喚醒
//一、接收到Source1事件
//二、啓動Timer
//三、爲運行循環設置超時值過時
//四、被外部手動喚醒
__CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort, poll ? 0 : TIMEOUT_INFINITY, &voucherState, &voucherCopy);
//第八步、通知Observers:RunLoop線程剛被喚醒
if (!poll && (rlm->_observerMask & kCFRunLoopAfterWaiting)) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopAfterWaiting);
//第九步、接收到消息開始進行處理
handle_msg:;
if (被Timer喚醒) {
//9-一、若是用戶自定義的計時器觸發,則處理計時器事件並從新啓動循環
__CFRunLoopDoTimers(rl, rlm, mach_absolute_time()
}
else if (被GCD喚醒) {
//9-二、若是GCD子線程中有回到主線程的操做,那麼喚醒線程,執行主線程的block
__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(msg);
} else {
//9-三、若是有基於port的Source1事件傳入,則處理Source1事件
CFRunLoopSourceRef rls = __CFRunLoopModeFindSourceForMachPort(rl, rlm, livePort);
if (rls) {
mach_msg_header_t *reply = NULL;
sourceHandledThisLoop = __CFRunLoopDoSource1(rl, rlm, rls, msg, msg->msgh_size, &reply) || sourceHandledThisLoop;
if (NULL != reply) {
(void)mach_msg(reply, MACH_SEND_MSG, reply->msgh_size, 0, MACH_PORT_NULL, 0, MACH_PORT_NULL); }
}
}
//再次執行被加入的block
__CFRunLoopDoBlocks(rl, rlm);
if (sourceHandledThisLoop && stopAfterHandle) {
//若是當前事件處理完成就直接返回
retVal = kCFRunLoopRunHandledSource;
} else if (timeout_context->termTSR < mach_absolute_time()) {
//RunLoop到超時時間了
retVal = kCFRunLoopRunTimedOut;
} else if (__CFRunLoopIsStopped(rl)) {
//RunLoop被強制中止了
retVal = kCFRunLoopRunStopped;
} else if (rlm->_stopped) {
rlm->_stopped = false;
retVal = kCFRunLoopRunStopped;
} else if (__CFRunLoopModeIsEmpty(rl, rlm, previousMode)) {
//若是RunLoop當前指定的Mode中Source/Timer/Observer都爲空
retVal = kCFRunLoopRunFinished;
}
//若是RunLoop沒有超時,沒有被中止,指定的Mode中存在ModeItem,則一直運行RunLoop
} while (0 == retVal);
return retVal;
}
複製代碼
RunLoop在進行回調時,都會調用一個特別長的函數,例如調用__CFRunLoopDoObservers通知Observers即將進入RunLoop時,內部會調用CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION函數。如下將源碼中對應的函數轉換成了實際調用的函數,以下
SInt32 CFRunLoopRunSpecific(CFRunLoopRef rl, CFStringRef modeName, CFTimeInterval seconds, Boolean returnAfterSourceHandled) { /* DOES CALLOUT */
//第一步、通知Observers:即將進入loop
__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopEntry);
//內部函數,進入loop
result = __CFRunLoopRun(rl, currentMode, seconds, returnAfterSourceHandled, previousMode);
//第十步、通知Observers:退出loop
__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopExit);
return result;
}
static int32_t __CFRunLoopRun(CFRunLoopRef rl, CFRunLoopModeRef rlm, CFTimeInterval seconds, Boolean stopAfterHandle, CFRunLoopModeRef previousMode) {
Boolean didDispatchPortLastTime = true;
int32_t retVal = 0;
do {
//第二步、通知Observers:即將處理Timers
__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopBeforeTimers);
//第三步、通知Observers:即將處理Source0(非port)
__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopBeforeSources);
//執行被加入的block
__CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__(block);
//第四步、處理Source0,觸發Source0回調
Boolean sourceHandledThisLoop = __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__(source0);
if (sourceHandledThisLoop) {
//再次執行執行被加入的block
__CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__(block);
}
Boolean poll = sourceHandledThisLoop || (0ULL == timeout_context->termTSR);
//第五步、判斷是否有Source1須要進行處理,若是有,跳轉到handle_msg標記處執行
if (MACH_PORT_NULL != dispatchPort && !didDispatchPortLastTime) {
msg = (mach_msg_header_t *)msg_buffer;
if (__CFRunLoopServiceMachPort(dispatchPort, &msg, sizeof(msg_buffer), &livePort, 0, &voucherState, NULL)) {
goto handle_msg;
}
}
didDispatchPortLastTime = false;
//第六步、通知Observers,RunLoop所在線程即將進入休眠
__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopBeforeWaiting);
//第七步、調用內核函數mach_msg,讓線程進入休眠,直到被如下事件喚醒
//一、接收到Source1事件
//二、啓動Timer
//三、爲運行循環設置超時值過時
//四、被外部手動喚醒
__CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort, poll ? 0 : TIMEOUT_INFINITY, &voucherState, &voucherCopy);
//第八步、通知Observers:RunLoop線程剛被喚醒
__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopAfterWaiting);
//第九步、接收到消息開始進行處理
handle_msg:;
if (被Timer喚醒) {
//9-一、若是用戶自定義的計時器觸發,則處理計時器事件並從新啓動循環
__CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__(timer);
}
else if (被GCD喚醒) {
//9-二、若是GCD子線程中有回到主線程的操做,那麼喚醒線程,執行主線程的block
__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(dispatched_block);
} else {
//9-三、若是有基於port的Source1事件傳入,則處理Source1事件
__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION__(source1);
}
//再次執行被加入的block
__CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__(block);
//若是RunLoop沒有超時,沒有被中止,指定的Mode中存在ModeItem,則一直運行RunLoop
} while (0 == retVal);
return retVal;
}
複製代碼
具體流程圖總結以下:
流程中穿插着對block的處理,其中的block能夠經過CFRunLoopPerformBlock函數來添加。
RunLoop一個很重要的做用就是能用來控制線程的生命週期,也就是線程包活。主線程的RunLoop默認開啓,所以主線程一直處於活躍狀態,可是子線程默認沒有開啓RunLoop,因此子線程執行完任務以後就會被銷燬。早期AFNetworking 2.x版本就使用了RunLoop來保活線程。具體實現以下:
XLThread.h源碼以下
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
typedef void(^XLThreadTask)(void);
@interface XLThread : NSObject
/** 開始任務 */
- (void)executeTask:(XLThreadTask)task;
/** 結束線程 */
- (void)stop;
@end
NS_ASSUME_NONNULL_END
複製代碼
XLThread.m以下
#import "XLThread.h"
#import <objc/runtime.h>
@interface XLThread ()
@property(nonatomic, strong)NSThread *innerThread;
@end
@implementation XLThread
#pragma mark - Public
- (instancetype)init
{
self = [super init];
if (self) {
self.innerThread = [[NSThread alloc] initWithTarget:self selector:@selector(__initThread) object:nil];
[self.innerThread start];
}
return self;
}
- (void)executeTask:(XLThreadTask)task{
if (!self.innerThread || !task) {
return;
}
[self performSelector:@selector(__executeTask:) onThread:self.innerThread withObject:task waitUntilDone:NO];
}
- (void)stop
{
if (!self.innerThread) return;
[self performSelector:@selector(__stop) onThread:self.innerThread withObject:nil waitUntilDone:YES];
}
- (void)dealloc
{
SBPLog(@"%s", __func__);
[self stop];
}
#pragma mark - Private
- (void)__stop{
CFRunLoopStop(CFRunLoopGetCurrent());
self.innerThread = nil;
}
- (void)__executeTask:(XLThreadTask)task{
task();
}
- (void)__initThread{
//建立上下文
CFRunLoopSourceContext context = {0};
//建立source
CFRunLoopSourceRef source = CFRunLoopSourceCreate(kCFAllocatorDefault, 0, &context);
//向runloop中添加source
CFRunLoopAddSource(CFRunLoopGetCurrent(), source, kCFRunLoopDefaultMode);
//銷燬source
CFRelease(source);
//啓動runLoop
CFRunLoopRunInMode(kCFRunLoopDefaultMode, 1.0e10, false);
}
@end
複製代碼
在iOS中,AutoreleasePool其實也是基於RunLoop來實現的,App啓動時會在主線程的RunLoop中註冊兩個Observer。
關於AutoreleasePool的具體實現會在後面的章節詳細介紹。
前面說到的CFRunLoopTimerRef其實就是NSTimer,NSTimer的觸發必須基於RunLoop,而且RunLoop須要處於開啓狀態。一般咱們會在主線程中使用定時器,由於主線程的RunLoop默認開啓,若是想要在子線程中使用定時器,就須要手動開啓子線程RunLoop。
當咱們建立NSTimer,而後將其註冊到RunLoop後,RunLoop會根據預設的觸發時間在重複的時間點註冊好事件,例如設置定時器在5:10分開始觸發,而且每隔5m觸發一次,定時器的觸發時間就是固定的5:十、5:1五、5:20、5:25等等,可是RunLoop爲了節省資源,並不會在很是準確的時間點觸發定時器。若是在觸發定時器以前RunLoop執行了一個很長的任務,以致於錯過了預設的時間點,那麼在長時間任務執行完成以後會當即觸發一次定時器,而後等到下一個預設的時間點再次觸發。
假設原定於5:10分觸發定時器,可是RunLoop因爲執行耗時任務到5:22,那麼此時,在耗時任務執行完成以後會當即觸發一次定時器任務,而後等待到5:25時再次觸發定時器。
上文提到,RunLoop中輸入源分爲兩種,一種是基於port的輸入源,還有一種是自定義輸入源。
Cocoa框架爲咱們提供了一種自定義源(Perform Selector Source),可讓咱們在任何線程上執行Selector,而且在執行完Selector以後,該源會自動從RunLoop中移除。當咱們使用performSelector在另外一個線程中執行Selector時,必需要保證目標線程中有開啓RunLoop,所以就須要咱們顯式的啓動目標線程的RunLoop。
如下就是在NSObject中聲明的performSelector方法
方法 | 描述 |
---|---|
performSelectorOnMainThread:withObject:waitUntilDone: performSelectorOnMainThread:withObject:waitUntilDone:modes: |
在應用程序主線程的RunLoop的下一次循環週期內執行指定的Selector。 而且該方法能夠阻塞當前線程,直到執行Selector爲止。 |
performSelector:onThread:withObject:waitUntilDone: performSelector:onThread:withObject:waitUntilDone:modes: |
在任何線程上指向指定的Selector。 而且該方法能夠阻塞當前線程,直到執行Selector爲止。 |
performSelector:withObject:afterDelay: performSelector:withObject:afterDelay:inModes: |
在下一個運行循環週期中以及可選的延遲時間以後,在當前線程上執行指定的selector。 由於是等到下一個運行循環週期執行selector,因此這些方法提供了當前執行代碼的最小自動延遲。 多個排隊的選擇器按照排隊的順序依次執行 |
cancelPreviousPerformRequestsWithTarget: cancelPreviousPerformRequestsWithTarget:selector:object: |
取消發送到當前線程的消息 |
舉一個簡單的例子,在touchesBegan:方法中添加如下示例代碼
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
NSLog(@"任務1");
[self performSelector:@selector(test) withObject:nil afterDelay:.0];
NSLog(@"任務3");
}
- (void)test{
NSLog(@"任務2");
}
複製代碼
當點擊頁面時,執行順序依次是任務一、任務3和任務2。緣由很簡單,由於performSelector:withObject:afterDelay方法本質實際上是建立一個定時器NSTimer,而且將定時器添加到RunLoop中進行處理。整個流程大體以下:
以上內容純屬我的理解,若是有什麼不對的地方歡迎留言指正。
一塊兒學習,一塊兒進步~~~