運行循環,在程序運行過程當中循環作一些事情,若是沒有Runloop程序執行完畢就會當即退出,若是有Runloop程序會一直運行,而且時時刻刻在等待用戶的輸入操做。RunLoop能夠在須要的時候本身跑起來運行,在沒有操做的時候就停下來休息。充分節省CPU資源,提升程序性能。html
UIApplicationMain函數內啓動了Runloop,程序不會立刻退出,而是保持運行狀態。所以每個應用必需要有一個runloop, 咱們知道主線程一開起來,就會跑一個和主線程對應的RunLoop,那麼RunLoop必定是在程序的入口main函數中開啓。面試
int main(int argc, char * argv[]) {
@autoreleasepool {
return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
}
}
複製代碼
進入UIApplicationMain數組
UIKIT_EXTERN int UIApplicationMain(int argc, char *argv[], NSString * __nullable principalClassName, NSString * __nullable delegateClassName);
複製代碼
咱們發現它返回的是一個int數,那麼咱們對main函數作一些修改xcode
int main(int argc, char * argv[]) {
@autoreleasepool {
NSLog(@"開始");
int re = UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
NSLog(@"結束");
return re;
}
}
複製代碼
運行程序,咱們發現只會打印開始,並不會打印結束,這說明在UIApplicationMain函數中,開啓了一個和主線程相關的RunLoop,致使UIApplicationMain不會返回,一直在運行中,也就保證了程序的持續運行。 咱們來看到RunLoop的源碼bash
// 用DefaultMode啓動
void CFRunLoopRun(void) { /* DOES CALLOUT */
int32_t result;
do {
result = CFRunLoopRunSpecific(CFRunLoopGetCurrent(), kCFRunLoopDefaultMode, 1.0e10, false);
CHECK_FOR_FORK();
} while (kCFRunLoopRunStopped != result && kCFRunLoopRunFinished != result);
}
複製代碼
咱們發現RunLoop確實是do while經過判斷result的值實現的。所以,咱們能夠把RunLoop當作一個死循環。若是沒有RunLoop,UIApplicationMain函數執行完畢以後將直接返回,也就沒有程序持續運行一說了。app
Fundation框架 (基於CFRunLoopRef的封裝) NSRunLoop對象框架
CoreFoundation CFRunLoopRef對象async
由於Fundation框架是基於CFRunLoopRef的一層OC封裝,這裏咱們主要研究CFRunLoopRef源碼ide
Foundation
[NSRunLoop currentRunLoop]; // 得到當前線程的RunLoop對象
[NSRunLoop mainRunLoop]; // 得到主線程的RunLoop對象
Core Foundation
CFRunLoopGetCurrent(); // 得到當前線程的RunLoop對象
CFRunLoopGetMain(); // 得到主線程的RunLoop對象
複製代碼
- 每條線程都有惟一的一個與之對應的RunLoop對象
- RunLoop保存在一個全局的Dictionary裏,線程做爲key,RunLoop做爲value
- 主線程的RunLoop已經自動建立好了,子線程的RunLoop須要主動建立
- RunLoop在第一次獲取時建立,在線程結束時銷燬
// 拿到當前Runloop 調用_CFRunLoopGet0
CFRunLoopRef CFRunLoopGetCurrent(void) {
CHECK_FOR_FORK();
CFRunLoopRef rl = (CFRunLoopRef)_CFGetTSD(__CFTSDKeyRunLoop);
if (rl) return rl;
return _CFRunLoopGet0(pthread_self());
}
// 查看_CFRunLoopGet0方法內部
CF_EXPORT CFRunLoopRef _CFRunLoopGet0(pthread_t t) {
if (pthread_equal(t, kNilPthreadT)) {
t = pthread_main_thread_np();
}
__CFLock(&loopsLock);
if (!__CFRunLoops) {
__CFUnlock(&loopsLock);
CFMutableDictionaryRef dict = CFDictionaryCreateMutable(kCFAllocatorSystemDefault, 0, NULL, &kCFTypeDictionaryValueCallBacks);
// 根據傳入的主線程獲取主線程對應的RunLoop
CFRunLoopRef mainLoop = __CFRunLoopCreate(pthread_main_thread_np());
// 保存主線程 將主線程-key和RunLoop-Value保存到字典中
CFDictionarySetValue(dict, pthreadPointer(pthread_main_thread_np()), mainLoop);
if (!OSAtomicCompareAndSwapPtrBarrier(NULL, dict, (void * volatile *)&__CFRunLoops)) {
CFRelease(dict);
}
CFRelease(mainLoop);
__CFLock(&loopsLock);
}
// 從字典裏面拿,將線程做爲key從字典裏獲取一個loop
CFRunLoopRef loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
__CFUnlock(&loopsLock);
// 若是loop爲空,則建立一個新的loop,因此runloop會在第一次獲取的時候建立
if (!loop) {
CFRunLoopRef newLoop = __CFRunLoopCreate(t);
__CFLock(&loopsLock);
loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
// 建立好以後,以線程爲key runloop爲value,一對一存儲在字典中,下次獲取的時候,則直接返回字典內的runloop
if (!loop) {
CFDictionarySetValue(__CFRunLoops, pthreadPointer(t), newLoop);
loop = newLoop;
}
// don't release run loops inside the loopsLock, because CFRunLoopDeallocate may end up taking it __CFUnlock(&loopsLock); CFRelease(newLoop); } if (pthread_equal(t, pthread_self())) { _CFSetTSD(__CFTSDKeyRunLoop, (void *)loop, NULL); if (0 == _CFGetTSD(__CFTSDKeyRunLoopCntr)) { _CFSetTSD(__CFTSDKeyRunLoopCntr, (void *)(PTHREAD_DESTRUCTOR_ITERATIONS-1), (void (*)(void *))__CFFinalizeRunLoop); } } return loop; } 複製代碼
從上面的代碼能夠看出,線程和 RunLoop 之間是一一對應的,其關係是保存在一個 Dictionary 裏。因此咱們建立子線程RunLoop時,只需在子線程中獲取當前線程的RunLoop對象便可[NSRunLoop currentRunLoop];
若是不獲取,那子線程就不會建立與之相關聯的RunLoop,而且只能在一個線程的內部獲取其 RunLoop [NSRunLoop currentRunLoop];
方法調用時,會先看一下字典裏有沒有存子線程相對用的RunLoop,若是有則直接返回RunLoop,若是沒有則會建立一個,並將與之對應的子線程存入字典中。當線程結束時,RunLoop會被銷燬。函數
經過源碼咱們找到__CFRunLoop結構體
struct __CFRunLoop {
CFRuntimeBase _base;
pthread_mutex_t _lock; /* locked for accessing mode list */
__CFPort _wakeUpPort; // used for CFRunLoopWakeUp
Boolean _unused;
volatile _per_run_data *_perRunData; // reset for runs of the run loop
pthread_t _pthread;
uint32_t _winthread;
CFMutableSetRef _commonModes;
CFMutableSetRef _commonModeItems;
CFRunLoopModeRef _currentMode;
CFMutableSetRef _modes;
struct _block_item *_blocks_head;
struct _block_item *_blocks_tail;
CFAbsoluteTime _runTime;
CFAbsoluteTime _sleepTime;
CFTypeRef _counterpart;
};
複製代碼
除一些記錄性屬性外,主要來看一下如下兩個成員變量
CFRunLoopModeRef _currentMode;
CFMutableSetRef _modes;
複製代碼
CFRunLoopModeRef 實際上是指向__CFRunLoopMode結構體的指針,__CFRunLoopMode結構體源碼以下
typedef struct __CFRunLoopMode *CFRunLoopModeRef;
struct __CFRunLoopMode {
CFRuntimeBase _base;
pthread_mutex_t _lock; /* must have the run loop locked before locking this */
CFStringRef _name;
Boolean _stopped;
char _padding[3];
CFMutableSetRef _sources0;
CFMutableSetRef _sources1;
CFMutableArrayRef _observers;
CFMutableArrayRef _timers;
CFMutableDictionaryRef _portToV1SourceMap;
__CFPortSet _portSet;
CFIndex _observerMask;
#if USE_DISPATCH_SOURCE_FOR_TIMERS
dispatch_source_t _timerSource;
dispatch_queue_t _queue;
Boolean _timerFired; // set to true by the source when a timer has fired
Boolean _dispatchTimerArmed;
#endif
#if USE_MK_TIMER_TOO
mach_port_t _timerPort;
Boolean _mkTimerArmed;
#endif
#if DEPLOYMENT_TARGET_WINDOWS
DWORD _msgQMask;
void (*_msgPump)(void);
#endif
uint64_t _timerSoftDeadline; /* TSR */
uint64_t _timerHardDeadline; /* TSR */
};
複製代碼
主要查看如下成員變量
CFMutableSetRef _sources0;
CFMutableSetRef _sources1;
CFMutableArrayRef _observers;
CFMutableArrayRef _timers;
複製代碼
經過上面分析咱們知道,CFRunLoopModeRef表明RunLoop的運行模式,一個RunLoop包含若干個Mode,每一個Mode又包含若干個Source0/Source1/Timer/Observer,而RunLoop啓動時只能選擇其中一個Mode做爲currentMode。
1. Source1 : 基於Port的線程間通訊
2. Source0 : 觸摸事件,PerformSelectors
咱們經過代碼驗證一下
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
NSLog(@"點擊了屏幕");
}
複製代碼
打斷點以後打印堆棧信息,當xcode工具區打印的堆棧信息不全時,能夠在控制檯經過「bt」指令打印完整的堆棧信息,由堆棧信息中能夠發現,觸摸事件確實是會觸發Source0事件。
一樣的方式驗證performSelector堆棧信息
dispatch_async(dispatch_get_global_queue(0, 0), ^{
[self performSelectorOnMainThread:@selector(test) withObject:nil waitUntilDone:YES];
});
複製代碼
能夠發現PerformSelectors一樣是觸發Source0事件
其實,當咱們觸發了事件(觸摸/鎖屏/搖晃等)後,由IOKit.framework生成一個 IOHIDEvent事件,而IOKit是蘋果的硬件驅動框架,由它進行底層接口的抽象封裝與系統進行交互傳遞硬件感應的事件,並專門處理用戶交互設備,由IOHIDServices和IOHIDDisplays兩部分組成,其中IOHIDServices是專門處理用戶交互的,它會將事件封裝成IOHIDEvents對象,接着用mach port轉發給須要的App進程,隨後 Source1就會接收IOHIDEvent,以後再回調__IOHIDEventSystemClientQueueCallback(),__IOHIDEventSystemClientQueueCallback()內觸發Source0,Source0 再觸發 _UIApplicationHandleEventQueue()。因此觸摸事件看到是在 Source0 內的。
3. Timers : 定時器,NSTimer
經過代碼驗證
[NSTimer scheduledTimerWithTimeInterval:3.0 repeats:NO block:^(NSTimer * _Nonnull timer) {
NSLog(@"NSTimer ---- timer調用了");
}];
複製代碼
打印完整堆棧信息
4. Observer : 監聽器,用於監聽RunLoop的狀態
經過上面的分析,咱們對RunLoop內部結構有了大體的瞭解,接下來來詳細分析RunLoop的相關類。如下爲Core Foundation中關於RunLoop的5個類
CFRunLoopRef - 得到當前RunLoop和主RunLoop
CFRunLoopModeRef - RunLoop 運行模式,只能選擇一種,在不一樣模式中作不一樣的操做
CFRunLoopSourceRef - 事件源,輸入源
CFRunLoopTimerRef - 定時器時間
CFRunLoopObserverRef - 觀察者
CFRunLoopModeRef表明RunLoop的運行模式 一個 RunLoop 包含若干個 Mode,每一個Mode又包含若干個Source、Timer、Observer 每次RunLoop啓動時,只能指定其中一個 Mode,這個Mode被稱做 CurrentMode 若是須要切換Mode,只能退出RunLoop,再從新指定一個Mode進入,這樣作主要是爲了分隔開不一樣組的Source、Timer、Observer,讓其互不影響。若是Mode裏沒有任何Source0/Source1/Timer/Observer,RunLoop會立馬退出 如圖所示:
RunLoop 有五種運行模式,其中常見的有1.2兩種
1. kCFRunLoopDefaultMode:App的默認Mode,一般主線程是在這個Mode下運行
2. UITrackingRunLoopMode:界面跟蹤 Mode,用於 ScrollView 追蹤觸摸滑動,保證界面滑動時不受其餘 Mode 影響
3. UIInitializationRunLoopMode: 在剛啓動 App 時第進入的第一個 Mode,啓動完成後就再也不使用,會切換到kCFRunLoopDefaultMode
4. GSEventReceiveRunLoopMode: 接受系統事件的內部 Mode,一般用不到
5. kCFRunLoopCommonModes: 這是一個佔位用的Mode,做爲標記kCFRunLoopDefaultMode和UITrackingRunLoopMode用,並非一種真正的Mode
複製代碼
咱們平時在開發中必定遇到過,當咱們使用NSTimer每一段時間執行一些事情時滑動UIScrollView,NSTimer就會暫停,當咱們中止滑動之後,NSTimer又會從新恢復的狀況,咱們經過一段代碼來看一下
代碼中的註釋也很重要,展現了咱們探索的過程
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
// [NSTimer scheduledTimerWithTimeInterval:2.0 target:self selector:@selector(show) userInfo:nil repeats:YES];
NSTimer *timer = [NSTimer timerWithTimeInterval:2.0 target:self selector:@selector(show) userInfo:nil repeats:YES];
// 加入到RunLoop中才能夠運行
// 1. 把定時器添加到RunLoop中,而且選擇默認運行模式NSDefaultRunLoopMode = kCFRunLoopDefaultMode
// [[NSRunLoop mainRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
// 當textFiled滑動的時候,timer失效,中止滑動時,timer恢復
// 緣由:當textFiled滑動的時候,RunLoop的Mode會自動切換成UITrackingRunLoopMode模式,所以timer失效,當中止滑動,RunLoop又會切換回NSDefaultRunLoopMode模式,所以timer又會從新啓動了
// 2. 當咱們將timer添加到UITrackingRunLoopMode模式中,此時只有咱們在滑動textField時timer纔會運行
// [[NSRunLoop mainRunLoop] addTimer:timer forMode:UITrackingRunLoopMode];
// 3. 那個如何讓timer在兩個模式下均可以運行呢?
// 3.1 在兩個模式下都添加timer 是能夠的,可是timer添加了兩次,並非同一個timer
// 3.2 使用站位的運行模式 NSRunLoopCommonModes標記,凡是被打上NSRunLoopCommonModes標記的均可以運行,下面兩種模式被打上標籤
//0 : <CFString 0x10b7fe210 [0x10a8c7a40]>{contents = "UITrackingRunLoopMode"}
//2 : <CFString 0x10a8e85e0 [0x10a8c7a40]>{contents = "kCFRunLoopDefaultMode"}
// 所以也就是說若是咱們使用NSRunLoopCommonModes,timer能夠在UITrackingRunLoopMode,kCFRunLoopDefaultMode兩種模式下運行
[[NSRunLoop mainRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
NSLog(@"%@",[NSRunLoop mainRunLoop]);
}
-(void)show
{
NSLog(@"-------");
}
複製代碼
由上述代碼能夠看出,NSTimer無論用是由於Mode的切換,由於若是咱們在主線程使用定時器,此時RunLoop的Mode爲kCFRunLoopDefaultMode,即定時器屬於kCFRunLoopDefaultMode,那麼此時咱們滑動ScrollView時,RunLoop的Mode會切換到UITrackingRunLoopMode,所以在主線程的定時器就不在管用了,調用的方法也就再也不執行了,當咱們中止滑動時,RunLoop的Mode切換回kCFRunLoopDefaultMode,因此NSTimer就又管用了。
一樣道理的還有ImageView的顯示,咱們直接來看代碼,再也不贅述了
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
NSLog(@"%s",__func__);
// performSelector默認是在default模式下運行,所以在滑動ScrollView時,圖片不會加載
// [self.imageView performSelector:@selector(setImage:) withObject:[UIImage imageNamed:@"abc"] afterDelay:2.0 ];
// inModes: 傳入Mode數組
[self.imageView performSelector:@selector(setImage:) withObject:[UIImage imageNamed:@"abc"] afterDelay:2.0 inModes:@[NSDefaultRunLoopMode,UITrackingRunLoopMode]];
複製代碼
使用GCD也但是建立計時器,並且更爲精確咱們來看一下代碼
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
//建立隊列
dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
//1.建立一個GCD定時器
/*
第一個參數:代表建立的是一個定時器
第四個參數:隊列
*/
dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
// 須要對timer進行強引用,保證其不會被釋放掉,纔會按時調用block塊
// 局部變量,讓指針強引用
self.timer = timer;
//2.設置定時器的開始時間,間隔時間,精準度
/*
第1個參數:要給哪一個定時器設置
第2個參數:開始時間
第3個參數:間隔時間
第4個參數:精準度 通常爲0 在容許範圍內增長偏差可提升程序的性能
GCD的單位是納秒 因此要*NSEC_PER_SEC
*/
dispatch_source_set_timer(timer, DISPATCH_TIME_NOW, 2.0 * NSEC_PER_SEC, 0 * NSEC_PER_SEC);
//3.設置定時器要執行的事情
dispatch_source_set_event_handler(timer, ^{
NSLog(@"---%@--",[NSThread currentThread]);
});
// 啓動
dispatch_resume(timer);
}
複製代碼
Source0:非基於Port的 用於用戶主動觸發的事件(點擊button 或點擊屏幕)
Source1:基於Port的 經過內核和其餘線程相互發送消息(與內核相關)
觸摸事件及PerformSelectors會觸發Source0事件源在前文已經驗證過,這裏不在贅述
CFRunLoopObserverRef是觀察者,可以監聽RunLoop的狀態改變
咱們直接來看代碼,給RunLoop添加監聽者,監聽其運行狀態
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
//建立監聽者
/*
第一個參數 CFAllocatorRef allocator:分配存儲空間 CFAllocatorGetDefault()默認分配
第二個參數 CFOptionFlags activities:要監聽的狀態 kCFRunLoopAllActivities 監聽全部狀態
第三個參數 Boolean repeats:YES:持續監聽 NO:不持續
第四個參數 CFIndex order:優先級,通常填0便可
第五個參數 :回調 兩個參數observer:監聽者 activity:監聽的事件
*/
/*
全部事件
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
};
*/
CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(CFAllocatorGetDefault(), kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
switch (activity) {
case kCFRunLoopEntry:
NSLog(@"RunLoop進入");
break;
case kCFRunLoopBeforeTimers:
NSLog(@"RunLoop要處理Timers了");
break;
case kCFRunLoopBeforeSources:
NSLog(@"RunLoop要處理Sources了");
break;
case kCFRunLoopBeforeWaiting:
NSLog(@"RunLoop要休息了");
break;
case kCFRunLoopAfterWaiting:
NSLog(@"RunLoop醒來了");
break;
case kCFRunLoopExit:
NSLog(@"RunLoop退出了");
break;
default:
break;
}
});
// 給RunLoop添加監聽者
/*
第一個參數 CFRunLoopRef rl:要監聽哪一個RunLoop,這裏監聽的是主線程的RunLoop
第二個參數 CFRunLoopObserverRef observer 監聽者
第三個參數 CFStringRef mode 要監聽RunLoop在哪一種運行模式下的狀態
*/
CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopDefaultMode);
/*
CF的內存管理(Core Foundation)
凡是帶有Create、Copy、Retain等字眼的函數,建立出來的對象,都須要在最後作一次release
GCD原本在iOS6.0以前也是須要咱們釋放的,6.0以後GCD已經歸入到了ARC中,因此咱們不須要管了
*/
CFRelease(observer);
}
複製代碼
咱們來看一下輸出
這時咱們再來分析RunLoop的處理邏輯,就會簡單明瞭不少,如今回頭看官方文檔RunLoop的處理邏輯,對RunLoop的處理邏輯有新的認識。
下面源碼僅保留了主流程代碼
// 共外部調用的公開的CFRunLoopRun方法,其內部會調用CFRunLoopRunSpecific
void CFRunLoopRun(void) { /* DOES CALLOUT */
int32_t result;
do {
result = CFRunLoopRunSpecific(CFRunLoopGetCurrent(), kCFRunLoopDefaultMode, 1.0e10, false);
CHECK_FOR_FORK();
} while (kCFRunLoopRunStopped != result && kCFRunLoopRunFinished != result);
}
// 通過精簡的 CFRunLoopRunSpecific 函數代碼,其內部會調用__CFRunLoopRun函數
SInt32 CFRunLoopRunSpecific(CFRunLoopRef rl, CFStringRef modeName, CFTimeInterval seconds, Boolean returnAfterSourceHandled) { /* DOES CALLOUT */
// 通知Observers : 進入Loop
// __CFRunLoopDoObservers內部會調用 __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__
函數
if (currentMode->_observerMask & kCFRunLoopEntry ) __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopEntry);
// 核心的Loop邏輯
result = __CFRunLoopRun(rl, currentMode, seconds, returnAfterSourceHandled, previousMode);
// 通知Observers : 退出Loop
if (currentMode->_observerMask & kCFRunLoopExit ) __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopExit);
return result;
}
// 精簡後的 __CFRunLoopRun函數,保留了主要代碼
static int32_t __CFRunLoopRun(CFRunLoopRef rl, CFRunLoopModeRef rlm, CFTimeInterval seconds, Boolean stopAfterHandle, CFRunLoopModeRef previousMode) {
int32_t retVal = 0;
do {
// 通知Observers:即將處理Timers
__CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeTimers);
// 通知Observers:即將處理Sources
__CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeSources);
// 處理Blocks
__CFRunLoopDoBlocks(rl, rlm);
// 處理Sources0
if (__CFRunLoopDoSources0(rl, rlm, stopAfterHandle)) {
// 處理Blocks
__CFRunLoopDoBlocks(rl, rlm);
}
// 若是有Sources1,就跳轉到handle_msg標記處
if (__CFRunLoopServiceMachPort(dispatchPort, &msg, sizeof(msg_buffer), &livePort, 0, &voucherState, NULL)) {
goto handle_msg;
}
// 通知Observers:即將休眠
__CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeWaiting);
// 進入休眠,等待其餘消息喚醒
__CFRunLoopSetSleeping(rl);
__CFPortSetInsert(dispatchPort, waitSet);
do {
__CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort, poll ? 0 : TIMEOUT_INFINITY, &voucherState, &voucherCopy);
} while (1);
// 醒來
__CFPortSetRemove(dispatchPort, waitSet);
__CFRunLoopUnsetSleeping(rl);
// 通知Observers:已經喚醒
__CFRunLoopDoObservers(rl, rlm, kCFRunLoopAfterWaiting);
handle_msg: // 看看是誰喚醒了RunLoop,進行相應的處理
if (被Timer喚醒的) {
// 處理Timer
__CFRunLoopDoTimers(rl, rlm, mach_absolute_time());
}
else if (被GCD喚醒的) {
__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(msg);
} else { // 被Sources1喚醒的
__CFRunLoopDoSource1(rl, rlm, rls, msg, msg->msgh_size, &reply);
}
// 執行Blocks
__CFRunLoopDoBlocks(rl, rlm);
// 根據以前的執行結果,來決定怎麼作,爲retVal賦相應的值
if (sourceHandledThisLoop && stopAfterHandle) {
retVal = kCFRunLoopRunHandledSource;
} else if (timeout_context->termTSR < mach_absolute_time()) {
retVal = kCFRunLoopRunTimedOut;
} else if (__CFRunLoopIsStopped(rl)) {
__CFRunLoopUnsetStopped(rl);
retVal = kCFRunLoopRunStopped;
} else if (rlm->_stopped) {
rlm->_stopped = false;
retVal = kCFRunLoopRunStopped;
} else if (__CFRunLoopModeIsEmpty(rl, rlm, previousMode)) {
retVal = kCFRunLoopRunFinished;
}
} while (0 == retVal);
return retVal;
}
複製代碼
上述源代碼中,相應處理事件函數內部還會調用更底層的函數,內部調用纔是真正處理事件的函數,經過上面bt打印所有堆棧信息也能夠獲得驗證。
__CFRunLoopDoObservers 內部調用 __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__
__CFRunLoopDoBlocks 內部調用 __CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__
__CFRunLoopDoSources0 內部調用 __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__
__CFRunLoopDoTimers 內部調用 __CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__
GCD 調用 __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__
__CFRunLoopDoSource1 內部調用 __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION__
此時咱們按照源碼從新整理一下RunLoop處理邏輯就會很清晰
[NSRunLoop currentRunLoop]runUntilDate:<#(nonnull NSDate *)#>
[NSRunLoop currentRunLoop]runMode:<#(nonnull NSString *)#> beforeDate:<#(nonnull NSDate *)#>
複製代碼
常駐線程的做用:咱們知道,當子線程中的任務執行完畢以後就被銷燬了,那麼若是咱們須要開啓一個子線程,在程序運行過程當中永遠都存在,那麼咱們就會面臨一個問題,如何讓子線程永遠活着,這時就要用到常駐線程:給子線程開啓一個RunLoop 注意:子線程執行完操做以後就會當即釋放,即便咱們使用強引用引用子線程使子線程不被釋放,也不能給子線程再次添加操做,或者再次開啓。 子線程開啓RunLoop的代碼,先點擊屏幕開啓子線程並開啓子線程RunLoop,而後點擊button。
#import "ViewController.h"
@interface ViewController ()
@property(nonatomic,strong)NSThread *thread;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
}
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
// 建立子線程並開啓
NSThread *thread = [[NSThread alloc]initWithTarget:self selector:@selector(show) object:nil];
self.thread = thread;
[thread start];
}
-(void)show
{
// 注意:打印方法必定要在RunLoop建立開始運行以前,若是在RunLoop跑起來以後打印,RunLoop先運行起來,已經在跑圈了就出不來了,進入死循環也就沒法執行後面的操做了。
// 可是此時點擊Button仍是有操做的,由於Button是在RunLoop跑起來以後加入到子線程的,當Button加入到子線程RunLoop就會跑起來
NSLog(@"%s",__func__);
// 1.建立子線程相關的RunLoop,在子線程中建立便可,而且RunLoop中要至少有一個Timer 或 一個Source 保證RunLoop不會由於空轉而退出,所以在建立的時候直接加入
// 添加Source [NSMachPort port] 添加一個端口
[[NSRunLoop currentRunLoop] addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
// 添加一個Timer
NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:2.0 target:self selector:@selector(test) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
//建立監聽者
CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(CFAllocatorGetDefault(), kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
switch (activity) {
case kCFRunLoopEntry:
NSLog(@"RunLoop進入");
break;
case kCFRunLoopBeforeTimers:
NSLog(@"RunLoop要處理Timers了");
break;
case kCFRunLoopBeforeSources:
NSLog(@"RunLoop要處理Sources了");
break;
case kCFRunLoopBeforeWaiting:
NSLog(@"RunLoop要休息了");
break;
case kCFRunLoopAfterWaiting:
NSLog(@"RunLoop醒來了");
break;
case kCFRunLoopExit:
NSLog(@"RunLoop退出了");
break;
default:
break;
}
});
// 給RunLoop添加監聽者
CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopDefaultMode);
// 2.子線程須要開啓RunLoop
[[NSRunLoop currentRunLoop]run];
CFRelease(observer);
}
- (IBAction)btnClick:(id)sender {
[self performSelector:@selector(test) onThread:self.thread withObject:nil waitUntilDone:NO];
}
-(void)test
{
NSLog(@"%@",[NSThread currentThread]);
}
@end
複製代碼
注意:建立子線程相關的RunLoop,在子線程中建立便可,而且RunLoop中要至少有一個Timer 或 一個Source 保證RunLoop不會由於空轉而退出,所以在建立的時候直接加入,若是沒有加入Timer或者Source,或者只加入一個監聽者,運行程序會崩潰
Timer和Source也是一些變量,須要佔用一部分存儲空間,因此要釋放掉,若是不釋放掉,就會一直積累,佔用的內存也就愈來愈大,這顯然不是咱們想要的。 那麼何時釋放,怎麼釋放呢? RunLoop內部有一個自動釋放池,當RunLoop開啓時,就會自動建立一個自動釋放池,當RunLoop在休息以前會釋放掉自動釋放池的東西,而後從新建立一個新的空的自動釋放池,當RunLoop被喚醒從新開始跑圈時,Timer,Source等新的事件就會放到新的自動釋放池中,當RunLoop退出的時候也會被釋放。 注意:只有主線程的RunLoop會默認啓動。也就意味着會自動建立自動釋放池,子線程須要在線程調度方法中手動添加自動釋放池。
@autorelease{
// 執行代碼
}
複製代碼
NSTimer、ImageView顯示、PerformSelector等在上面已經有過例子,這裏再也不贅述。
文章開頭的面試題,在文中均可以找到答案,這裏不在贅述了。
文中若是有不對的地方歡迎指出。我是xx_cc,一隻長大好久但尚未二夠的傢伙。須要視頻一塊兒探討學習的coder能夠加我Q:2336684744