RunLoop應(yīng)用
這張圖是蘋果官網(wǎng)中圖烹俗,接下來通過示例理解這種圖
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
// 循環(huán)引用
[NSTimer scheduledTimerWithTimeInterval:1 repeats:YES block:^(NSTimer * _Nonnull timer) {
// __CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__
NSLog(@"1111");
}];
//
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(gotNotification:) name:@"MyNotification" object:nil];
[self performSelector:@selector(fire) withObject:nil afterDelay:1.0];
////
dispatch_async(dispatch_get_main_queue(), ^{
//__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__
NSLog(@"hello word");
});
//
void (^block)(void) = ^{
//__CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__
NSLog(@"123");
};
block();
}
- (void)fire {
//__CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__
NSLog(@"performSeletor");
}
- (void)gotNotification:(NSNotification *)noti {
NSLog(@"gotNotification = %@",noti);
}
#pragma mark - 觸摸事件
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
NSLog(@"來了,老弟!!!");
// __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__
[[NSNotificationCenter defaultCenter] postNotificationName:@"MyNotification" object:@"ssl"];
}
@end
首先測試下NStimer
躏吊,斷點bt下
- 這里timer收到runloop影響
- 這里有個
__CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__
測試下block
- 這里block收到runloop影響
- 這里有個
__CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__
這里就不一一進行測試了,給出其余幾種情況
- timer:
__CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__
- GCD主隊列:
__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__
- source0:
__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__
- source1:
__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION__
- observer :
__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__
- block:
__CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__
RunLoop作用
- 保持程序的持續(xù)運行
- 處理App的各種時間(觸摸、定時器等)
- 節(jié)省
CPU
資源、提供程序的性能:做事的時候做事、休息的時候休息
RunLoop與線程關(guān)系
在CFRunLoop
源碼中找到CFRunLoopGetMain
和CFRunLoopGetCurrent
方法
進入_CFRunLoopGet0
方法
CF_EXPORT CFRunLoopRef _CFRunLoopGet0(pthread_t t) {
if (pthread_equal(t, kNilPthreadT)) {
t = pthread_main_thread_np();
}
__CFLock(&loopsLock);
if (!__CFRunLoops) {
__CFUnlock(&loopsLock);
// 創(chuàng)建一個可變字典
CFMutableDictionaryRef dict = CFDictionaryCreateMutable(kCFAllocatorSystemDefault, 0, NULL, &kCFTypeDictionaryValueCallBacks);
// 創(chuàng)建主線程runloop
CFRunLoopRef mainLoop = __CFRunLoopCreate(pthread_main_thread_np());
// 進行綁定 dict[@"pthread_main_thread_np"] = mainLoop
CFDictionarySetValue(dict, pthreadPointer(pthread_main_thread_np()), mainLoop);
if (!OSAtomicCompareAndSwapPtrBarrier(NULL, dict, (void * volatile *)&__CFRunLoops)) {
CFRelease(dict);
}
CFRelease(mainLoop);
__CFLock(&loopsLock);
}
// 根據(jù)當(dāng)前線程獲取loop
CFRunLoopRef loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
__CFUnlock(&loopsLock);
// 如果不是主線程腋舌,進行類似操作將線程和創(chuàng)建的CFRunLoopRef進行綁定
if (!loop) {
CFRunLoopRef newLoop = __CFRunLoopCreate(t);
__CFLock(&loopsLock);
loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
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;
}
- 如果
__CFRunLoops
不存在,創(chuàng)建CFMutableDistionaryRef
,并默認初始化主線程的RunLoop
并將RunLoop
放入到字典中,線程為key渗蟹,runloop為value
- 在通過線程獲取
RunLoop
時块饺,以key-value方式從字典中獲取對應(yīng)的RunLoop; - 如果
RunLoop
為空,則創(chuàng)建一個newLoop
雌芽,以線程為key授艰,RunLoop為value,存儲到__CFRunLoops中
【線程總結(jié)】線程的RunLoop會默認被創(chuàng)建世落,而子線程的RunLoop是懶加載的淮腾,需要時才會創(chuàng)建,RunLoop和線程是一對一的關(guān)系,存儲在一個字典中谷朝。
- 子線程
runLoop
分析
dispatch_queue_t queue = dispatch_queue_create("", NULL);
dispatch_async(queue, ^{
NSLog(@"running....");
[NSTimer scheduledTimerWithTimeInterval:1.0 repeats:YES block:^(NSTimer * _Nonnull timer) {
NSLog(@"helloc timer...%@", [NSThread currentThread]);
}];
});
由上圖可知洲押,這里只打印了
定時器前的打印
,定時任務(wù)并沒有執(zhí)行圆凰,這是因為NSTimer需要依賴于RunLoop杈帐,主線程的RunLoop默認開啟,而子線程的RunLoop是懶加載专钉,需要手動開啟挑童。RunLoop數(shù)據(jù)結(jié)構(gòu)
創(chuàng)建RunLoop是使用的__CFRunLoopCreate函數(shù),查看函數(shù)實現(xiàn):
CFRunLoopRef
是結(jié)構(gòu)體指針跃须,查看__CFRunLoop
結(jié)構(gòu)體:
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;
};
由這個結(jié)構(gòu)體站叼,可以得知一個runLoop
對應(yīng)_commonModes
,這個_commonModes
是個集合類型,所以一個runLoop對應(yīng)多個mode
mode定義
struct __CFRunLoopMode {
CFStringRef _name;
CFMutableSetRef _sources0;
CFMutableSetRef _sources1;
CFMutableArrayRef _observers;
CFMutableArrayRef _timers;
...
};
- CFRunLoopModeRef代表RunLoop的運行模式菇民;
- 一個RunLoop包含若干個 Mode尽楔,每個 Mode 又包含若干個Source0/Source1/Timer/Observer;
- RunLoop啟動時只能選擇其中一個 Mode第练,作為 currentMode翔试;
如果需要切換 Mode,只能退出當(dāng)前 Loop复旬,再重新選擇一個 Mode 進入,切換模式不會導(dǎo)致程序退出冲泥; - 不同 Mode 中的
Source0/Source1/Timer/Observer
能分隔開來驹碍,互不影響; - 如果 Mode 里沒有任何
Source0/Source1/Timer/Observer
凡恍,RunLoop會立馬退出志秃。
__CFRunLoopMode
源碼定義中包括了4個set集合_sources0、_sources1嚼酝、_observers浮还、_timers
,這四個集合也就是我們常說的事件(事務(wù))闽巩。所以我們可以得出結(jié)論:CFRunLoopMode和sourses钧舌、timer、observer也是一對多的關(guān)系涎跨。
這里的timer洼冻、observer
比較好理解,什么是_sources0隅很、_sources1
呢撞牢?
mode類型
-kCFRunLoopDefaultMode
默認的運行模式,通常主線程是在這個Mode下運行
-UITrackingRunLoopMode
界面跟蹤Mode,用于ScrollView等視圖屋彪,追蹤觸摸滑動所宰,保證界面的滑動不受其他Mode的影響
-UIInitializationRunLoopMode
在剛啟動App時進入的第一個Mode,啟動完成后就不在使用
-GSEventReceiveRunLoopMode
接受系統(tǒng)時間的內(nèi)部Mode畜挥,通常用不到
-kCFRunLoopCommonModes
是一個偽模式仔粥,可以在標記為CommonModes的模式下運行,RunLoop會自動將_commonModeItems里的source砰嘁、observe件炉、timer同步到具有標記的Mode里。
runLoop原理
在源碼中查找CFRunLoopRunSpecific
的方法實現(xiàn)矮湘,見下圖:
由上圖可知:這個函數(shù)是對runLoop的生命周期的處理,進入
__CFRunLoopRun
/**
* 運行 run loop
*
* @param rl 運行的RunLoop對象
* @param rlm 運行的mode
* @param seconds run loop超時時間
* @param stopAfterHandle true:run loop處理完事件就退出 false:一直運行直到超時或者被手動終止
* @param previousMode 上一次運行的mode
*
* @return 返回4種狀態(tài)
*/
static int32_t __CFRunLoopRun(CFRunLoopRef rl, CFRunLoopModeRef rlm, CFTimeInterval seconds, Boolean stopAfterHandle, CFRunLoopModeRef previousMode) {
int32_t retVal = 0;
do { // itmes do
/// 2. 通知 Observers: RunLoop 即將觸發(fā) Timer 回調(diào)斟冕。
__CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeTimers);
/// 3. 通知 Observers: RunLoop 即將觸發(fā) Source0 (非port) 回調(diào)。
__CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeSources)
/// 執(zhí)行被加入的block
__CFRunLoopDoBlocks(rl, rlm);
/// 4. RunLoop 觸發(fā) Source0 (非port) 回調(diào)缅阳。
Boolean sourceHandledThisLoop = __CFRunLoopDoSources0(rl, rlm, stopAfterHandle);
/// 處理sources0返回為YES
if (sourceHandledThisLoop) {
/// 執(zhí)行被加入的block
__CFRunLoopDoBlocks(rl, rlm);
}
/// 5. 如果有 Source1 (基于port) 處于 ready 狀態(tài)磕蛇,直接處理這個 Source1 然后跳轉(zhuǎn)去處理消息。
if (__CFRunLoopServiceMachPort(dispatchPort, &msg, sizeof(msg_buffer), &livePort, 0, &voucherState, NULL)) {
/// 如果接收到了消息的話十办,前往第9步開始處理消息
goto handle_msg;
}
/// 6.通知 Observers: RunLoop 的線程即將進入休眠(sleep)秀撇。
__CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeWaiting);
__CFRunLoopSetSleeping(rl);
/// 7. 接收waitSet端口的消息
__CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort, poll ? 0 : TIMEOUT_INFINITY, &voucherState, &voucherCopy);
/// 7. 等待接受 mach_port 的消息。線程將進入休眠, 直到被下面某一個事件喚醒向族。
/// ? 一個基于 port 的Source 的事件呵燕。
/// ? 一個 Timer 到時間了
/// ? RunLoop 自身的超時時間到了
/// ? 被其他什么調(diào)用者手動喚醒
// 取消runloop的休眠狀態(tài)
__CFRunLoopUnsetSleeping(rl);
/// 8. 通知 Observers: RunLoop 的線程剛剛被喚醒了。
__CFRunLoopDoObservers(rl, rlm, kCFRunLoopAfterWaiting);
handle_msg:
if (被Timer喚醒) {
/// 9.1 如果一個 Timer 到時間了件相,觸發(fā)這個Timer的回調(diào)再扭。
__CFRunLoopDoTimers(rl, rlm, mach_absolute_time());
} else if (被GCD喚醒) {
/// 9.2 如果有dispatch到main_queue的block夜矗,執(zhí)行block
__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(msg);
} else if (被Source1喚醒) {
/// 9.3 如果一個 Source1 (基于port) 發(fā)出事件了泛范,處理這個事件
__CFRunLoopDoSource1(rl, rlm, rls, msg, msg->msgh_size, &reply)
}
/// 執(zhí)行加入到Loop的block
__CFRunLoopDoBlocks(rl, rlm);
if (sourceHandledThisLoop && stopAfterHandle) {
/// 進入loop時參數(shù)說處理完事件就返回。
retVal = kCFRunLoopRunHandledSource;
} else if (timeout_context->termTSR < mach_absolute_time()) {
/// 超出傳入?yún)?shù)標記的超時時間了
retVal = kCFRunLoopRunTimedOut;
} else if (__CFRunLoopIsStopped(rl)) {
/// 被外部調(diào)用者強制停止了
__CFRunLoopUnsetStopped(rl);
retVal = kCFRunLoopRunStopped;
} else if (rlm->_stopped) {
/// 自動停止了
rlm->_stopped = false;
retVal = kCFRunLoopRunStopped;
} else if (__CFRunLoopModeIsEmpty(rl, rlm, previousMode)) {
/// source/timer/observer一個都沒有了
retVal = kCFRunLoopRunFinished;
}
/// 如果沒超時紊撕,mode里沒空罢荡,loop也沒被停止,那繼續(xù)loop对扶。
} while (0 == retVal);
return retVal;
}
下圖就是上圖的處理邏輯
【總結(jié)】
RunLoop是通過系統(tǒng)內(nèi)部維護的循環(huán)進行事件区赵、消息管理的一個對象。RunLoop
實際上就是一個do...while
循環(huán)浪南,有任務(wù)時開始惧笛,無任務(wù)時休眠。本質(zhì)是通過mach_msg()
函數(shù)接收逞泄、發(fā)送消息患整。
CFRunLoopModeRef 這樣設(shè)計有什么好處拜效?Runloop為什么會有多個 Mode?
-Mode 做到了屏蔽的效果各谚,當(dāng)RunLoop運行在 Mode1 下面的時候紧憾,是處理不了 Mode2 的事件的;
比如NSDefaultRunLoopMode默認模式和UITrackingRunLoopMode滾動模式昌渤,滾動屏幕的時候就會切換到滾動模式赴穗,就不用去處理默認模式下的事件了,保證了 UITableView等的滾動順暢膀息。
RunLoop與線程的關(guān)系:
- 每個線程都有一個與之對應(yīng)的RunLoop般眉,所以RunLoop與線程是一一對應(yīng)的,其綁定關(guān)系通過一個全局的DIctionary存儲潜支,線程為key甸赃,runloop為value。
- 線程中的RunLoop主要是用來管理線程的冗酿,當(dāng)線程的RunLoop開啟后埠对,會在執(zhí)行完任務(wù)后進行休眠狀態(tài),當(dāng)有事件觸發(fā)喚醒時裁替,又開始工作项玛,即有活時干活,沒活就休息
- 主線程的RunLoop是默認開啟的弱判,在程序啟動之后襟沮,會一直運行,不會退出
- 其他線程的RunLoop默認是不開啟的昌腰,如果需要臣嚣,則手動開啟
RunLoop中涉及到5個重要的類:
CFRunLoop - RunLoop對象
CFRunLoopMode - 五種運行模式
CFRunLoopSource - 輸入源/事件源,包括Source0和Source1
CFRunLoopTimer - 定時源剥哑,也就是NSTimer
CFRunLoopObserve - 觀察者,用來監(jiān)聽RunLoop
CFRunLoopSource - 事件源
- Source1:基于mach_port和回調(diào)函數(shù)指針淹父,也就是端口通訊株婴,處理來自系統(tǒng)內(nèi)核或其他進程的事件,比如點擊手機屏幕
- Source0:非基于Port的處理事件暑认,也就是應(yīng)用層事件(內(nèi)部事件困介、APP負責(zé)管理的事件,UIEvent)蘸际,包含一個回調(diào)函數(shù)指針座哩,需要手動標記為待處理或者手動喚醒RunLoop,如performSelector粮彤、block等
例如:一個APP在前臺靜止根穷,用戶點擊APP界面姜骡,屏幕表面的時事件會先包裝成Event告訴source1(基于mach_port),source1喚醒RunLoop將事件Event分發(fā)給source0屿良,由source0來處理圈澈。
CFRunLooTimer - 定時源
就是NSTimer,在預(yù)設(shè)的時間點喚醒RunLoop執(zhí)行回調(diào)尘惧。因為它是基于RunLoop的康栈,因此它不是實時的(Timer是不準確的,因為RunLoop只負責(zé)分發(fā)源消息喷橙。如果線程當(dāng)前正在處理繁重的任務(wù)啥么,就有可能導(dǎo)致Timer本次延時,或者少執(zhí)行一次)贰逾。
CFRunLoopObserver - 觀察者
用來監(jiān)聽時間點事件CFRunLoopActivity悬荣。
- KCFRunLoopEntery RunLoop準備啟動
- kCFRunLoopBeforeTimers RunLoop將要處理一些Timer相關(guān)的事件
- kCFRunLoopBeforeSources RunLoop將要處理一些Source事件
- kCFRunLoopBeforeWaiting RunLoop將要進行休眠狀態(tài),即將由用戶狀態(tài)切換內(nèi)核態(tài)
- kCFRunLoopAfterWaiting RunLoop被喚醒似踱,即從內(nèi)核態(tài)切換到用戶態(tài)
- kCFRunLoopExit RunLoop退出
- kCfRunLoopAllActivitires 監(jiān)聽所有狀態(tài)
為什么main函數(shù)能夠保持一直存在且不退出隅熙?
在main函數(shù)內(nèi)容會調(diào)用UIApplication函數(shù),而在UIAPPlicationMain內(nèi)部會啟動主線程的RunLoop核芽,可以做到有消息處理囚戚,能夠迅速從內(nèi)核態(tài)到用戶態(tài)的切換,立刻喚醒處理轧简,而沒有消息處理時驰坊,通過用戶態(tài)到內(nèi)核態(tài)的切換進入等待狀態(tài),避免資源的占用哮独。因此main函數(shù)能夠一直存在并且不退出拳芙。
NSRunLoop 和 CFRunLoopRef 區(qū)別
- NSRunLoop是基于CFRunLoopRef面向?qū)ο蟮腁PI,是不安全的
- CFRunLoopRef是基于C語言皮璧,是線程安全的
Runloop的mode作用是什么舟扎?
mode主要是用于指定RunLoop中事件優(yōu)先級的