什麼是Runloop
?
Runloop
是通過內部維護的事件循環來對事件和消息進行管理的一種機制。當沒有消息需要處理的時候,線程進入休眠以避免佔用資源,有消息需要處理時,立即被喚醒。
runloop
循環不是單獨的do-while
循環,而是發生一個用戶態到內核態切換,以及內核態到用戶態切換。它維護的事件循環可以用來不斷的處理消息和事件,當沒有消息和事件需要處理時會從用戶態切換到內核態,由此可以用來休眠線程,避免資源佔用。當有消息需要處理時會從內核態切換到用戶態,當前線程會被喚醒,所以狀態切換纔是runloop
的關鍵。
iOS
中提供了兩套Runloop
接口,一個是NSRunLoop
基於Objective-C
,在Foundation
框架中,另一個是CFRunLoopRef
基於C
,在CoreFoundation
中。而NSRunLoop
是對CFRunLoopRef
的封裝,兩者接口基本都是對應的。CFRunLoopRef runloop = [nsrunloop getCFRunLoop]
可以獲取對應的CFRunLoopRef
。通過一個表格來對比一下:
特徵 | NSRunLoop | CFRunLoopRef |
---|---|---|
所屬框架 | Objective-C/Foundation | C/CoreFoundation |
獲取Runloop | [NSRunLoop currentRunLoop] [NSRunLoop mainRunLoop] |
CFRunLoopGetCurrent() CFRunLoopGetMain() |
Source事件 | addPort:forMode: removePort:forMode: |
CFRunLoopAddSource(...) CFRunLoopRemoveSource(...) |
Timer事件 | addTimer:forMode: | CFRunLoopAddTimer(...) |
Observer事件 | CFRunLoopAddObserver(...) CFRunLoopRemoveObserver(...) |
|
run | run runUntilDate: runMode:beforeDate: |
CFRunLoopRun() CFRunLoopRunInMode(...) CFRunLoopRunSpecific(...) |
1. __CFRunLoop相關數據結構
struct __CFRunLoop {
...
pthread_t _pthread;//對應的線程
CFMutableSetRef _commonModes;
CFMutableSetRef _commonModeItems;
CFRunLoopModeRef _currentMode;
CFMutableSetRef _modes;
...
};
__CFRunLoop
是runloop
本身:typedef struct __CFRunLoop *CFRunLoopRef
。__CFRunLoop
對應多個__CFRunLoopMode
。
struct __CFRunLoopMode {
...
CFStringRef _name;
CFMutableSetRef _sources0;//
CFMutableSetRef _sources1;
CFMutableArrayRef _observers;
CFMutableArrayRef _timers;
...
};
__CFRunLoopMode
是runloop的運行模式:typedef struct __CFRunLoopMode *CFRunLoopModeRef
。每一個__CFRunLoopMode
又包含多個_sources0、_sources1、_observers、_timers事件。_sources0:非基於Port的,也就是用戶主動發出的事件。_sources1:基於Port的,也就是系統內部的消息事件。_observers:觀察者。
_timers:定時器事件。
系統默認註冊了5中類型的Mode
系統註冊的mode | 說明 |
---|---|
kCFRunLoopDefaultMode | App的默認Mode,通常主線程是在這個Mode下運行 |
UITrackingRunLoopMode | 界面跟蹤 Mode,用於 ScrollView 追蹤觸摸滑動,保證界面滑動時不受其他 Mode 影響 |
UIInitializationRunLoopMode | 在剛啓動 App 時第進入的第一個 Mode,啓動完成後就不再使用 |
GSEventReceiveRunLoopMode | 接受系統事件的內部 Mode,通常用不到 |
kCFRunLoopCommonModes | 這是一個佔位用的Mode,不是一種真正的Mode |
主線程默認運行在kCFRunLoopDefaultMode
下,滑動scrollView
,就變成了UITrackingRunLoopMode
,手指離開又變成了kCFRunLoopDefaultMode
。
相關的類的成員變量與關係:
如上圖時Runloop中用到的基礎結構,再對應關係方面:一個__CFRunLoop實例可以包含多個__CFRunLoopMode;一個__CFRunLoopMode又包含多個CFRunLoopSourceRef、CFRunLoopObserverRef、CFRunLoopTimerRef事件。一個Runloop要想跑起來,內部必須要有一個Mode,並且這個Mode裏邊必須包含一個Source/Observer/Timer事件。
CFRunLoop的狀態:
名稱 | 說明 |
---|---|
kCFRunLoopEntry | 即將進入runloop |
kCFRunLoopBeforeTimers | 即將處理timer事件 |
kCFRunLoopBeforeSources | 即將處理source事件 |
kCFRunLoopBeforeWaiting | 即將進入睡眠 |
kCFRunLoopAfterWaiting | 被喚醒 |
kCFRunLoopExit | runloop退出 |
2._CFRunLoop的創建、運行、退出
-創建
[NSRunLoop mainRunLoop]
對應底層的CFRunLoopGetMain()
,[NSRunLoop currentRunLoop]
對應底層的CFRunLoopGetCurrent()
,內部都是通過CF_EXPORT CFRunLoopRef _CFRunLoopGet0(pthread_t t)
獲取的runloop
,分別傳入pthread_main_thread_np()
和pthread_self()
也就是主線程和當前線程的id。
CF_EXPORT CFRunLoopRef _CFRunLoopGet0(pthread_t t) {
if (pthread_equal(t, kNilPthreadT)) {
//如果外部傳入無效的0,則將主線程ID賦值給t。
t = pthread_main_thread_np();
}
__CFLock(&loopsLock);
if (!__CFRunLoops) {
//如果__CFRunLoops爲空,則創建主線程對應的runloop
__CFUnlock(&loopsLock);
CFMutableDictionaryRef dict = CFDictionaryCreateMutable(kCFAllocatorSystemDefault, 0, NULL, &kCFTypeDictionaryValueCallBacks);
//__CFRunLoopCreate做線程的初始化
CFRunLoopRef mainLoop = __CFRunLoopCreate(pthread_main_thread_np());
// 將mainLoop保存到dict中,以線程id爲key,mainLoop爲value
CFDictionarySetValue(dict, pthreadPointer(pthread_main_thread_np()), mainLoop);
//將dict中的內容複製到__CFRunLoops地址上
if (!OSAtomicCompareAndSwapPtrBarrier(NULL, dict, (void * volatile *)&__CFRunLoops)) {
CFRelease(dict);
}
CFRelease(mainLoop);
__CFLock(&loopsLock);
}
//通過線程id獲取runloop
CFRunLoopRef loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
__CFUnlock(&loopsLock);
if (!loop) {
//如果獲取到的爲空,則直接創建
CFRunLoopRef newLoop = __CFRunLoopCreate(t);
__CFLock(&loopsLock);
loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
if (!loop) {
//如果創建後還不能獲取到則使用剛纔創建的,並將newLoop保存到__CFRunLoops中
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;
}
在__CFRunLoopCreate
中通過_CFRuntimeCreateInstance
實例華再進行其他變量的一些初始化。其中loop->_pthread = t;
將線程id
綁定到了runloop
上。runloop
通過線程id
去查找,如果沒有則進行創建並將線程id
綁定到runloop
上,通過這個規則我們知道:線程與runloop
一一對應;線程不一定都有runloop
,首個runloop
創建時會檢查__CFRunLoops
是否爲空,爲空則先創建主線程的runloop
,再創建指定線程的runloop
。
-運行
啓動Runloop
,調用CFRunLoopRun()
即可,Runloop
進入運行循環,運行狀態只要不是kCFRunLoopRunStopped
和kCFRunLoopRunFinished
就會一直運行下去不退出。
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的運行狀態切換到指定的Mode
//代碼較長,只列出重要步驟的
SInt32 CFRunLoopRunSpecific(CFRunLoopRef rl, CFStringRef modeName, CFTimeInterval seconds, Boolean returnAfterSourceHandled) {
...
CFRunLoopModeRef currentMode = __CFRunLoopFindMode(rl, modeName, false);
//如果沒有獲取到mode或者mode中的事件爲空(無sources0/sources1等)返回kCFRunLoopRunFinished
if (NULL == currentMode || __CFRunLoopModeIsEmpty(rl, currentMode, rl->_currentMode)) {
return kCFRunLoopRunFinished;
}
//保存previousPerRun、previousMode
volatile _per_run_data *previousPerRun = __CFRunLoopPushPerRunData(rl);
CFRunLoopModeRef previousMode = rl->_currentMode;
rl->_currentMode = currentMode;
int32_t result = kCFRunLoopRunFinished;
//通知Observer即將進入循環
if (currentMode->_observerMask & kCFRunLoopEntry ) __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopEntry);
result = __CFRunLoopRun(rl, currentMode, seconds, returnAfterSourceHandled, previousMode);
//通知Observer即將退出循環
if (currentMode->_observerMask & kCFRunLoopExit ) __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopExit);
//恢復previousPerRun,previousMode
__CFRunLoopPopPerRunData(rl, previousPerRun);
rl->_currentMode = previousMode;
return result;
}
3.Runloop的使用
3.1.main
函數爲何能保持不退出?
在main
函數中,會調用UIApplicationMain
函數,在內部會啓動主線程的Runloop
,可以不斷的接收消息,比如點擊屏幕事件,滑動列表以及處理網絡請求的返回等接收消息後對事件進行處理,處理完之後,就會繼續等待。
3.2.NSTimer相關案例
案例1:
NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(run) userInfo:nil repeats:YES];
我們主線程執行如下代碼,我們Timer能夠正常運行,但是如果我們在進行scrollView滑動的時候定時器會停止,這是什麼原因呢?在新開子線程調用它運行不起來,這是什麼原因呢?
這裏我們需要明白:1.scheduledTimerWithTimeInterval:
方法會自動把當前初始化的Timer
加入到currentRunLoop
的kCFRunLoopDefaultMode
模式下,主線程的runloop
已經在run
狀態了,所以定時器會立即啓動。如果手動滑動scrollView
,則主線程的runloop
的狀態切換爲UITrackingRunLoopMode
模式了,添加在kCFRunLoopDefaultMode
模式的Timer
自然就沒有回調了。解決辦法:將Timer
手動添加到UITrackingRunLoopMode
模式或者kCFRunLoopCommonModes
模式即可。
如果在新開的子線程執行上面的代碼,由於新開的子線程並不會主動創建runloop
,所以定時器自然運行不起來。解決辦法:手動將Timer
加入到currentRunLoop
的kCFRunLoopCommonModes
模式,並且執行run
方法。
3.3監聽runloop的運行狀態
-(void)addObserver{
/*1.創建監聽者
第一個參數:怎麼分配存儲空間
第二個參數:要監聽的狀態 。kCFRunLoopAllActivities表示所有的狀態
第三個參數:是否持續監聽
第四個參數:優先級 總是傳0
第五個參數:當狀態改變時候的回調
*/
CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(CFAllocatorGetDefault(), kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
switch (activity) {
case kCFRunLoopEntry:
NSLog(@"即將進入runloop"); break;
case kCFRunLoopBeforeTimers:
NSLog(@"即將處理timer事件"); break;
case kCFRunLoopBeforeSources:
NSLog(@"即將處理source事件");break;
case kCFRunLoopBeforeWaiting:
NSLog(@"即將進入睡眠"); break;
case kCFRunLoopAfterWaiting:
NSLog(@"被喚醒"); break;
case kCFRunLoopExit:
NSLog(@"runloop退出");break;
default:
break;
}
});
/*2.添加監聽者
第一個參數:要監聽哪個runloop
第二個參數:觀察者
第三個參數:運行模式
*/
CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopDefaultMode);
}
3.4常駐線程
一個線程要想跑起來,則需要至少一個mode,一個事件。所以我們可以使用Timer或者Source事件。
第一步創建一個新的線程
- (void)createThread {
self.thread = [[NSThread alloc]initWithTarget:self selector:@selector(task1) object:nil];
[self.thread start];
}
第二步在新開的線程添加Timer或者Source事件。
- (void)task1{
[[NSRunLoop currentRunLoop] addPort:[NSPort port] forMode:NSDefaultRunLoopMode];
[[NSRunLoop currentRunLoop] run];
}
第三部測試線程是否正常運行。可以通過點擊等連續觸發,也可以在主線程多次調用test來測試
- (void)test {
[self performSelector:@selector(task2) onThread:self.thread withObject:nil waitUntilDone:YES];
}
-(void)task2{
NSLog(@"task2---%@",[NSThread currentThread]);
}