一直想寫關(guān)于RunLoop的文章攻泼,但是發(fā)現(xiàn)要完全搞明白實在是太難了揖赴。RunLoop好多設(shè)計到了內(nèi)核⊙┪唬現(xiàn)在只能說一說RunLoop的表層了竭钝。
首先RunLoop是跟線程相關(guān)的”⑾矗可以理解為輔助線程執(zhí)行任務(wù)的工具香罐。
如果沒有RunLoop線程的執(zhí)行順序就是任務(wù)執(zhí)行完就銷毀了。如果有了RunLoop就可以調(diào)用內(nèi)核 可以讓線程掛起 休眠时肿。節(jié)省CPU資源庇茫,下次有任務(wù)的時候再喚醒,節(jié)省線程開辟的消耗螃成。如果你是單一的任務(wù) 那么你就不用關(guān)系RunLoop了旦签。但是如果你是重復 并且是間斷執(zhí)行的時候,就可以使用RunLoop了寸宏。
先說下大概的使用場景
RunLoop應(yīng)用
1.NSTimer CADisplayLink 定時器
//在主線程創(chuàng)建的定時器
NSTimer *timer = [NSTimer timerWithTimeInterval:2 target:self selector:@selector(calculate) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
//當控制器里有scrollView滾動的時候 主線程的runloop會切換到UITrackingRunLoopMode 模式下執(zhí)行runLoop 所以在原來NSDefaultRunLoopMode事件下的定時器就不走了
解決辦法就是
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
//還有如果是在子線程添加的控制器 子線程的runLoop是默認沒有的宁炫。
只有調(diào)用[NSRunLoop currentRunLoop] 系統(tǒng)才會去創(chuàng)建對應(yīng)子線程的runLoop ,只創(chuàng)建了還是沒有用的 還得調(diào)用runLoop的 run發(fā)法讓子線程的runLoop跑起來
這樣加進來的定時器才能夠執(zhí)行
2.performSelector 的使用
大家在調(diào)用performSelector方法的時候 可以點擊option看看蘋果的注釋氮凝。
這個方法是跟RunLoop有關(guān)的
默認是執(zhí)行在當前線程的NSDefaultRunLoopMode 模式下
NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(creatThread) object:nil];
_thread = thread;
[thread start];
- (void)creatThread {
//創(chuàng)建線程的時候最好加上 自動釋放池
@autoreleasepool{
NSPort *port = [[NSPort alloc] init];
//為了保證runLoop不退出 需要有事件源 這個port就是占位的
[[NSRunLoop currentRunLoop] addPort:port forMode:NSDefaultRunLoopMode];
//開啟運行循環(huán) 如果不開啟的話 后期再這個線程是沒法執(zhí)行任務(wù)
[[NSRunLoop currentRunLoop] run];
}
}
[self performSelector:@selector(calculate) onThread:self.thread withObject:nil waitUntilDone:NO];
3.常駐線程
跟第2條一樣 可以常駐線程 用來做一些頻繁的任務(wù) 比如日志的收集上傳 耗時操作的計算 等
運用的經(jīng)典案例 AFNetworking2.0中的
+ (void)networkRequestThreadEntryPoint:(id)__unused object {
@autoreleasepool {
[[NSThread currentThread] setName:@"AFNetworking"];
NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
[runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
[runLoop run];
}
}
+ (NSThread *)networkRequestThread {
static NSThread *_networkRequestThread = nil;
static dispatch_once_t oncePredicate;
dispatch_once(&oncePredicate, ^{
_networkRequestThread = [[NSThread alloc] initWithTarget:self selector:@selector(networkRequestThreadEntryPoint:) object:nil];
[_networkRequestThread start];
});
return _networkRequestThread;
}
- (void)start {
[self.lock lock];
if ([self isCancelled]) {
[self performSelector:@selector(cancelConnection) onThread:[[self class] networkRequestThread] withObject:nil waitUntilDone:NO modes:[self.runLoopModes allObjects]];
} else if ([self isReady]) {
self.state = AFOperationExecutingState;
[self performSelector:@selector(operationDidStart) onThread:[[self class] networkRequestThread] withObject:nil waitUntilDone:NO modes:[self.runLoopModes allObjects]];
}
[self.lock unlock];
}
希望我們以后也可以用到常駐線程
4.autoreleaspool 相關(guān)
自動釋放池跟runloop也有關(guān)系
在第一次創(chuàng)建runloop的時候會創(chuàng)建自動釋放池
在runloop休眠的時候會銷毀上次創(chuàng)建的自動釋放池
在被喚醒的時候會創(chuàng)建新的自動釋放池
已達到無用對象的快速釋放 減少內(nèi)存
說了這么多應(yīng)用 我們看看他它的源碼吧
RunLoop大概構(gòu)造
NSRunLoop 和 CFRunLoopRef是一一對應(yīng)的 只不過是不同框架下的
NSRunLoop是基于CFRunLoopRef的的一層OC封裝羔巢,所以了解RunLoop的內(nèi)部結(jié)構(gòu),就需要研究CFRunLoopRef(core Foundation框架)
RunLoop的相關(guān)類
CFRunLoopRef
CFRunLoopModeRef
CFRunLoopSourceRef
CFRunLoopTimerRef
CFRunLoopObserverRef
CFRunLoopModeRef代表RunLoop的運行模式
每個runLoop跟線程是一對一的
1.一個 RunLoop 包含若干個 Mode,每個Mode又包含若干個Source/Timer/Observer
2.每次RunLoop啟動時朵纷,只能指定其中一個 Mode炭臭,這個Mode被稱作 CurrentMode.
3. 如果需要切換Mode,只能退出Loop袍辞,再重新指定一個Mode進入
4. 這樣做主要是為了分隔開不同組的Source/Timer/Observer,讓其互不影響
CFRunLoopSourceRef 代表事件 Source有兩個版本:Source0 和 Source1常摧。
Source0 只包含了一個回調(diào)(函數(shù)指針)搅吁,它并不能主動觸發(fā)事件。使用時落午,你需要先調(diào)用 CFRunLoopSourceSignal(source)谎懦,將這個 Source 標記為待處理,然后手動調(diào)用 CFRunLoopWakeUp(runloop) 來喚醒 RunLoop溃斋,讓其處理這個事件界拦。
Source1 包含了一個 mach_port 和一個回調(diào)(函數(shù)指針),被用于通過內(nèi)核和其他線程相互發(fā)送消息梗劫。這種 Source 能主動喚醒 RunLoop 的線程
CFRunLoopTimerRef 是基于時間的觸發(fā)器享甸,它和 NSTimer 是toll-free bridged 的,可以混用梳侨。其包含一個時間長度和一個回調(diào)(函數(shù)指針)蛉威。當其加入到 RunLoop 時,RunLoop會注冊對應(yīng)的時間點走哺,當時間點到時蚯嫌,RunLoop會被喚醒以執(zhí)行那個回調(diào)。
CFRunLoopObserverRef 是觀察者丙躏,每個 Observer 都包含了一個回調(diào)(函數(shù)指針)择示,當 RunLoop 的狀態(tài)發(fā)生變化時,觀察者就能通過回調(diào)接受到這個變化晒旅≌っぃ可以觀測的時間點有以下幾個:
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
};
上面的 Source/Timer/Observer 被統(tǒng)稱為 mode item,一個 item 可以被同時加入多個 mode敢朱。但一個 item 被重復加入同一個 mode 時是不會有效果的剪菱。如果一個 mode 中一個 item 都沒有,則 RunLoop 會直接退出拴签,不進入循環(huán)孝常。
RunLoop的Mode
UIInitializationRunLoopMode 在剛啟動 App 時第進入的第一個 Mode,啟動完成后就不再使用蚓哩。
GSEventReceiveRunLoopMode接受系統(tǒng)事件的內(nèi)部 Mode构灸,通常用不到。
kCFRunLoopDefaultModeApp的默認 Mode岸梨,通常主線程是在這個 Mode 下運行的喜颁。
UITrackingRunLoopMode界面跟蹤 Mode稠氮,用于 ScrollView 追蹤觸摸滑動,保證界面滑動時不受其他 Mode 影響半开。
kCFRunLoopCommonModes這是一個占位的 Mode隔披,沒有實際作用
關(guān)系圖如下
蘋果官方文檔
https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/Multithreading/RunLoopManagement/RunLoopManagement.html#//apple_ref/doc/uid/10000057i-CH16-131281
CFRunLoop源碼地址
http://opensource.apple.com/tarballs/CF/
#######接下來我們看下源碼
/// 用DefaultMode啟動
void CFRunLoopRun(void) {
CFRunLoopRunSpecific(CFRunLoopGetCurrent(), kCFRunLoopDefaultMode, 1.0e10, false);
}
/// 用指定的Mode啟動,允許設(shè)置RunLoop超時時間
int CFRunLoopRunInMode(CFStringRef modeName, CFTimeInterval seconds, Boolean stopAfterHandle) {
return CFRunLoopRunSpecific(CFRunLoopGetCurrent(), modeName, seconds, returnAfterSourceHandled);
}
/// RunLoop的實現(xiàn)
int CFRunLoopRunSpecific(runloop, modeName, seconds, stopAfterHandle) {
/// 首先根據(jù)modeName找到對應(yīng)mode
CFRunLoopModeRef currentMode = __CFRunLoopFindMode(runloop, modeName, false);
/// 如果mode里沒有source/timer/observer, 直接返回寂拆。
if (__CFRunLoopModeIsEmpty(currentMode)) return;
/// 1. 通知 Observers: RunLoop 即將進入 loop奢米。
__CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopEntry);
/// 內(nèi)部函數(shù),進入loop
__CFRunLoopRun(runloop, currentMode, seconds, returnAfterSourceHandled) {
Boolean sourceHandledThisLoop = NO;
int retVal = 0;
do {
/// 2. 通知 Observers: RunLoop 即將觸發(fā) Timer 回調(diào)纠永。
__CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeTimers);
/// 3. 通知 Observers: RunLoop 即將觸發(fā) Source0 (非port) 回調(diào)鬓长。
__CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeSources);
/// 執(zhí)行被加入的block
__CFRunLoopDoBlocks(runloop, currentMode);
/// 4. RunLoop 觸發(fā) Source0 (非port) 回調(diào)。
sourceHandledThisLoop = __CFRunLoopDoSources0(runloop, currentMode, stopAfterHandle);
/// 執(zhí)行被加入的block
__CFRunLoopDoBlocks(runloop, currentMode);
/// 5. 如果有 Source1 (基于port) 處于 ready 狀態(tài)尝江,直接處理這個 Source1 然后跳轉(zhuǎn)去處理消息涉波。
if (__Source0DidDispatchPortLastTime) {
Boolean hasMsg = __CFRunLoopServiceMachPort(dispatchPort, &msg)
if (hasMsg) goto handle_msg;
}
/// 通知 Observers: RunLoop 的線程即將進入休眠(sleep)。
if (!sourceHandledThisLoop) {
__CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeWaiting);
}
/// 7. 調(diào)用 mach_msg 等待接受 mach_port 的消息炭序。線程將進入休眠, 直到被下面某一個事件喚醒啤覆。
/// ? 一個基于 port 的Source 的事件。
/// ? 一個 Timer 到時間了
/// ? RunLoop 自身的超時時間到了
/// ? 被其他什么調(diào)用者手動喚醒
__CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort) {
mach_msg(msg, MACH_RCV_MSG, port); // thread wait for receive msg
}
/// 8. 通知 Observers: RunLoop 的線程剛剛被喚醒了少态。
__CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopAfterWaiting);
/// 收到消息城侧,處理消息。
handle_msg:
/// 9.1 如果一個 Timer 到時間了彼妻,觸發(fā)這個Timer的回調(diào)嫌佑。
if (msg_is_timer) {
__CFRunLoopDoTimers(runloop, currentMode, mach_absolute_time())
}
/// 9.2 如果有dispatch到main_queue的block,執(zhí)行block侨歉。
else if (msg_is_dispatch) {
__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(msg);
}
/// 9.3 如果一個 Source1 (基于port) 發(fā)出事件了屋摇,處理這個事件
else {
CFRunLoopSourceRef source1 = __CFRunLoopModeFindSourceForMachPort(runloop, currentMode, livePort);
sourceHandledThisLoop = __CFRunLoopDoSource1(runloop, currentMode, source1, msg);
if (sourceHandledThisLoop) {
mach_msg(reply, MACH_SEND_MSG, reply);
}
}
/// 執(zhí)行加入到Loop的block
__CFRunLoopDoBlocks(runloop, currentMode);
if (sourceHandledThisLoop && stopAfterHandle) {
/// 進入loop時參數(shù)說處理完事件就返回。
retVal = kCFRunLoopRunHandledSource;
} else if (timeout) {
/// 超出傳入?yún)?shù)標記的超時時間了
retVal = kCFRunLoopRunTimedOut;
} else if (__CFRunLoopIsStopped(runloop)) {
/// 被外部調(diào)用者強制停止了
retVal = kCFRunLoopRunStopped;
} else if (__CFRunLoopModeIsEmpty(runloop, currentMode)) {
/// source/timer/observer一個都沒有了
retVal = kCFRunLoopRunFinished;
}
/// 如果沒超時幽邓,mode里沒空炮温,loop也沒被停止,那繼續(xù)loop牵舵。
} while (retVal == 0);
}
/// 10. 通知 Observers: RunLoop 即將退出柒啤。
__CFRunLoopDoObservers(rl, currentMode, kCFRunLoopExit);
}
可以看到,實際上 RunLoop 就是這樣一個函數(shù)畸颅,其內(nèi)部是一個 do-while 循環(huán)担巩。當你調(diào)用 CFRunLoopRun() 時,線程就會一直停留在這個循環(huán)里没炒;直到超時或被手動停止涛癌,該函數(shù)才會返回。
相關(guān)邏輯圖
demo
//demo中有 起死回升術(shù)
// 切記 崩潰的時候不支持xcode調(diào)試 要不你就會以為只能起死回生一次呢
//還可以用來檢測主線程的卡頓等
https://github.com/rjb0514/RunLoop.git
參考blog
//深入理解RunLoop YYKit的作者 https://blog.ibireme.com/2015/05/18/runloop/
iOS—RunLoop深度剖析 http://www.reibang.com/p/de2716807570
//sunnyxx線下分享RunLoop
https://v.youku.com/v_show/id_XODgxODkzODI0.html