原文鏈接:https://blog.csdn.net/u014795020/article/details/72084735
前言
RunLoop 是 iOS 和 OSX 開發(fā)中非常基礎(chǔ)的一個概念,為了讓大家更加快速融入,請先一段代碼:
+ (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)networkRequestThreadEntryPoint:(id)__unused object {
@autoreleasepool {
[[NSThread currentThread] setName:@"AFNetworking"];
NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
[runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
[runLoop run];
}
}
以上是AFN2.x的一段經(jīng)典代碼
首先我們要明確一個概念,線程一般都是一次執(zhí)行完畢任務(wù),就銷毀了
。
而在線程中添加了runloop
,并運行起來,實際上是添加了一個do,while循環(huán)
,這樣這個線程的程序就一直卡在do,while循環(huán)上
,這樣相當(dāng)于線程的任務(wù)一直沒有執(zhí)行完
,所以線程一直不會銷毀
。
所以,一旦我們添加了一個runloop,并run了,我們?nèi)绻N毀這個線程,必須停止runloop,至于停止的方式,我們接著往下看。
這里創(chuàng)建了一個線程,取名為AFNetworking,因為添加了一個runloop,所以這個線程不會被銷毀,直到runloop停止。
[runloop addPort: [NSMachPort port] forMode: NSDefaultRunLoopMode];
這行代碼的目的是添加一個端口監(jiān)事件,這也是我們后面會講到的一種線程間的通信方式-基于端口的通信。
[runloop run];
runloop開始跑起來,但是要注意,這種runloop,只有一種方式能停止。
[NSRunloop currentRunloop] removePort: <#(nonnull NSPort)#> forMode: <#(nonull NSRunLoopMode)#>
只有從runloop中移除我們之前添加的端口,這樣runloop就沒有任何事件,所以runloop會直接退出。
再次回到AFN2.x的這行源碼上,因為他用的是run,而且并沒有記錄下自己添加的NSMachPort,所有顯然,它沒有打算退出這個runloop,這是一個常駐線程
。事實上,看過AFN2.x源碼的同學(xué)都會知道,這個thread需要常駐的原因,在此就不做贅述了。
接下來我們看看AFN3.x是怎么用runloop的:
需要開啟的時候:
CFRunLoopRun();
終止的時候:
CFRunloopStop(CFRunLoopGetCurrent());
由于NSUrlSession參考了AFN2.x的優(yōu)點,自己維護了一個線程池,做Request線程的調(diào)度與管理,所以在AFN3.x中,沒有了常駐線程,都是用的run,結(jié)束的時候stop。
再看RAC中runloop:
do {
[NSRunloop.mainRunloop runMode:NSDefaultRunLoopMode beforeDate: [NSDate dateWithTimeIntervalSinceNow: 0.1]];
} while(!done);
大致講下這段代碼實現(xiàn)的內(nèi)容,自己用一個Bool值done去控制runloop的運行,每次只運行這個模式的runloop,0.1秒。0.1秒后開啟runloop的下次運行。
以上我們都大致分析一下,后面我們再來講為什么。
首先我們講講runloop的概念
Runloop,顧名思義就是跑圈,他的本質(zhì)就是一個do,while
循環(huán),當(dāng)有事做時就做事,沒事做時就休眠。至于怎么做事,怎么休眠,這個是由系統(tǒng)內(nèi)核來調(diào)度的,我們后面會講到。
每個線程都由一個Run Loop
,主線程的Run Loop會在App運行的時自動運行
,子線程需要手動獲取運行
,第一次獲取時,才會去創(chuàng)建。
每個Run Loop都會以一個模式mode來運行,可以使用NSRunLoop的方法運行在某個特定的mode。
- (BOOL)runMode:(NSString *)mode beforeDate:(NSDate *)limitDate;
Run Loop的處理兩大類事件源:Timer Source和Input Source(包括performSelector *方法簇、Port或者自定義的Input Source),每個事件源都會綁定在Run Loop的某個特定模式mode上,而且只有RunLoop在這個模式下運行的時候,才會觸發(fā)Timer和Input Source。
最后,如果沒有把事件源添加到Run Loop上,Run Loop就會立刻exit,這也是一開始AFN例子,為什么需要綁定一個Port的原因。
我們先來談?wù)凴unLoop Mode
OS下Run Loop的主要運行模式mode有:
1)NSDefaultRunLoopMode
:默認的運行模式
,除了NSConnection對象的事件。
2)NSRunLoopCommonModes
:是一組常用的模式集合,將一個input source關(guān)聯(lián)到這個模式集合上,等于將input source關(guān)聯(lián)到這個模式集合中的所有模式上。在iOS系統(tǒng)中NSRunLoopCommonMode包含NSDefaultRunLoopMode、NSTaskDeathCheckMode、UITrackingRunLoopMode
。
假如我有個timer要關(guān)聯(lián)到這些模式上,一個個注冊很麻煩,我可以用:
CFRunLoopAddCommonMode([[NSRunLoop currentRunLoop] getCFRunLoop],(__bridge CFStringRef) UITrackingRunLoopMode);
將UITrackingRunLoopMode
或者其他模式添加到
這個NSRunLoopCommonModes模式
中,然后只需要將Timer關(guān)聯(lián)到NSRunLoopCommonModes
,即可以實現(xiàn)RunLoop運行在這個模式集合中任意一個模式時,這個Timer都可以觸發(fā)
。
當(dāng)然,默認情況下NSRunLoopCommonModes包含了NSDefaultRunLoopMode和UITrackingRunLoopMode。我指的是如果有其他自定義的Mode。
注意: 讓Run Loop運行在NSRunLoopCommonModes下是沒有意義的,因為一個時刻Run Loop只能運行在一個特定模式下,而不可能是個模式集合。
3)UITrackingRunLoopMode:用于跟蹤觸摸事件觸發(fā)的模式(例如UIScrollView上下滾動), 在主線程中觸摸事件
會設(shè)置為這個模式,可以用來在控件事件觸發(fā)過程中設(shè)置Timer。
- GSEventReceiveRunLoopMode:用于接受系統(tǒng)事件,屬于內(nèi)部的Run Loop模式。
5)自定義Mode:可以設(shè)置自定義的運行模式Mode,你也可以用CFRunLoopAddCommonMode
添加到NSRUnLoopCommonModes
中。
總結(jié)一下:
Run Loop 運行時只能以一種固定的模式運行,如果我們需要它切換模式,只有停掉它,再重新開其它。
運行時它只會監(jiān)控這個模式下添加的Timer Source和Input Source,如果這個模式下沒有相應(yīng)的事件源,RunLoop的運行也會立刻返回的。注意Run Loop不能在NSRunLoopCommonModes模式運行,因為NSRunLoopCommonModes其實是個模式集合,而不是一個具體的模式
,我可以添加事件源的時候使用NSRunLoopCommonModes,只要Run Loop運行在NSRunLoopCommonModes中任何一個模式,這個事件源都可以被觸發(fā)
。
Run Loop運行接口
要操作Run Loop,F(xiàn)oundation層和Core Foundation層都有相應(yīng)的接口可以操作Run Loop:Foundation層對應(yīng)的是NSRunLoop,Core Foundation層對應(yīng)的是CFRunLoopRef;
Foundation層對應(yīng)的是NSRunLoop,Core Foundation層對應(yīng)的是CFRunLoopRef
;
兩組接口差不多,不過功能上還是有許多區(qū)別的:
例如CF層可以添加自定義的Input Source事件源、(CFRunLoopSourceRef)RunLoop觀察者Observer(CFRunLoopObserverRef),很多類似功能的接口特性也是不一樣的。
NSRunLoop的運行接口:
// 運行NSRunLoop,運行模式為默認的NSDefaultRunLoopMode模式,沒有超時限制
- (void)run;
// 運行NSRunLoop:參數(shù)為時間期限,運行模式為默認的NSDefaultRunLoopMode模式
- (void)runUntilDate:(NSDate *)limitDate;
// 運行NSRunLoop:參數(shù)為運行模式、時間期限,返回值為YES表示處理事件后返回的,NO表示是超時或者停止運行導(dǎo)致返回的。
- (BOOL)runMode:(NSString *)mode beforeDate:(NSDtate *)limitDate;
CFRunLoopRef的運行接口:
// 運行CFRunLoopRef
void CFRunLoopRun();
// 運行CFRunLoopRef:參數(shù)為運行模式、時間和是否在處理Input Source后退出標(biāo)志,返回值是exit原因
SInt32 CFRunLoopRunInMode(mode, second, returnAfterSourceHandled);
// 停止運行CFRunLoop
void CFRunLoopStop(CFRunLoopRef rl);
// 喚醒CFRunLoopRef
void CFRunLoopWakeUp(CFRunLoopRef rl);
首先,詳細講解下NSRunLoop的三個運行接口:
一
- (void)run; // 無條件運行
不建議使用,因為這個接口會導(dǎo)致Run Loop永久性的在NSDefaultRunLoopMode模式。
即使用CFRunLoopStop(runloopRef);也無法停止Run Loop的運行,除非能移除這個runloop上的所有事件源,包括定時器和source事件,不然這個子線程就無法停止,只能永久運行下去。
二
- (void)runUntilDate:(NSDate *)limitDate; // 有一個超時時間限制
比上面的接口好點,有個超時時間,可以控制每次Run Loop的運行時間,也是運行在NSDefaultRunLoopMode模式。
這個方法運行Run Loop一段時間會退出給你檢查運行條件的機會,如果需要可以再次運行Run Loop。
注意CFRunLoopStop(runloopRef), 也無法停止Run Loop的運行。
使用如下的代碼:
while(!Done) {
[[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow: 10]];
NSLog(@"exiting runloop, ......");
}
注意這個Done是我們自定義的一個Bool值,用來控制是否還需要開啟下一次runloop。
這個例子大概做了如下的事情: 這個RunLoop會每10秒退出一次,然后輸出exiting runloop ……,然后下次根據(jù)我們的Done值來判斷是否再去運行runloop。
三
// 有一個超時時間限制,而且設(shè)置運行模式
- (BOOL)runMode:(NSString *)mode beforeDate:(NSDate *)limitDate;
從方法上來看,比上面多了一個參數(shù),可以設(shè)置運行模式
。
注意:這種運行方式是可以被CFRunLoopStop(runloopRef)所停止的(大家可以自己寫個例子試試)。
除此之外,這個方法和第二個方法還有一個很大的區(qū)別,就是這樣去運行runloop會多一種退出方式。這里我指的退出方式是除了timer觸發(fā)以外的事件,都會導(dǎo)致runloop退出,這里舉個簡答的例子:
- (void)testDemo1{
dispatch_async(dispatch_get_global_queue(0,0), ^ {
NSLog(@"線程開始");
// 獲取當(dāng)前線程
self.thread = [NSThread currentThread];
NSRunLoop *runloop = [NSRunLoop currentRunLoop];
// 添加一個Port,同理為了防止runloop沒事干直接退出
[runloop addPort: [NSMachPort port] forMode: NSDefaultRunLoopMode];
// 運行一個runloop, [NSDate distantFuture]:很久很久以后才讓它失效
[runloop runMode:NSDefaultRunloopMode beforeDate: [NSDate distantFuture]];
NSLog(@"線程結(jié)束");
});
dispatch_after(dispatch_time(DISPATCH_TIME_NOW,(int64_t)(2*NSEC_PER_SEC)), dispatch_get_main_queue(), ^ {
// 在我們開啟的異步線程調(diào)用方法
[self performSelector:@selector(recieveMsg) onThread: self.thread withObject: nil waitUntilDone: NO];
});
}
- (void)recieveMsg {
NSLog(@"收到消息了,在這個線程:%@", [NSThread currentThread]);
}
2016-11-22 14:04:15.250 TestRunloop3[70591:1742754] 線程開始
2016-11-22 14:04:17.250 TestRunloop3[70591:1742754] 收到消息了,在這個線程:<NSThread: 0x600000263c80>{number = 3, name = (null)}
2016-11-22 14:04:17.250 TestRunloop3[70591:1742754] 線程結(jié)束
在這里我們用了performSelector: onThread…這個方法去進行線程間的通信,這只是其中最簡單的方式。但是缺點也很明顯,就是在去調(diào)用這個線程的時候,如果線程已經(jīng)不存在了,程序就會crash。后面我們會仔細講各種線程間的通信。
我們看到,我們收到一個消息,這個消息是一個 非timer得事件,所有runloop處理完就退出,這里為什么會這樣呢,我們可以看看runloop的源代碼:
/// 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;
}
/// 6.通知 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);
/// 9.收到消息,處理消息。
handle_msg:
/// 10.1 如果一個 Timer 到時間了,觸發(fā)這個Timer的回調(diào)。
if (msg_is_timer) {
__CFRunLoopDoTimers(runloop, currentMode, mach_absolute_time())
}
/// 10.2 如果有dispatch到main_queue的block,執(zhí)行block。
else if (msg_is_dispatch) {
__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(msg);
}
/// 10.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ù)標(biāo)記的超時時間了
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);
}
/// 11. 通知 Observers: RunLoop 即將退出。
__CFRunLoopDoObservers(rl, currentMode, kCFRunLoopExit);
}
代碼一長串,但是標(biāo)了注釋,應(yīng)該大致能看明白,大概講一下:
函數(shù)的主體是一個do, while循環(huán)
,用一個變量retVal,來控制循環(huán)的執(zhí)行
。默認為0,無限循環(huán)
。
剛進入循環(huán)1,2,3,4,5在做一件事,就是檢查是否有事件需要處理,如果有的話,直接跳到9去處理事件。
處理完事件之后,到第10行,會去判斷4種是否應(yīng)該跳出循環(huán)的情況,給出變量retVal賦一個不為0的值,來跳出循環(huán)。
如果走到6,則說明沒有事情做,那么runloop就睡眠了,停在第7行,這一行類似sync這樣的同步機制(其實不是,只是舉個例子。。),把程序阻塞在這一行,直到有消息返回值,才繼續(xù)往下進行。這一阻塞操作是系統(tǒng)內(nèi)核掛起來的,阻塞了當(dāng)前的線程,當(dāng)有消息返回時,因為當(dāng)前線程是被阻塞的,系統(tǒng)內(nèi)核會再開辟一條新的線程去返回這個消息。然后程序繼續(xù)往下進行。
走到第8、9,通知Observers,然后處理事件。
到10,去判斷是否退出循環(huán)的條件,如果滿足條件退出循環(huán),runloop結(jié)束。反之,又從新開始循環(huán),從2開始。
這就是一個完整的runloop處理事件的流程。
回到上述的例子這種模式下的runloop:
- (BOOL) runMode:(NSString *)mode beforeDate:(NSDate *)limitDate;
我們讓線程執(zhí)行了一個事件,結(jié)果執(zhí)行完,runloop就退出了,原因是這樣的:
if (sourceHandledThisLoop && stopAfterHandle) {
/// 進入loop時參數(shù)處理完畢事件就返回
retVal = kCFRunLoopHandledSource;
}
這種形式開啟的runloop, stopAfterHandle這個參數(shù)為YES,而sourceHandledThisLoop這個參數(shù)在如下代碼中被賦值為YES:
/// 10.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);
}
}
所以在這里我們觸發(fā)了事件之后,runloop被退出了,這個時候我們也明白了為什么timer并不會導(dǎo)致runloop的退出。
接下來我們分析一下Core Foundation中運行的runloop的接口:
/// 運行CFRunLoopRef
void CFRunLoopRun()
/// 運行CFRunLoopRef: 參數(shù)為運行模式、時間和是否在處理Input Source后退出標(biāo)示,返回值是exit原因
SInt32 CFRunLoopRunInMode (mode, seconds, returnAfterSourceHandled);
/// 停止運行 CFRunLoopRef
void CFRunLoopStop(CFRunLoopRef rl);
/// 喚醒 CFRunLoopRef
void CFRunLoopWakeUp(CFRunLoopRef rl);
一
void CFRunLoopRun();
運行在默認的kCFRunLoopDefaultMode模式下,直到CFRunLoopStop接口調(diào)用停止這個RunLoop,或者RunLoop的所有事件源被刪除。
NSRunLoop是基于CFRunLoop來封裝的,NSRunLoop是線程不安全的
,而CFRunLoop是線程安全的
。
在這里我們可以看到和上面NSRunLoop有一個直觀的區(qū)別是:CFRunLoop能直接停止掉所有的CFRunLoop運行起來的runloop
,其實之前講到的:
- (BOOL)runMode:(NSString *)mode beforeDate:(NSDate *)limitDate;
這種方式運行起來的runloop也能用CFRunLoopStop 停止掉的,原因是它完全是基于下面這種方式封裝的:
SInt32 CFRunLoopRunInMode(mode, seconds, returnAfterSourceHandled);
可以看到參數(shù)幾乎一模一樣,前者默認returnAfterSourceHandled參數(shù)為YES,當(dāng)觸發(fā)一個非timer事件后,runloop就終止了。
這里比較簡單,就不舉例贅述了。
二
SInt32 CFRunLoopRunInMode(mode, seconds, returnAfterSourceHandled);
這里有3個參數(shù),1個返回值
其中第一個參數(shù)
是指RunLoop運行的模式
(例如kCFRunLoopDefaultMode或者kCFRunLoopCommonModes),第二個參數(shù)
是運行事件
,第三個參數(shù)
是是否在處理事件后讓Run Loop退出返回
,NSRunLoop的第三種開啟runloop的方法,綜上所述,我們知道,實際上就是設(shè)置stopAfterHande這個參數(shù)為YES
關(guān)于返回值,我們知道調(diào)用runloop運行,代碼是停在這一行不返回的,當(dāng)返回的時候runloop就結(jié)束了,所以這個返回值就是runloop結(jié)束的原因,為一個枚舉值,具體原因如下:
enum {
kCFRunLoopRunFinished = 1, // Run Loop結(jié)束,沒有Timer或者其他Input Source
kCFRunLoopRunStopped = 2, // Run Loop被停止,使用CFRunLoopStop停止Run Loop
kCFRunLoopRunTimedOut = 3, // Run Loop超時
kCFRunLoopRunHandledSource = 4, // Run Loop處理完事件,注意Timer事件的觸發(fā)是不會讓Run Loop退出返回的,即使CFRunLoopRunInMode的第三個參數(shù)是YES也不行
}
看到這,我們發(fā)現(xiàn)我們忽略了NSRunLoop第三種開啟方式的返回值。
- (Bool)runMode:(NSString *)mode beforeDate:(NSDate *)limitDate;
它其實就是基于CFRunLoopRunInMode封裝的,它的返回值為一個Bool值,如果是PerformSelector *事件或者其他Input Source事件觸發(fā)處理后,Run Loop會退出返回YES, 其他返回NO。
舉個例子:
- (void)testDemo2
{
dispatch_async(dispatch_get_global_queue(0, 0), ^{
NSLog(@"starting thread.......");
NSTimer *timer = [NSTimer timerWithTimeInterval:1 target:self selector:@selector(doTimerTask1:) userInfo:remotePort repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
//最后一個參數(shù),是否處理完事件返回,結(jié)束runLoop
SInt32 result = CFRunLoopRunInMode(kCFRunLoopDefaultMode, 100, YES);
/*
kCFRunLoopRunFinished = 1, //Run Loop結(jié)束,沒有Timer或者其他Input Source
kCFRunLoopRunStopped = 2, //Run Loop被停止,使用CFRunLoopStop停止Run Loop
kCFRunLoopRunTimedOut = 3, //Run Loop超時
kCFRunLoopRunHandledSource = 4 Run Loop處理完事件,注意Timer事件的觸發(fā)是不會讓Run Loop退出返回的,即使CFRunLoopRunInMode的第三個參數(shù)是YES也不行
*/
switch (result) {
case kCFRunLoopRunFinished:
NSLog(@"kCFRunLoopRunFinished");
break;
case kCFRunLoopRunStopped:
NSLog(@"kCFRunLoopRunStopped");
case kCFRunLoopRunTimedOut:
NSLog(@"kCFRunLoopRunTimedOut");
case kCFRunLoopRunHandledSource:
NSLog(@"kCFRunLoopRunHandledSource");
default:
break;
}
NSLog(@"end thread.......");
});
}
- (void)doTimerTask1:(NSTimer *)timer
{
count++;
if (count == 2) {
[timer invalidate];
}
NSLog(@"do timer task count:%d",count);
}
2016-11-23 09:19:28.342 TestRunloop3[88598:1971412] starting thread.......
2016-11-23 09:19:29.347 TestRunloop3[88598:1971412] do timer task count:1
2016-11-23 09:19:30.345 TestRunloop3[88598:1971412] do timer task count:2
2016-11-23 09:19:30.348 TestRunloop3[88598:1971412] kCFRunLoopRunFinished
2016-11-23 09:19:30.348 TestRunloop3[88598:1971412] end thread.......
很清楚的可以看到,當(dāng)timer被置無效的時候,runloop里面沒有了任何事件源,所以退出了,退出原因為:kCFRunLoopRunFinished,線程也就結(jié)束了。
總結(jié)一下:
runloop的運行方法一共有5種:包括NSRunLoop的3種,CFRunLoop的2種;
而取消的方式一共為3種:
1)移除掉runloop種的所有事件源(timer和source)。
2)設(shè)置一個超時時間。
3)只要CFRunLoop運行起來就可以用:void CFRunLoopStop(CFRunLoopRef rl); 去停止。
除此之外用 NSRunLoop也能使用void CFRunLoopStop(CFRunLoopRef rl);
去停止:
[NSRunLoop currentRunLoop] runMode:<#(nonull NSRunLoopMode)#> beforeDate:<#(nonull NSDate)#>
實現(xiàn)過程中,可以根據(jù)需求,我們可以設(shè)置一個自己的Bool值,來控制runloop的開始與停止,類似下面這樣:
while(!cancel) {
CFRunLoopRunInMode(kCFRunLoopDefaultMode, 1, YES);
}
每次runloop只運行1秒就停止,然后開始下一次runloop。
這里最后一個參數(shù)設(shè)置為YES,當(dāng)有非timer事件進來,也會立即開始下一次runloop。
當(dāng)然每次進來我們都可以去修改Mode的值,這樣我們可以讓runloop每次都運行在不同的模式下。
當(dāng)我們不需要runloop的時候,可以直接將cancel設(shè)置為YES即可。
當(dāng)然,這里只是提供一個思路,具體需求,可以根據(jù)實際需要進行處理。
基于runloop的線程通信
首先明確一個概念,線程間的通信(不僅限于通信,幾乎所有iOS事件都是如此),實際上是各種輸入源,觸發(fā)runloop去處理對應(yīng)的事件,所以我們先來講講輸入源:
輸入源異步的發(fā)送消息給你的線程。事件來源取決于輸入源的種類:
- 基于端口的輸入源和自定義輸入源。
基于端口的輸入源
監(jiān)聽程序相應(yīng)的端口。自定義輸入源
則監(jiān)聽自定義的事件源。
當(dāng)你創(chuàng)建輸入源
,你需要將其分配給run loop中的一個或多個模式
。模式只會在特定事件影響監(jiān)聽的源。大多數(shù)情況下,runloop運行在默認模式下,但是你也可以使其運行在自定義模式。輸入源生成的消息只在run loop運行在其關(guān)聯(lián)的模式下才會被傳遞
。
1)基于端口的輸入源:
在runloop中,被定義名為source1。Cocoa和Core Foundation內(nèi)置支持使用端口相關(guān)的對象和函數(shù)來創(chuàng)建的基于端口對象,并使用NSPort的方法把端口添加到runloop。端口對象會自己處理創(chuàng)建和配置輸入源。
在Core Foundation,你必須人工創(chuàng)建端口和它的run loop源。在兩種情況下,你都可以使用端口相關(guān)的函數(shù)(CFMachPortRef, CFMessagePortRef, CFSockerRef)來創(chuàng)建合適的對象。
這里用Cocoa里的舉個例子,Cocoa里用來線程傳值的NSMachPort,它的父類是NSPort。
首先我們看下面:
NSPort *port1 = [[NSPort alloc]init];
NSPort *port2 = [[NSMachPort alloc]init];
NSPort *port3 = [NSPort port];
NSPort *port4 = [NSMachPort port];
我們可以打斷點看到如下:
發(fā)現(xiàn)我們怎么創(chuàng)建,都返回給我們的是NSMachPort的實例,這應(yīng)該是NSProt內(nèi)部做了一個消息的轉(zhuǎn)發(fā),這就有點像是一個抽象類,它本身只是定義一些公有屬性和方法,然后利用集成它的子類去實現(xiàn)(只是我個人猜測。。)
繼續(xù)看我們寫的一個利用NSMachPort來線程通信的實例:
- (void)testDemo3
{
//聲明兩個端口 隨便怎么寫創(chuàng)建方法,返回的總是一個NSMachPort實例
NSMachPort *mainPort = [[NSMachPort alloc]init];
NSPort *threadPort = [NSMachPort port];
//設(shè)置線程的端口的代理回調(diào)為自己
threadPort.delegate = self;
//給主線程runloop加一個端口
[[NSRunLoop currentRunLoop]addPort:mainPort forMode:NSDefaultRunLoopMode];
dispatch_async(dispatch_get_global_queue(0, 0), ^{
//添加一個Port
[[NSRunLoop currentRunLoop]addPort:threadPort forMode:NSDefaultRunLoopMode];
[[NSRunLoop currentRunLoop]runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
});
NSString *s1 = @"hello";
NSData *data = [s1 dataUsingEncoding:NSUTF8StringEncoding];
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
NSMutableArray *array = [NSMutableArray arrayWithArray:@[mainPort,data]];
//過2秒向threadPort發(fā)送一條消息,第一個參數(shù):發(fā)送時間。msgid 消息標(biāo)識。
//components,發(fā)送消息附帶參數(shù)。reserved:為頭部預(yù)留的字節(jié)數(shù)(從官方文檔上看到的,猜測可能是類似請求頭的東西...)
[threadPort sendBeforeDate:[NSDate date] msgid:1000 components:array from:mainPort reserved:0];
});
}
//這個NSMachPort收到消息的回調(diào),注意這個參數(shù),可以先給一個id。如果用文檔里的NSPortMessage會發(fā)現(xiàn)無法取值
- (void)handlePortMessage:(id)message
{
NSLog(@"收到消息了,線程為:%@",[NSThread currentThread]);
//只能用KVC的方式取值
NSArray *array = [message valueForKeyPath:@"components"];
NSData *data = array[1];
NSString *s1 = [[NSString alloc]initWithData:data encoding:NSUTF8StringEncoding];
NSLog(@"%@",s1);
// NSMachPort *localPort = [message valueForKeyPath:@"localPort"];
// NSMachPort *remotePort = [message valueForKeyPath:@"remotePort"];
}
2016-11-23 16:50:20.604 TestRunloop3[1322:120162] 收到消息了,線程為:<NSThread: 0x60800026d700>{number = 3, name = (null)}
2016-11-23 16:50:26.551 TestRunloop3[1322:120162] hello
我們跨越線程,確實從主線程往另外一個線程發(fā)送了消息。
這里我們要注意幾個點:
1)- (void)handlePortMessage:(id)message 這里這個代理的參數(shù),從.h中去復(fù)制過來的為NSPortMessage類型的一個對象,但是我們發(fā)現(xiàn)蘋果只是在.h中@class過來,我們無法調(diào)用它的任何方法。所以我們用id聲明,然后通過kvc去取它的屬性。
2)關(guān)于下面這個傳值類型的問題:
NSMutableArray *array = [NSMutableArray arrayWithArray: @[mainPort, data]];
在此我困惑了好一會兒。。之前我是往數(shù)組里添加的是String或者其他類型的對象,但是發(fā)現(xiàn)參數(shù)傳過去之后,變成了nil。于是查了半天資料,依然沒有結(jié)果。于是翻看官方文檔,終于在方法描述里看到了(其實很醒目,然后作者英文水平有限。。)
The components array consists of a series of instances
of some subclass of NSData, and instances of some
subclass of NSPort; since one subclass of NSPort does
not necessarily know how to transport an instance of
another subclass of NSPort (or could do it even if it
knew about the other subclass), all of the instances
of NSPort in the components array and the ‘receivePort’
argument MUST be of the same subclass of NSPort that
receives this message. If multiple DO transports are
being used in the same program, this requires some care.
從這段描述中我們可以看出,這個傳參數(shù)組里面只能裝兩種類型的數(shù)據(jù),一種是NSPort的子類,一種是NSData的子類。所有我們?nèi)绻眠@種方式傳值必須得先把數(shù)據(jù)轉(zhuǎn)成NSData類型的才行。
2)Cocoa 執(zhí)行Selector的源
除了基于端口的源,Cocoa定義了自定義輸入源,允許你在任何線程執(zhí)行selector。它被稱為source0,和基于端口的源一樣,執(zhí)行selector請求會在目標(biāo)線程上序列化,減緩許多在線程上允許多個方法容易引起的同步問題。不像基于源的端口,一個selector執(zhí)行完后會自動從run loop里面移除
[self performSelectorOnMainThread:<#(nonnull SEL)#> withObject:<#(nullable id)#> waitUntilDone:<#(BOOL)#>]
[self performSelectorOnMainThread:<#(nonnull SEL)#> withObject:<#(nullable id)#> waitUntilDone:<#(BOOL)#> modes:<#(nullable NSArray<NSString *> *)#>]
[self performSelector:<#(nonnull SEL)#> onThread:<#(nonnull NSThread *)#> withObject:<#(nullable id)#> waitUntilDone:<#(BOOL)#>]
[self performSelector:<#(nonnull SEL)#> onThread:<#(nonnull NSThread *)#> withObject:<#(nullable id)#> waitUntilDone:<#(BOOL)#> modes:<#(nullable NSArray<NSString *> *)#>]
這四個方法很類似,一個是在主線程去掉,一個可以指定一個線程。然后一個帶Mode,一個不帶。
大概講一下 waitUntilDone
這個參數(shù),顧名思義,就是是否等到結(jié)束。
1)如果這個值設(shè)為YES,那么就需要等到這個方法執(zhí)行完,線程才能繼續(xù)往下去執(zhí)行。它會阻塞提交的線程
。
2)如果為NO的話,這個調(diào)用的方法會異步的實行,不會阻塞提交的線程
。
3)自定義輸入源
:
為了自定義輸入源,必須使用Core Foundation里面的CGRunLoopSourceRef類型相關(guān)的函數(shù)來創(chuàng)建
。你可以使用回調(diào)函數(shù)來配置自定義輸入源。CoreFoundation會在配置源的不同地方調(diào)用回調(diào)函數(shù),處理輸入時間,在源從runloop移除的時候清理它。除了定義在事件到達時自定義輸入源的行為,你也必須定義消息傳遞機制。源的這部分運行在單獨的線程里面,并負責(zé)在數(shù)據(jù)等待處理的時候?qū)?shù)據(jù)傳遞給源并通知它處理數(shù)據(jù)。消息傳遞機制的定義取決于你,但是最好不要過于復(fù)雜。
創(chuàng)建自定義的輸入源包括定義下面內(nèi)容:
1.輸入源要處理的信息;
2.使感興趣的客戶端知道如何和輸入源交互的調(diào)度歷程;
3.處理其他任何客戶端發(fā)送請求的歷程;
4.使輸入源失效的取消歷程。
由于創(chuàng)建輸入源來處理自定義消息,實際配置項是靈活的。調(diào)度歷程,處理歷程和取消歷程都是創(chuàng)建自定義輸入源的關(guān)鍵歷程。輸入源其他的大部分行為都發(fā)生在這些歷程的外部。比如,由你決定數(shù)據(jù)傳輸?shù)捷斎朐吹臋C制,還有輸入源的其他線程的通信機制也是由你決定。
下圖中,程序的主線程維護了一個輸入源的引用,輸入源所需的自定義命令緩沖區(qū)和輸入源所在的runloop
。當(dāng)主線程有任務(wù)需要分發(fā)給工作線程時候,主線程會給命令緩沖區(qū)發(fā)送命令和必須的信息來通知工作線程開始執(zhí)行任務(wù).(因為主線程和輸入源所在工作線程都是可以訪問命令緩沖區(qū)的,因此這些方法必須是同步的),一旦命令發(fā)送出去,主線程會通知輸入源并喚醒工作線程的runloop。而一收到喚醒命令,runloop會調(diào)用輸入源的處理程序,由它來執(zhí)行命令緩沖區(qū)的響應(yīng)命令。
這樣一來,我們來寫一個實例來講講自定義的輸入源(自定義的輸入源,用CF來實現(xiàn)):
CFRunLoopRef _runLoopRef;
CFRunLoopSourceRef _source;
CFRunLoopSourceContext _source_context;
首先我們聲明3個成員變量,這是我們自定義輸入源所需要的3個參數(shù)。具體我們舉例之后再說:
- (void)testDemo4
{
dispatch_async(dispatch_get_global_queue(0, 0), ^{
NSLog(@"starting thread.......");
_runLoopRef = CFRunLoopGetCurrent();
//初始化_source_context。
bzero(&_source_context, sizeof(_source_context));
//這里創(chuàng)建了一個基于事件的源,綁定了一個函數(shù)
_source_context.perform = fire;
//參數(shù)
_source_context.info = "hello";
//創(chuàng)建一個source
_source = CFRunLoopSourceCreate(NULL, 0, &_source_context);
//將source添加到當(dāng)前RunLoop中去
CFRunLoopAddSource(_runLoopRef, _source, kCFRunLoopDefaultMode);
//開啟runloop 第三個參數(shù)設(shè)置為YES,執(zhí)行完一次事件后返回
CFRunLoopRunInMode(kCFRunLoopDefaultMode, 9999999, YES);
NSLog(@"end thread.......");
});
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
if (CFRunLoopIsWaiting(_runLoopRef)) {
NSLog(@"RunLoop 正在等待事件輸入");
//添加輸入事件
CFRunLoopSourceSignal(_source);
//喚醒線程,線程喚醒后發(fā)現(xiàn)由事件需要處理,于是立即處理事件
CFRunLoopWakeUp(_runLoopRef);
}else {
NSLog(@"RunLoop 正在處理事件");
//添加輸入事件,當(dāng)前正在處理一個事件,當(dāng)前事件處理完成后,立即處理當(dāng)前新輸入的事件
CFRunLoopSourceSignal(_source);
}
});
}
//此輸入源需要處理的后臺事件
static void fire(void* info){
NSLog(@"我現(xiàn)在正在處理后臺任務(wù)");
printf("%s",info);
}
輸出結(jié)果如下:
2016-11-24 10:42:24.045 TestRunloop3[4683:238183] starting thread.......
2016-11-24 10:42:26.045 TestRunloop3[4683:238082] RunLoop 正在等待事件輸入
2016-11-24 10:42:31.663 TestRunloop3[4683:238183] 我現(xiàn)在正在處理后臺任務(wù)
hello
2016-11-24 10:42:31.663 TestRunloop3[4683:238183] end thread.......
例中可見我們創(chuàng)建一個自定義的輸入源,綁定了一個函數(shù),一個參數(shù),并且這個輸入源,實現(xiàn)了線程間的通信.
大概說一下:
a)CFRunLoopRef _runLoopRef;CF的runLoop。
b)CFRunLoopSourceContext _source_context; 注意到例中用了一個C函數(shù)bzero(&_source_context, sizeof(_source_context)); 來初始化。其實它的本質(zhì)是一個結(jié)構(gòu)體:
typedef struct {
CFIndex version;
void * info;
const void *(*retain)(const void *info);
void (*release)(const void *info);
CFStringRef (*copyDescription)(const void *info);
Boolean (*equal)(const void *info1, const void *info2);
CFHashCode (*hash)(const void *info);
void (*schedule)(void *info, CFRunLoopRef rl, CFRunLoopMode mode);
void (*cancel)(void *info, CFRunLoopRef rl, CFRunLoopMode mode);
void (*perform)(void *info);
} CFRunLoopSourceContext;
所以bzero(&_source_context, sizeof(_source_context)); 這個函數(shù)其實就是把所有的內(nèi)容先置為0。
我們在這里綁定了兩個參數(shù)一個是signal觸發(fā)的函數(shù),一個是函數(shù)的參數(shù),至于其他參數(shù)的用途,可以看看蘋果官方文檔的說明:
version
Version number of the structure. Must be 0.
info
An arbitrary pointer to program-defined data, which can be associated with the CFRunLoopSource at creation time. This pointer is passed to all the callbacks defined in the context.
retain
A retain callback for your program-defined info pointer. Can be NULL.
release
A release callback for your program-defined info pointer. Can be NULL.
copyDescription
A copy description callback for your program-defined info pointer. Can be NULL.
equal
An equality test callback for your program-defined info pointer. Can be NULL.
hash
A hash calculation callback for your program-defined info pointer. Can be NULL.
schedule
A scheduling callback for the run loop source. This callback is called when the source is added to a run loop mode. Can be NULL.
cancel
A cancel callback for the run loop source. This callback is called when the source is removed from a run loop mode. Can be NULL.
perform
A perform callback for the run loop source. This callback is called when the source has fired.
c) CFRunLoopSourceRef _source;
這個是自定義輸入源中最重要的一個參數(shù)
。它用來連接runloop和CFRunLoopSourceContext的一些配置選項
,注意我們自定義的輸入源,必須由我們手動觸發(fā)
。需要先CFRunLoopSourceSignal(_source); 再看當(dāng)前runloop是否在休眠中,來看是否需要調(diào)用CFRunLoopWakeUp(_runLoopRef); (一般都是要調(diào)用的)。
4)定時源:
定時源在預(yù)設(shè)的時間點以同步方式傳遞消息。定時器是線程通知自己做某事的一種方法。
盡管定時器可以產(chǎn)生基于時間的通知,但他并不是實時機制。和輸入源一樣,定時器也和runloop的特點模式相關(guān)。如果定時器所在的模式當(dāng)前未被runloop監(jiān)視,那么定時器將不會知道runloop運行在響應(yīng)的模式下。類似的,如果定時器在runloop處理某一事件期間,定時器會一直等待直到下次runloop開始響應(yīng)再處理程序,如果runloop不運行了,那么定時器也永遠不啟動。
配置定時源:
Cocoa中可以使用以下NSTimer類方法來創(chuàng)建一個定時器:
[NSTimer scheduledTimerWithTimeInterval:<#(NSTimeInterval)#> target:<#(nonnull id)#> selector:<#(nonnull SEL)#> userInfo:<#(nullable id)#> repeats:<#(BOOL)#>
[NSTimer timerWithTimeInterval:<#(NSTimeInterval)#> target:<#(nonnull id)#> selector:<#(nonnull SEL)#> userInfo:<#(nullable id)#> repeats:<#(BOOL)#>]
當(dāng)前還有Block,invocation的形式,就不做贅述了。
第一種timer默認是加到NSDefaultRunLoopMode模式下
。
第二種timer沒有默認值,我們使用的時候必須調(diào)用 [[NSRunLoop currentRunLoop] addTimer: timer forMode: NSDefaultRunLoopMode]; 去給它指定一個mode
。
Core Foundation 創(chuàng)建定時器
CFRunLoopRef runLoop = CFRunLoopGetCurrent();
CFRunLoopTimerContext context = {0, NULL, NULL, NULL, NULL};
CFRunLoopTimerRef timer = CFRunLoopTimerCreate(kCFAllocatorDefault, 0.1, 0.3, 0, 0, &myCFTimeCallback, &context);
最后用一張runloop運行時的流程圖來梳理一下我們這些源觸發(fā)的順序
如圖所示,首先我要明確一個知識點:runloop跑一圈,只能執(zhí)行一個事件
。
timer和source0進入runloop中,都只是通知Observer我要處理,但是還是會有6、7、8睡眠喚醒這一步。但是source1如果有,就會直接跳到第9步執(zhí)行。
我們前面也講過第7步,這里再提一下。它一直阻塞在這一列,直到:
1)a.source1來了。b.定時器Timer啟動。c.runloop超時。d.runloop被顯式喚醒CFRunLoopWakeUp(runloop)(也就是source0)來了。
2)這里大家就奇怪了,之前不是說source1有的話就直接跳到第9步去執(zhí)行了嗎?。但是仔細想想,如果runloop正處于睡眠狀態(tài)下,這個時候source1來了,是不是也需要喚醒runloop~
3)至于其他的,應(yīng)該不難理解了。
Run Loop的Observer
上圖提到了Observer:
Core Foundaton層的接口可以定義一個Run Loop的觀察,在Run Loop進入下一個狀態(tài)時得到通知:
Run loop進入
Run loop處理一個Timer的時刻
Run loop處理一個Input Source的時刻
Run loop進入睡眠的時刻
Run loop被喚醒的時刻,但在喚醒它的事件被處理之前
Run loop的終止
Observer的創(chuàng)建以及添加到Run Loop中需要使用Core Foundation的接口:
方法很簡單如下:
// 創(chuàng)建observer
CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(CFAllocatorGetDefault(), kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
});
// 添加觀察者:監(jiān)聽RunLoop的狀態(tài)
CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopDefaultMode);
// 釋放Observer
CFRelease(observer);
1)方法就是創(chuàng)建一個observer,綁定一個runloop和模式,而block回調(diào)就是監(jiān)聽到runloop每種狀態(tài)的時候回觸發(fā)。
2)其中CFRunLoopActivity是一枚舉值,與每種狀態(tài)對應(yīng):
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
kCFRunLoopEntry = (1UL << 0), // 1 // 即將進入Loop
kCFRunLoopBeforeTimers = (1UL << 1), // 2 // 即將處理 Timer
kCFRunLoopBeforeSources = (1UL << 2), // 4 即將處理 Source
kCFRunLoopBeforeWaiting = (1UL << 5), // 32 // 即將進入休眠
kCFRunLoopAfterWaiting = (1UL << 6), // 64
// 剛從休眠中喚醒
kCFRunLoopExit = (1UL << 7), // 128 // 即將退出Loop
kCFRunLoopAllActivities = 0x0FFFFFFFU // 可以監(jiān)聽以上所有狀態(tài)
};