這是AF2.x經(jīng)典的代碼:
+ (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];
}
}
- 首先我們要明確一個概念,線程一般都是一次執(zhí)行完任務(wù),就銷毀了。
- 而添加了runloop,并運行起來,實際上是添加了一個 do-while 循環(huán),這樣這個線程的程序一直卡在這個 do-while循環(huán)上,這樣相當于線程的任務(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:<#(nonnull NSRunLoopMode)#>```
只有從runloop中移除我們之前添加的端口,這樣runloop沒有任何事件,所以直接退出。
再次回到 AF2.x 的這行源碼上,因為他用的是run,而且并沒有記錄下自己添加的NSMachPort,所以顯然,他就沒打算退出這個runloop,**這是一個常駐線程**。事實上,看過AF2.x源碼的同學會知道,這個thread需要常駐的原因,在此就不在贅述了。
##### 我們看看AF3.x是怎么用runloop的:
需要開啟的時候:
CFRunLoopRun();
終止的時候:
CFRunLoopStop(CFRunLoopGetCurrent());
- 由于NSUrlSession參考了AF的2.x的優(yōu)點,自己維護了一個線程池,做Request線程的調(diào)度與管理,所以在AF3.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),當有事做時做事,沒事做時睡眠。**至于怎么做事,怎么睡眠,這個是由內(nèi)核來調(diào)度的,我們后面會講到。
- 每一個線程都有一個RunLoop,主線程的runloop會在app運行時自動運行,子線程中需要手動獲取運行,第一次獲取時,才會去創(chuàng)建。
- 每個Runloop都會以一個模式mode來運行,可以使用NSRunloop的方法運行在某個特定模式mode。
- Runloop的處理兩大類事件源:Timer Source和Input Source(包括performSelector***方法簇、Port或者自定義Input Source),每個事件源都會綁定在Runloop的某個特定模式mode上,而且只有Runloop在這個模式運行的時候才會觸發(fā)該Timer和Input Source。
- 最后,如果沒有任何事件源添加到Runloop上,Runloop就會立刻exit,這也是一開始的AF例子,為什么需要綁定一個port的原因。
#### 我們先來談?wù)凴unloop Mode
iOS下Runloop的主要運行模式mode有:
1)NSDefaultRunLoopMode:默認的運行模式,除了NSConnection對象的事件。
2)NSRunLoopCommonModes:是一組常用的模式集合,將一個input source關(guān)聯(lián)到這個模式集合上,等于將input source關(guān)聯(lián)到這個模式集合中的所有模式上。在iOS系統(tǒng)中NSRunLoopCommonMode包含NSDefaultRunLoopMode、NSTaskDeathCheckMode。
- 假如我有一個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ā)。
- 當然,默認情況下NSRunLoopCommonModes包含了NSDefaultRunLoopMode和UITrackingRunLoopMode。我指的是如果有其他自定義Mode。
- 注意:讓Runloop運行在NSRunLoopCommonModes下是沒有意義的,因為一個時刻Runloop只能運行在一個特定模式下,而不可能是個模式集合。
3) UITrackingRunLoopMode: 用于跟蹤觸摸事件觸發(fā)的模式(例如UIScrollView上下滾動),主線程當觸摸事件觸發(fā)時會設(shè)置為這個模式,可以用來在控件事件觸發(fā)過程中設(shè)置Timer。
4) GSEventReceiveRunLoopMode: 用于接受系統(tǒng)事件,屬于內(nèi)部的Run Loop模式。
5) 自定義Mode:可以設(shè)置自定義的運行模式Mode,你也可以用CFRunLoopAddCommonMode添加到NSRunLoopCommonModes中。
##### 總結(jié)一下:
**Run Loop運行時只能以一種固定的模式運行,如果我們需要它切換模式,只有停掉它,再重新開啟它。**運行時它只會監(jiān)控這個模式下添加的Timer Source和Input Source,如果這個模式下沒有相應(yīng)的事件源,Run Loop的運行也會立刻返回的。注意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;
- 兩組接口差不多,不過功能上還是有許多區(qū)別的:
例如CF層可以添加自定義Input Source事件源、(CFRunLoopSourceRef)Run Loop觀察者Observer(CFRunLoopObserverRef),很多類似功能的接口特性也是不一樣的。
## NSRunLoop的運行接口:
//運行 NSRunLoop,運行模式為默認的NSDefaultRunLoopMode模式,沒有超時限制
- (void)run;
//運行 NSRunLoop: 參數(shù)為運時間期限,運行模式為默認的NSDefaultRunLoopMode模式
- (void)runUntilDate:(NSDate *)limitDate;
//運行 NSRunLoop: 參數(shù)為運行模式、時間期限,返回值為YES表示是處理事件后返回的,NO表示是超時或者停止運行導致返回的
- (BOOL)runMode:(NSString *)mode beforeDate:(NSDate *)limitDate;
##### 首先,第一個
- (void)run; 無條件運行
- 不建議使用,因為這個接口會導致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ā)以外的事件,都會導致runloop退出,這里簡單的舉個例子:
-
(void)testDemo1
{
dispatch_async(dispatch_get_global_queue(0, 0), ^{
NSLog(@"線程開始");
//獲取到當前線程
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]);
}
- (void)recieveMsg
輸出結(jié)果如下:
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ù)標記的超時時間了
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);
}
代碼一長串,但是標了注釋,應(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行,這一行
__CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort)
{
// thread wait for receive msg
mach_msg(msg, MACH_RCV_MSG, port);
}
這一行類似sync這樣的一個同步機制(其實不是,舉個例子。。),把程序阻塞在這一行,直到有消息返回值,才繼續(xù)往下進行。**這一阻塞操作是系統(tǒng)內(nèi)核來掛起的,阻塞了當前的線程**,當有消息返回時,**因為當前線程是被阻塞的,系統(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 = kCFRunLoopRunHandledSource;
}
- 這種形式開啟的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并不會導致runloop的退出。
## CFRunLoopRef的運行接口:
//運行 CFRunLoopRef
void CFRunLoopRun();
//運行 CFRunLoopRef: 參數(shù)為運行模式、時間和是否在處理Input Source后退出標志,返回值是exit原因
SInt32 CFRunLoopRunInMode (mode, seconds, returnAfterSourceHandled);
//停止運行 CFRunLoopRef
void CFRunLoopStop( CFRunLoopRef rl );
//喚醒CFRunLoopRef
void CFRunLoopWakeUp ( CFRunLoopRef rl );
- 我們一個一個來看
##### 第一個
void CFRunLoopRun();
- 運行在默認的kCFRunLoopDefaultMode模式下,直到使用**CFRunLoopStop**接口停止這個Run Loop,或者Run Loop的所有事件源都被刪除。
- NSRunloop是基于CFRunloop來封裝的,NSRunloop是線程不安全的,而CFRunloop則是線程安全的。
- 在這里我們可以看到和上面NSRunloop有一個直觀的區(qū)別就是,**CFRunLoopStop能直接停止掉所有用CFRunloop運行起runloop**,其實之前講到的:
- (void)runUntilDate:(NSDate *)limitDate; 有一個超時時間限制
這方式運行的runloop也能用`CFRunLoopStop`
停止掉的原因它是完全基于下面這種方式封裝的:
SInt32 CFRunLoopRunInMode (mode, seconds, returnAfterSourceHandled);
可以看到參數(shù)幾乎一模一樣,前者默認returnAfterSourceHandled參數(shù)為YES,當觸發(fā)一個非timer事件后,runloop就終止了。
##### 第二個
SInt32 CFRunLoopRunInMode (mode, seconds, returnAfterSourceHandled);
- 這里有3個參數(shù),1個返回值。
- 其中第一個參數(shù)是指RunLoop運行的模式(例如kCFRunLoopDefaultMode或者kCFRunLoopCommonModes),第二個參數(shù)是運行時間,第三個參數(shù)是是否在處理事件后讓Run Loop退出返回,NSRunloop的第三種開啟runloop的方法,綜上述,我們知道,實際上就是設(shè)置**stopAfterHandle這個參數(shù)為YES**
- 關(guān)于返回值,我們知道調(diào)用runloop運行,代碼是停在這一行不返回的,當返回的時候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值,如果是PerfromSelector事件或者其他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);
}
- (void)doTimerTask1:(NSTimer *)timer
輸出結(jié)果如下:
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.......
- 很清楚的可以看到,當timer被置無效的時候,runloop里面沒有了任何的事件源,所以退出了,退出原因為:**kCFRunLoopRunFinished**,線程也就結(jié)束了。
#### 總結(jié)一下:
- runloop運行方法一共5種:包括NSRunloop的3種,CFRunloop的兩種;
而取消的方式一共為3種:
1)移除掉runloop中的所有事件源(timer和source)。
2)設(shè)置一個超時時間。
3)只要CFRunloop運行起來就可以用:void CFRunLoopStop( CFRunLoopRef rl );去停止。
- 除此之外用NSRunLoop下面這個方法運行也能使用void CFRunLoopStop( CFRunLoopRef rl );停止:
[NSRunLoop currentRunLoop]runMode:<#(nonnull NSRunLoopMode)#> beforeDate:<#(nonnull NSDate *)#>
- 實際過程中,可以根據(jù)需求,我們可以設(shè)置一個自己的Bool值,來控制runloop的開始與停止,類似下面這樣:
while (!cancel) {
CFRunLoopRunInMode(kCFRunLoopDefaultMode, 1, YES);
}
- 每次runloop只運行1秒就停止,然后開始下一次的runloop。
- 這里最后一個參數(shù)設(shè)置為YES,當有非timer事件進來,也會立即開始下一次runloop。
- 當然每次進來我們都可以去修改Mode的值,這樣我們可以讓runloop每次都運行在不同的模式下。
- 當我們不需要runloop的時候,直接將cancel置為YES即可。
當然,這里只是提供一個思路,具體有需求,可以根據(jù)實際需要。
#### 基于runloop的線程通信
首先明確一個概念,線程間的通信(不僅限于通信,幾乎所有iOS事件都是如此),實際上是各種輸入源,觸發(fā)runloop去處理對應(yīng)的事件,所以我們先來講講輸入源:
輸入源異步的發(fā)送消息給你的線程。事件來源取決于輸入源的種類:
- 基于端口的輸入源和自定義輸入源。基于端口的輸入源監(jiān)聽程序相應(yīng)的端口。自定義輸入源則監(jiān)聽自定義的事件源。
至于run loop,它不關(guān)心輸入源的是基于端口的輸入源還是自定義的輸入源。系統(tǒng)會實現(xiàn)兩種輸入源供你使用。兩類輸入源的區(qū)別在于:
- 基于端口的輸入源由內(nèi)核自動發(fā)送,而自定義的則需要人工從其他線程發(fā)送。
當你創(chuàng)建輸入源,你需要將其分配給run loop中的一個或多個模式。模式只會在特定事件影響監(jiān)聽的源。大多數(shù)情況下,run loop運行在默認模式下,但是你也可以使其運行在自定義模式。若某一源在當前模式下不被監(jiān)聽,那么任何其生成的消息只在run loop運行在其關(guān)聯(lián)的模式下才會被傳遞。
1)基于端口的輸入源:
在runloop中,被定義名為souce1。Cocoa和Core Foundation內(nèi)置支持使用端口相關(guān)的對象和函數(shù)來創(chuàng)建的基于端口的源。例如,在Cocoa里面你從來不需要直接創(chuàng)建輸入源。你只要簡單的創(chuàng)建端口對象,并使用NSPort的方法把該端口添加到run loop。端口對象會自己處理創(chuàng)建和配置輸入源。
在Core Foundation,你必須人工創(chuàng)建端口和它的run loop源.在兩種情況下,你都可以使用端口相關(guān)的函數(shù)(CFMachPortRef,CFMessagePortRef,CFSocketRef)來創(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)該是NSPort內(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 消息標識。 //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里去復制過來的為NSPortMessage類型的一個對象,但是我們發(fā)現(xiàn)蘋果只是在.h中@class進來,我們無法調(diào)用它的任何方法。所以我們用id聲明,然后通過KVC去取它的屬性。
2)關(guān)于下面這個傳值類型的問題:
NSMutableArray *array = [NSMutableArray arrayWithArray:@[mainPort,data]];
**這個傳參數(shù)組里面只能裝兩種類型的數(shù)據(jù),一種是NSPort的子類,一種是NSData的子類。**所以我們?nèi)绻眠@種方式傳值必須得先把數(shù)據(jù)轉(zhuǎn)成NSData類型的才行。
#### Cocoa 執(zhí)行 Selector 的源:
- 除了基于端口的源,Cocoa定義了自定義輸入源,允許你在任何線程執(zhí)行selector。它被稱為source0,和基于端口的源一樣,執(zhí)行selector請求會在目標線程上序列化,減緩許多在線程上允許多個方法容易引起的同步問題。不像基于端口的源,一個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)用的方法會異步的實行,不會阻塞提交線程。方法比較簡單,就不舉例說明了。
#### 自定義輸入源:
- 為了自定義輸入源,必須使用 Core Foundation里面的 CGRunLoopSourceRef類型相關(guān)的函數(shù)來創(chuàng)建。你可以使用回調(diào)函數(shù)來配置自定義輸入源。Corefondation 會在配置源的不同地方調(diào)用回調(diào)函數(shù),處理輸入時間,在源從 runloop 移除的時候清理它。除了定義在事件到達時自定義輸入源的行為,你也必須定義消息傳遞機制。源的這部分運行在單獨的線程里面,并負責在數(shù)據(jù)等待處理的時候傳遞數(shù)據(jù)給源并源并通知它處理數(shù)據(jù)。消息傳遞機制的定義取決于你,但是最好不要過于復雜。創(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。當主線程有任務(wù)需要分發(fā)給工作線程時候,***主線程會給命令緩沖區(qū)發(fā)送命令和必須的信息來通知工作線程開始執(zhí)行任務(wù)。(因為主線程和輸入源所在工作線程都可以訪問命令緩沖區(qū),因此這些訪問必須是同步的)***一旦命令傳送出去,主線程會通知輸入源并且喚醒工作線程的 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添加到當前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 正在處理事件"); //添加輸入事件,當前正在處理一個事件,當前事件處理完成后,立即處理當前新輸入的事件 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);`
在看當前runloop是否在休眠中,來看是否需要調(diào)用`CFRunLoopWakeUp(_runLoopRef);`
(一般都是要調(diào)用的)。
4) 定時源:
- 定時源在預(yù)設(shè)的時間點同步方式傳遞消息。定時器是線程通知自己做某事的一種方法。
- 盡管定時器可以產(chǎn)生基于時間的通知,但它并不是實時機制。和輸入源一樣,定時器也和 runloop 的特定模式相關(guān)。如果定時器所在的模式當前未被 runloop 監(jiān)視,那么定時器將不會開始知道 runloop 運行在響應(yīng)的模式下。類似的。如果定時器在 runloop 處理某一事件期間開始,定時器會一直等待直到下次 runloop 開始響應(yīng)的處理程序。如果 runloop 不運行了,那么定時器也永遠不啟動。
- 配置定時源:Cocoa 中可以使用以下 NSTimer 類方法來創(chuàng)建并調(diào)配一個定時器:??
[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)#>]
當然還有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,&myCFTimerCallback, &context);
最后用一張runloop運行時的流程圖來梳理一下我們這些源觸發(fā)的順序

