RunLoop 的概念
一般來講柜与,一個線程一次只能執(zhí)行一個任務(wù),執(zhí)行完成后線程就會退出粘室。如果我們需要一個機制马昙,讓線程能隨時處理事件但并不退出桃犬,通常的代碼邏輯是這樣的:
function loop() {
initialize();
do {
var message = get_next_message();
process_message(message);
} while (message != quit);
}
這種模型通常被稱作 Event Loop刹悴。 Event Loop 在很多系統(tǒng)和框架里都有實現(xiàn),比如 Node.js 的事件處理攒暇,比如 Windows 程序的消息循環(huán)土匀,再比如 OSX/iOS 里的 RunLoop。實現(xiàn)這種模型的關(guān)鍵點在于:如何管理事件/消息形用,如何讓線程在沒有處理消息時休眠以避免資源占用就轧、在有消息到來時立刻被喚醒。
所以田度,RunLoop 實際上就是一個對象钓丰,這個對象管理了其需要處理的事件和消息,并提供了一個入口函數(shù)來執(zhí)行上面 Event Loop 的邏輯每币。線程執(zhí)行了這個函數(shù)后,就會一直處于這個函數(shù)內(nèi)部 "接受消息->等待->處理" 的循環(huán)中琢歇,直到這個循環(huán)結(jié)束(比如傳入 quit 的消息)兰怠,函數(shù)返回。
OSX/iOS 系統(tǒng)中李茫,提供了兩個這樣的對象:NSRunLoop 和 CFRunLoopRef揭保。
CFRunLoopRef 是在 CoreFoundation 框架內(nèi)的,它提供了純 C 函數(shù)的 API魄宏,所有這些 API 都是線程安全的秸侣。
NSRunLoop 是基于 CFRunLoopRef 的封裝,提供了面向?qū)ο蟮?API宠互,但是這些 API 不是線程安全的味榛。
CFRunLoopRef 的代碼是開源的,你可以在這里 http://opensource.apple.com/tarballs/CF/ 下載到整個 CoreFoundation 的源碼來查看予跌。
(Update: Swift 開源后搏色,蘋果又維護(hù)了一個跨平臺的 CoreFoundation 版本:https://github.com/apple/swift-corelibs-foundation/,這個版本的源碼可能和現(xiàn)有 iOS 系統(tǒng)中的實現(xiàn)略不一樣券册,但更容易編譯频轿,而且已經(jīng)適配了 Linux/Windows。)
RunLoop 與線程的關(guān)系
首先烁焙,iOS 開發(fā)中能遇到兩個線程對象: pthread_t 和 NSThread航邢。過去蘋果有份文檔標(biāo)明了 NSThread 只是 pthread_t 的封裝,但那份文檔已經(jīng)失效了骄蝇,現(xiàn)在它們也有可能都是直接包裝自最底層的 mach thread膳殷。蘋果并沒有提供這兩個對象相互轉(zhuǎn)換的接口,但不管怎么樣乞榨,可以肯定的是 pthread_t 和 NSThread 是一一對應(yīng)的秽之。比如当娱,你可以通過 pthread_main_thread_np() 或 [NSThread mainThread] 來獲取主線程;也可以通過 pthread_self() 或 [NSThread currentThread] 來獲取當(dāng)前線程考榨。CFRunLoop 是基于 pthread 來管理的跨细。
蘋果不允許直接創(chuàng)建 RunLoop,它只提供了兩個自動獲取的函數(shù):CFRunLoopGetMain() 和 CFRunLoopGetCurrent()河质。 這兩個函數(shù)內(nèi)部的邏輯大概是下面這樣:
/// 全局的Dictionary冀惭,key 是 pthread_t, value 是 CFRunLoopRef
static CFMutableDictionaryRef loopsDic;
/// 訪問 loopsDic 時的鎖
static CFSpinLock_t loopsLock;
/// 獲取一個 pthread 對應(yīng)的 RunLoop掀鹅。
CFRunLoopRef _CFRunLoopGet(pthread_t thread) {
OSSpinLockLock(&loopsLock);
if (!loopsDic) {
// 第一次進(jìn)入時散休,初始化全局Dic,并先為主線程創(chuàng)建一個 RunLoop乐尊。
loopsDic = CFDictionaryCreateMutable();
CFRunLoopRef mainLoop = _CFRunLoopCreate();
CFDictionarySetValue(loopsDic, pthread_main_thread_np(), mainLoop);
}
/// 直接從 Dictionary 里獲取戚丸。
CFRunLoopRef loop = CFDictionaryGetValue(loopsDic, thread));
if (!loop) {
/// 取不到時,創(chuàng)建一個
loop = _CFRunLoopCreate();
CFDictionarySetValue(loopsDic, thread, loop);
/// 注冊一個回調(diào)扔嵌,當(dāng)線程銷毀時限府,順便也銷毀其對應(yīng)的 RunLoop。
_CFSetTSD(..., thread, loop, __CFFinalizeRunLoop);
}
OSSpinLockUnLock(&loopsLock);
return loop;
}
CFRunLoopRef CFRunLoopGetMain() {
return _CFRunLoopGet(pthread_main_thread_np());
}
CFRunLoopRef CFRunLoopGetCurrent() {
return _CFRunLoopGet(pthread_self());
}
從上面的代碼可以看出痢缎,線程和 RunLoop 之間是一一對應(yīng)的胁勺,其關(guān)系是保存在一個全局的 Dictionary 里。線程剛創(chuàng)建時并沒有 RunLoop独旷,如果你不主動獲取署穗,那它一直都不會有。RunLoop 的創(chuàng)建是發(fā)生在第一次獲取時嵌洼,RunLoop 的銷毀是發(fā)生在線程結(jié)束時案疲。你只能在一個線程的內(nèi)部獲取其 RunLoop(主線程除外)。
http://blog.ibireme.com/2015/05/18/runloop/
runloop是iOS系統(tǒng)對事件接受和分發(fā)機制的一個實現(xiàn)麻养,是線程的基本架構(gòu)部分络拌。一個runloop就是一個事件處理循環(huán),用來不停的調(diào)配工作以及處理輸入事件回溺。 使用runloop的目的是使你的線程在有工作的時候工作春贸,沒有的時候休眠,以達(dá)到節(jié)省cpu的目的遗遵。runloop的管理并不完全是自動萍恕,當(dāng)我們創(chuàng)建一個子線程時,我們必須在適當(dāng)?shù)臅r候啟動Runloop并正確響應(yīng)事件车要。 子線程不需要顯式的創(chuàng)建RunLoop允粤,每個線程,包括程序的主線程都有與之對應(yīng)的RunLoop對象,但是自己創(chuàng)建的線程需要手動運行RunLoop的運行方法类垫。不過程序啟動時司光,主線程會自動創(chuàng)建并運行RunLoop。
在沒有手加Autorelease Pool的情況下悉患,Autorelease對象是在當(dāng)前的runloop迭代結(jié)束時釋放的残家,而它能夠釋放的原因是系統(tǒng)在每個runloop迭代中都加入了自動釋放池Push和Pop.
什么是Runloop
Runloop,顧名思義就是運行的循環(huán)售躁。簡單理解就是多線程機制中的基礎(chǔ)坞淮,它能夠接收外部事件的輸入,并且在有事件的時候保持運行陪捷,在沒有事件的時候進(jìn)入休眠回窘。并且它對于線程的消息處理機制進(jìn)行了很好的封裝。Runloop的作用就是要減少cpu做無謂的空轉(zhuǎn)市袖,cpu可在空閑的時候休眠啡直,以節(jié)約電量。
對于線程來說苍碟,每一個線程都有一個runloop對象付枫,是否能向某個線程的runloop發(fā)送事件取決于你是否啟動了這個runloop,系統(tǒng)會默認(rèn)在你的程序啟動的時候運行主線程上的runloop驰怎,但是你自定義創(chuàng)建出來的線程可以不需要運行runloop,一些第三方框架二打,例如AFNetworking县忌,就有在自己的線程上維護(hù)一個runloop對象。
在 Core Foundation 里面關(guān)于 RunLoop 有5個類:
CFRunLoopRef
CFRunLoopModeRef
CFRunLoopSourceRef
CFRunLoopTimerRef
CFRunLoopObserverRef
他們的關(guān)系可以從NSRunloop對象的結(jié)構(gòu)定義中得出继效。首先症杏,runloop對象在Cocoa和Core Foundation中都有實現(xiàn),但是他們做了很好的橋接瑞信,你可以直接調(diào)用
CFRunLoopRef runLoopRef = currentThreadRunLoop.getCFRunLoop;
來獲取一個CoreFoundation中的runloop對象厉颤。然后,當(dāng)你在查看NSRunloop的結(jié)構(gòu)的時候凡简,你應(yīng)該能看到:
<CFRunLoop 0x7fd360f5af30 [0x1090a1180]>{wakeup port = 0x4507, stopped = false, ignoreWakeUps = true,
current mode = (none),
common modes = <CFBasicHash 0x7fd360f5a470 [0x1090a1180]>{type = mutable set, count = 1,
entries =>
2 : <CFString 0x10907d080 [0x1090a1180]>{contents = "kCFRunLoopDefaultMode"}},
common mode items = (null),
modes = <CFBasicHash 0x7fd360f5b2b0 [0x1090a1180]>{type = mutable set, count = 1,
entries =>
2 : <CFRunLoopMode 0x7fd360f5aff0 [0x1090a1180]>{name = kCFRunLoopDefaultMode, port set = 0x4703, timer port = 0x4803,
sources0 = (null),
sources1 = (null),
observers = <CFArray 0x7fd360f5b1a0 [0x1090a1180]>{type = mutable-small, count = 1, values = (
0 : <CFRunLoopObserver 0x7fd360f5c7f0 [0x1090a1180]>{valid = Yes, activities = 0xfffffff, repeats = Yes, order = 0, callout = currentRunLoopObserver (0x10855b340), context = <CFRunLoopObserver context 0x7fd361213d70>}
)},
timers = <CFArray 0x7fd360e020d0 [0x1090a1180]>{type = mutable-small, count = 1, values = (
0 : <CFRunLoopTimer 0x7fd360e01f90 [0x1090a1180]>{valid = Yes, firing = No, interval = 1, tolerance = 0, next fire date = 463742311 (-2.53606331 @ 23607719248079), callout = (NSTimer) [SCCustomThread handleTimerTask] (0x1086416f1 / 0x10855b560) (/Users/useruser/Library/Developer/CoreSimulator/Devices/424D3C6E-8DC0-418B-A2EC-8EDF89507348/data/Containers/Bundle/Application/4D07AF38-9BFC-4617-BAE0-4CB0D7966CC8/runloopTest.app/runloopTest), context = <CFRunLoopTimer context 0x7fd360e01f70>}
)},
currently 463742313 (23610255156065) / soft deadline in: 1.84467441e+10 sec (@ 23607719248079) / hard deadline in: 1.84467441e+10 sec (@ 23607719248079)
},}}
可以看到一個runloop對象包含各種Mode——currentMode逼友,common mode,modes等等秤涩,這里的示例我只指定了一個defaultMode帜乞。每個mode對應(yīng)了source,observers和timers筐眷。
也許你會注意到 source 包括了source0和source1兩個版本黎烈。
Source0 只包含了一個回調(diào)(函數(shù)指針),它并不能主動觸發(fā)事件。使用時照棋,你需要先調(diào)用 CFRunLoopSourceSignal(source)资溃,將這個 Source 標(biāo)記為待處理,然后手動調(diào)用 CFRunLoopWakeUp(runloop) 來喚醒 RunLoop烈炭,讓其處理這個事件溶锭。
Source1 包含了一個 mach_port 和一個回調(diào)(函數(shù)指針),被用于通過內(nèi)核和其他線程相互發(fā)送消息梳庆。這種 Source 能主動喚醒 RunLoop 的線程暖途。
CFRunLoopObserver類型的對象也可以稱之為觀察者。每個觀察者都包含了一個回調(diào)膏执,當(dāng)runloop的狀態(tài)發(fā)生變化時驻售,你可以通過回調(diào)來知道當(dāng)前的狀態(tài)。
在你的程序中更米,runloop的過程實際上是一個無限循環(huán)的循環(huán)體欺栗,這個循環(huán)體是由你的程序來運行的。主線程的runloop由于系統(tǒng)已經(jīng)實現(xiàn)并且沒有它程序就不能運行征峦,因此不需要我們手動去運行這個runloop迟几。然而如果我們需要在自定義的線程中使用到runloop,我們則需要用一個do…while循環(huán)來驅(qū)動它栏笆。而runloop對象負(fù)責(zé)不斷地在循環(huán)體中運行傳進(jìn)來的事件类腮,然后將事件發(fā)給相應(yīng)的響應(yīng)。
如果你打開你的程序的main.m蛉加,你就會發(fā)現(xiàn)其實主線程的runloop就是在main函數(shù)中進(jìn)行的蚜枢,并且系統(tǒng)已經(jīng)為你生成好了autoreleasepool,因此你也無需操心主線程上的內(nèi)存釋放到底是在什么時候執(zhí)行了:
int main(int argc, char * argv[]) {
@autoreleasepool {
return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
}
}
根據(jù)響應(yīng)源的不同针饥,runloop也被分成了許多種不同的模式厂抽,這就是被Cocoa和Core Foundation都封裝了的runloopMode。主要是這么幾種:
NSDefaultRunLoopMode: 大多數(shù)工作中默認(rèn)的運行方式丁眼。
NSConnectionReplyMode: 使用這個Mode去監(jiān)聽NSConnection對象的狀態(tài)筷凤。
NSModalPanelRunLoopMode: 使用這個Mode在Model Panel情況下去區(qū)分事件(OS X開發(fā)中會遇到)。
NSEventTrackingRunLoopMode: 使用這個Mode去跟蹤來自用戶交互的事件(比如UITableView上下滑動)苞七。
NSRunLoopCommonModes: 這是一個偽模式藐守,其為一組run loop mode的集合。如果將Input source加入此模式蹂风,意味著關(guān)聯(lián)Input source到Common Modes中包含的所有模式下吗伤。在iOS系統(tǒng)中NSRunLoopCommonMode包含NSDefaultRunLoopMode、NSTaskDeathCheckMode硫眨、NSEventTrackingRunLoopMode.可使用CFRunLoopAddCommonMode方法向Common Modes中添加自定義mode足淆。
在文首的情況中巢块,我們可以根據(jù)蘋果官方文檔的定義知道,當(dāng)你在滑動頁面的時候巧号,主線程的runloop自動進(jìn)入了NSEventTrackingRunLoopMode族奢,而你的timer只是運行在DefaultMode下,所以不能響應(yīng)。那么最簡單的辦法就是將你的timer添加在其他的mode下,像這樣即可:
[[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSRunLoopCommonModes];
需要注意的是CommonModes其實并不是一種Mode蛉迹,而是一個集合。因此runloop并不能在CommonModes下運行廊敌,相反,你可以將需要輸入的事件源添加為這個mode门怪,這樣無論runloop運行在哪個mode下都可以響應(yīng)這個輸入事件骡澈,否則這個事件將不會得到響應(yīng)。
Input Source
輸入源包括三種掷空,端口肋殴,自定義輸入源和performSelector的消息。根據(jù)上面的圖我們可以看出坦弟,在runloop接收到消息并執(zhí)行了指定方法的時候护锤,它會執(zhí)行runUntilDate:這個方法來退出當(dāng)前循環(huán)。
端口源是基于Mach port的酿傍,其他進(jìn)程或線程可以通過端口來發(fā)送消息烙懦。這里的知識點需要深入到Mach,就已經(jīng)比較晦澀難懂了……這里你只需要知道你可以用Cocoa封裝的NSPort對象來進(jìn)行線程之間的通信赤炒,而這種通信方式所產(chǎn)生的事件就是通過端口源來傳入runloop的氯析。關(guān)于Mach port的更深層介紹可以看這篇。http://segmentfault.com/a/1190000002400329
自定義輸入源可霎。Core Foundation提供了CFRunLoopSourceRef類型的相關(guān)函數(shù),可以用來創(chuàng)建自定義輸入源宴杀。
performSelector輸入源:
//在主線程的Run Loop下執(zhí)行指定的 @selector 方法
performSelectorOnMainThread:withObject:waitUntilDone:
performSelectorOnMainThread:withObject:waitUntilDone:modes:
//在當(dāng)前線程的Run Loop下執(zhí)行指定的 @selector 方法
performSelector:onThread:withObject:waitUntilDone:
performSelector:onThread:withObject:waitUntilDone:modes:
//在當(dāng)前線程的Run Loop下延遲加載指定的 @selector 方法
performSelector:withObject:afterDelay:
performSelector:withObject:afterDelay:inModes:
//取消當(dāng)前線程的調(diào)用
cancelPreviousPerformRequestsWithTarget:
cancelPreviousPerformRequestsWithTarget:selector:object:
runloop生命周期
每一次runloop其實都是一次循環(huán)癣朗,runloop會在循環(huán)中執(zhí)行runUntilDate: 或者runMode: beforeDate: 來開始每一個循環(huán)。而每一個循環(huán)又分為下面幾個階段旺罢,也就是runloop的生命周期:
kCFRunLoopEntry 進(jìn)入循環(huán)
kCFRunLoopBeforeTimers 先接收timer的事件
kCFRunLoopBeforeSources 接收來自input source的事件
kCFRunLoopBeforeWaiting 如果沒有事件旷余,則準(zhǔn)備進(jìn)入休眠模式,在這里扁达,如果沒有事件傳入正卧,runloop會運行直到循環(huán)中給定的日期,如果你給的是distantFuture跪解,那么這個runloop會無限等待下去
kCFRunLoopAfterWaiting 從休眠中醒來炉旷,直接回到kCFRunLoopBeforeTimers狀態(tài)
kCFRunLoopExit 退出循環(huán)
這些狀態(tài)也是一個枚舉類型,系統(tǒng)是這么定義的,你可以使用observer來觀測到這些狀態(tài):
/* Run Loop Observer Activities */
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
kCFRunLoopEntry = (1UL << 0),
kCFRunLoopBeforeTimers = (1UL << 1),
kCFRunLoopBeforeSources = (1UL << 2),
kCFRunLoopBeforeWaiting = (1UL << 5),
kCFRunLoopAfterWaiting = (1UL << 6),
kCFRunLoopExit = (1UL << 7),
kCFRunLoopAllActivities = 0x0FFFFFFFU
};
我們下面做一個測試窘行,在demo中我們定義了一個新的線程類饥追,這樣我們可以自己啟動和維護(hù)它的runloop對象。
- (void)main
{
@autoreleasepool {
NSLog(@"Thread Enter");
[[NSThread currentThread] setName:@"This is a test thread"];
NSRunLoop *currentThreadRunLoop = [NSRunLoop currentRunLoop];
// 或者
// CFRunLoopRef currentThreadRunLoop = CFRunLoopGetCurrent();
CFRunLoopObserverContext context = {0, (__bridge void *)(self), NULL, NULL, NULL};
CFRunLoopObserverRef observer = CFRunLoopObserverCreate(kCFAllocatorDefault, kCFRunLoopAllActivities, YES, 0, ¤tRunLoopObserver, &context);
if (observer) {
CFRunLoopRef runLoopRef = currentThreadRunLoop.getCFRunLoop;
CFRunLoopAddObserver(runLoopRef, observer, kCFRunLoopDefaultMode);
}
// 創(chuàng)建一個Timer罐盔,重復(fù)調(diào)用來驅(qū)動Run Loop
//[NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(handleTimerTask) userInfo:nil repeats:YES];
do {
[currentThreadRunLoop runMode:NSDefaultRunLoopMode beforeDate:[NSDate dateWithTimeIntervalSinceNow:3]];
} while (1);
}
}
輸入源或者timer對于runloop來說是必要條件但绕,如果沒有添加任何輸入源,則runloop根本不會啟動惶看,所以上面的代碼中添加timer的操作捏顺,實際上是添加了一個默認(rèn)的事件輸入源,能讓runloop保持運行纬黎。但是實際上幅骄,當(dāng)你創(chuàng)建好一個runloop對象后,任何輸入的事件都可以觸發(fā)runloop的啟動莹桅。
例如下面的:
[self performSelector:@selector(selectorTest) onThread:self.runLoopThread withObject:nil waitUntilDone:NO];
記住昌执,如果你需要自己來啟動和維護(hù)runloop的話,核心就在于一個do…while循環(huán)诈泼,你可以為runloop的跳出設(shè)置一個條件懂拾,也可以讓runloop無限進(jìn)行下去。在runloop沒有接收到事件進(jìn)入休眠狀態(tài)之后铐达,如果調(diào)用performSelector岖赋,runloop的狀態(tài)變化如下:
Current thread Run Loop activity: kCFRunLoopAfterWaiting
Current thread Run Loop activity: kCFRunLoopBeforeTimers
Current thread Run Loop activity: kCFRunLoopBeforeSources
fuck
fuck_1
Current thread Run Loop activity: kCFRunLoopExit
Current thread Run Loop activity: kCFRunLoopEntry
Current thread Run Loop activity: kCFRunLoopBeforeTimers
Current thread Run Loop activity: kCFRunLoopBeforeSources
Current thread Run Loop activity: kCFRunLoopExit
Current thread Run Loop activity: kCFRunLoopEntry
Current thread Run Loop activity: kCFRunLoopBeforeTimers
Current thread Run Loop activity: kCFRunLoopBeforeSources
Current thread Run Loop activity: kCFRunLoopBeforeWaiting
在這里我連續(xù)調(diào)用了兩次performSelector,可以看到runloop也經(jīng)歷了兩個循環(huán)瓮孙,而如果只調(diào)用一次的話唐断,不會有多出來的那次runloop(你可以自己嘗試一下),這是否說明每一次performSelector執(zhí)行完畢之后都會立即結(jié)束當(dāng)前runloop開始新的杭抠,蘋果的官方文檔里有一句話:
The run loop processes all queued perform selector calls each time through the loop, rather than processing one during each loop iteration
應(yīng)該意思是并不是像上面看到的結(jié)果那樣每一次循環(huán)執(zhí)行一次脸甘,而是有一個待執(zhí)行的操作隊列。如果我同時執(zhí)行四次performSelector偏灿,像這樣:
[self performSelector:@selector(selectorTest) onThread:self.runLoopThread withObject:nil waitUntilDone:NO];
[self performSelector:@selector(selectorTest_1) onThread:self.runLoopThread withObject:nil waitUntilDone:NO];
[self performSelector:@selector(selectorTest_2) onThread:self.runLoopThread withObject:nil waitUntilDone:NO];
[self performSelector:@selector(selectorTest_2) onThread:self.runLoopThread withObject:nil waitUntilDone:NO];
實際上得到的結(jié)果和上面是一樣的丹诀,然而當(dāng)我將他們的waitUntilDone參數(shù)都設(shè)置為YES之后,我們可以看到不一樣的地方:
Thread Enter
Current thread Run Loop activity: kCFRunLoopEntry
Current thread Run Loop activity: kCFRunLoopBeforeTimers
Current thread Run Loop activity: kCFRunLoopBeforeSources
fuck
Current thread Run Loop activity: kCFRunLoopExit
Current thread Run Loop activity: kCFRunLoopEntry
Current thread Run Loop activity: kCFRunLoopBeforeTimers
Current thread Run Loop activity: kCFRunLoopBeforeSources
fuck_1
Current thread Run Loop activity: kCFRunLoopExit
Current thread Run Loop activity: kCFRunLoopEntry
Current thread Run Loop activity: kCFRunLoopBeforeTimers
Current thread Run Loop activity: kCFRunLoopBeforeSources
fuck_2
Current thread Run Loop activity: kCFRunLoopExit
Current thread Run Loop activity: kCFRunLoopEntry
Current thread Run Loop activity: kCFRunLoopBeforeTimers
Current thread Run Loop activity: kCFRunLoopBeforeSources
fuck_2
Current thread Run Loop activity: kCFRunLoopExit
Current thread Run Loop activity: kCFRunLoopEntry
Current thread Run Loop activity: kCFRunLoopBeforeTimers
Current thread Run Loop activity: kCFRunLoopBeforeSources
Current thread Run Loop activity: kCFRunLoopBeforeWaiting
你可以看到每一個performSelector操作都單獨執(zhí)行了一個runloop翁垂,從蘋果的文檔中我們可以找到這個方法的定義:
performSelector:onThread:withObject:waitUntilDone:
performSelector:onThread:withObject:waitUntilDone:modes:
Performs the specified selector on any thread for which you have an NSThread object. These methods give you the option of blocking the current thread until the selector is performed.
也就是說铆遭,waitUntilDone意味著這個操作是否會在當(dāng)前線程阻塞其他的輸入源,如果等于True沿猜,則每一次runloop循環(huán)只會處理這一個selector的調(diào)用枚荣,如果為False,則隊列中后面等待著的selector調(diào)用都會在同一次runloop循環(huán)中執(zhí)行啼肩。至于上文的執(zhí)行了兩個runloop循環(huán)的現(xiàn)象橄妆,我猜測應(yīng)該是當(dāng)runloop從休眠模式被喚醒的時候衙伶,當(dāng)前循環(huán)執(zhí)行完喚醒的操作后就會立即結(jié)束,釋放掉之前可能累積下來的內(nèi)存呼畸,然后開始新的循環(huán)痕支,將隊列中的其他輸入逐個放進(jìn)runloop循環(huán)中執(zhí)行。
來自http://sergiochan.xyz/2015/10/22/runloop初窺/
NSRunLoop的原理
以下內(nèi)容來自RyanJIN http://www.reibang.com/p/ebb3e42049fd的簡書蛮原。
關(guān)于NSRunLoop推薦看一下來自百度工程師孫源的分享視頻:http://v.youku.com/v_show/id_XODgxODkzODI0.html
RunLoop就是跑圈, 保證程序一直在執(zhí)行. App運行起來之后, 即使你什么都不做, 放在那兒它也不會退出, 而是一直在"跑圈", 這就是RunLoop干的事. 主線程會自動創(chuàng)建一個RunLoop來保證程序一直運行. 但子線程默認(rèn)不創(chuàng)建NSRunLoop, 所以子線程的任務(wù)一旦返回, 線程就over了.
上面的并發(fā)operation當(dāng)start函數(shù)返回后子線程就退出了, 當(dāng)NSURLConnection的delegate回調(diào)時, 線程已經(jīng)木有了, 所以你也就收不到回調(diào)了. 為了保證子線程持續(xù)live(等待connection回調(diào)), 你需要在子線程中加入RunLoop, 來保證它不會被kill掉.
RunLoop在某一時刻只能在一種模式下運行, 更換模式時需要暫停當(dāng)前的Loop, 然后重啟新的Loop. RunLoop主要有下面幾個模式:
NSDefalutRunLoopMode : 默認(rèn)Mode, 通常主線程在這個模式下運行
UITrackingRunLoopMode : 滑動ScrollView是會切換到這個模式
NSRunLoopCommonModes: 包括上面兩個模式
這邊需要特別注意的是, 在滑動ScrollView的情況下, 系統(tǒng)會自動把RunLoop模式切換成UITrackingRunLoopMode來保證ScrollView的流暢性.
[NSTimer scheduledTimerWithTimeInterval:1.f
target:self
selector:@selector(timerAction:)
userInfo:nil
reports:YES];
當(dāng)你在滑動ScrollView的時候上面的timer會失效, 原因是Timer是默認(rèn)加在NSDefalutRunLoopMode上的, 而滑動ScrollView后系統(tǒng)把RunLoop切換為UITrackingRunLoopMode, 所以timer就不會執(zhí)行了. 解決方法是把該Timer加到NSRunLoopCommonModes下, 這樣即使滑動ScrollView也不會影響timer了.
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
另外還有一個trick是當(dāng)tableview的cell從網(wǎng)絡(luò)異步加載圖片, 加載完成后在主線程刷新顯示圖片, 這時滑動tableview會造成卡頓. 通常的思路是tableview滑動的時候延遲加載圖片, 等停止滑動時再顯示圖片. 這里我們可以通過RunLoop來實現(xiàn).
[self.cellImageView performSelector:@sector(setImage:)
withObject:downloadedImage
afterDelay:0
inModes:@[NSDefaultRunLoopMode]];
當(dāng)NSRunLoop為NSDefaultRunLoopMode的時候tableview肯定停止滑動了, why? 因為如果還在滑動中, RunLoop的mode應(yīng)該是UITrackingRunLoopMode.
呼叫NSURLConnection的異步回調(diào)
現(xiàn)在解決方案已經(jīng)很清晰了, 就是利用RunLoop來監(jiān)督線程, 讓它一直等待delegate的回調(diào). 上面已經(jīng)說到Main Thread是默認(rèn)創(chuàng)建了一個RunLoop的, 所以我們的Option 1是讓start函數(shù)在主線程運行(即使[operation start]是在子線程調(diào)用的).
- (void)start
{
if (![NSThread isMainThread]) {
[self performSelectorOnMainThread:@selector(start)
withObject:nil
waitUntilDone:NO];
return;
}
// set up NSURLConnection...
}
或者這樣:
- (void)start
{
[[NSOperationQueue mainQueue] addOperationWithBlock:^{
self.connection = [NSURLConnection connectionWithRequest:self.request delegate:self];
}];
}
這樣我們可以簡單直接的使用main run loop, 因為數(shù)據(jù)delivery是非澄孕耄快滴. 然后我們就可以將處理incoming data的操作放到子線程去...
Option 2是讓operation的start函數(shù)在子線程運行, 但是我們?yōu)樗鼊?chuàng)建一個RunLoop. 然后把URL connection schedule到上面去. 我們先來瞅瞅AFNetworking是怎么做滴:
+ (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];
}
AFNetworking創(chuàng)建了一個新的子線程(在子線程中調(diào)用NSRunLoop *runloop = [NSRunLoop currentRunLoop]; 獲取RunLoop對象的時候, 就會創(chuàng)建RunLoop), 然后把它加到RunLoop里面來保證它一直運行.
這邊我們可以簡單的判斷下當(dāng)前start()的線程是子線程還是主線程, 如果是子線程則調(diào)用[NSRunLoop currentRunLoop]創(chuàng)新RunLoop, 否則就直接調(diào)用[NSRunLoop mainRunLoop], 當(dāng)然在主線程下就沒必要調(diào)用[runLoop run]了, 因為它本來就是一直run的.
P.S. 我們還可以使用CFRunLoop來啟動和停止RunLoop, 像下面這樣:
[self.connection scheduleInRunLoop:[NSRunLoop currentRunLoop]
forMode:NSRunLoopCommonModes];
CFRunLoopRun();
等到該Operation結(jié)束的時候, 一定要記得調(diào)用CFRunLoopStop()停止當(dāng)前線程的RunLoop, 讓當(dāng)前線程在operation finished之后可以退出.
多線程。