- 如圖所示,首先我要明確一個知識點:**runloop跑一圈,只能執(zhí)行一個事件。**
- **timer和source0進入runloop中,都只是通知Observer我要處理,但是還是會有 678睡眠喚醒這一步。但是source1如果有,就會直接跳到第9步去執(zhí)行。**
- 我們前面也講過第7步,這里再提一下。它是一直阻塞在這一行的,直到:
- a.soruce1來了。 b.定時器啟動。 c。runloop超時。d。runloop被顯示喚醒CFRunLoopWakeUp(runloop) (也就是source0來了)。
- 這里可能大家會奇怪了,之前不是說source1有的話就直接跳到第9步去執(zhí)行了么?但是仔細想想,如果runloop正處在睡眠狀態(tài)下,這時候有個soruce1來了,是不是也需要喚醒runloop~
- 至于其他的,應(yīng)該不難理解了。
#### Run Loop的Observer
上圖提到了Observer,順帶簡單講講吧:Core Foundation層的接口可以定義一個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);
- 方法就是創(chuàng)建一個observer,綁定一個runloop和模式,而block回調(diào)就是監(jiān)聽到runloop每種狀態(tài)的時候會觸發(fā)。
- 其中`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)
};
###### 寫在結(jié)尾
- 關(guān)于runloop,還有一些更深層次拓展性的內(nèi)容,包括與**gcd的協(xié)同關(guān)系**、**AutoreleasePool的釋放時機**、**GCDTimer和NStimer區(qū)別**等等,本來是想寫一寫的,奈何篇幅已經(jīng)很大了,實在寫不動了,推薦感興趣的可以看看YY大神這篇:[深入理解RunLoop](http://blog.ibireme.com/2015/05/18/runloop/)
> 本文轉(zhuǎn)載于大神 [[涂耀輝](http://m.itdecent.cn/u/14431e509ae8) ](http://m.itdecent.cn/u/14431e509ae8)的[基于runloop的線程保活、銷毀與通信](http://m.itdecent.cn/p/4d5b6fc33519#)