我的博客鏈接:http://superyang.gitcafe.io/blog/2016/01/18/runloop-5/
使用 Run Loop 對(duì)象
一個(gè) run loop 對(duì)象提供了一些主要接口用于向你的 run loop 中添加 input source ,timers, 和run loop observer,并且運(yùn)行它。每一條線程有且只有一個(gè)run loop 與他相關(guān)聯(lián)。在 Cocoa 中,這個(gè)對(duì)象是 NSRunLoop 類的一個(gè)實(shí)例。在底層的應(yīng)用中,它是指向 CFRunLoopRef 這種不透明類型的一個(gè)指針。
獲取 Run Loop 對(duì)象
你需要使用以下其中之一來獲取當(dāng)前線程的 Run Loop :
- 在 Cocoa 中,使用 NSRunLoop 的類方法 currentRunLoop 去拿到一個(gè)
NSRunLoop
對(duì)象。 - 使用 CFRunLoopGetCurrent 函數(shù)。
盡管這兩種方法不是 toll-free bridged type(在Foundation 和 Core Foundation 中擁有等價(jià)替換接口的能力的類型)的類型,但是如果你需要可以從 NSRunLoop
對(duì)象里拿到 CFRunLoopRef 這種不透明類型
(蘋果封裝在內(nèi)部的C語言類型)。NSRunLoop
類定義了 getCFRunLoop
方法用來返回一個(gè)可以傳入到 Core Foundation 代碼中的 CFRunLoopRef
類型的C語言指針對(duì)象(結(jié)構(gòu)體指針)。這兩種對(duì)象都可以來自于同一個(gè) run loop,你可以根據(jù)你的需要來選擇具體使用 NSRunLoop
和 CFRunLoopRef
這兩種對(duì)象的哪一種。
配置 Run Loop
在你運(yùn)行一個(gè)子線程的 run loop 之前,你必須向其添加至少一個(gè) input source 或者 timer。如果 run loop 沒有任何需要監(jiān)視的 source, 它將會(huì)在你嘗試運(yùn)行它的時(shí)候立即退出。請(qǐng)參考配置RunLoop Sounce(本文接下來的章節(jié)將有介紹)。
除了安裝 source,你還可以 run loop observer 并且使用他們檢測(cè) runloop的處于不同執(zhí)行階段。為了安裝 run loop observer ,你需要?jiǎng)?chuàng)建一個(gè) CFRunLoopObserverRef 不透明類型的指針并使用 CFRunLoopAddObserver 函數(shù)將 Observer 添加到你的 run loop 中去,Run Loop Observer 必須使用 Core Foundation 框架接口創(chuàng)建,在 Cocoa 應(yīng)用中也一樣。
表 3-1 展示了在線程 runloop 中,添加 run loop Observer 的主要代碼流程。本例的目的旨在告訴你如何創(chuàng)建一個(gè) run loop Observer, 所以代碼只是簡(jiǎn)單設(shè)置了一個(gè)run loop Observer 用來監(jiān)視 run loop 的所有活動(dòng) 。基本的處理代碼(沒有展示)僅僅是日志輸出 run loop 的各項(xiàng)活動(dòng)行為 作為 timer 的事件回調(diào)。
表3-1 創(chuàng)建 runloop Observer
- (void)threadMain {
// 應(yīng)用使用垃圾回收,所以不需要 自動(dòng)釋放池 autorelease pool
NSRunLoop *myRunLoop = [NSRunLoop currentRunLoop];
// 創(chuàng)建一個(gè) run loop observer 并且將他添加到當(dāng)前 run loop 中去
/*!
* @author 楊超, 16-01-13 15:01:45
*
* @brief CFRunLoopObserverContext 用來配置 CFRunLoopObserver 對(duì)象行為的結(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);
} CFRunLoopObserverContext;
*
* @param version 結(jié)構(gòu)體版本號(hào),必須為0
* @param info 一個(gè)程序預(yù)定義的任意指針,可以再 run loop Observer 創(chuàng)建時(shí)為其關(guān)聯(lián)。這個(gè)指針將被傳到所有 context 多定義的所有回調(diào)中。
* @param retain 程序定義 info 指針的內(nèi)存保留(retain)回調(diào),可以為 NULL
* @param release 程序定義 info 指針的內(nèi)存釋放(release)回調(diào),可以為 NULL
* @param copyDescription 程序定于 info 指針的 copy 描述回調(diào),可以為 NULL
*
* @since
*/
CFRunLoopObserverContext context = {0 , (__bridge void *)(self), NULL, NULL, NULL};
CFRunLoopObserverRef observer = CFRunLoopObserverCreate(kCFAllocatorDefault, kCFRunLoopAllActivities, YES, 0, &myRunLoopObserverCallBack, &context);
if (observer) {
CFRunLoopRef cfLoop = [myRunLoop getCFRunLoop];
CFRunLoopAddObserver(cfLoop, observer, kCFRunLoopDefaultMode);
}
// 創(chuàng)建并安排好 timer
[NSTimer scheduledTimerWithTimeInterval:0.1 target:self selector:@selector(doFireTimer) userInfo:nil repeats:YES];
NSInteger loopCount = 10;
do {
// 3秒后運(yùn)行 run loop 實(shí)際效果是每三秒進(jìn)入一次當(dāng)前 while 循環(huán)
[myRunLoop runUntilDate:[NSDate dateWithTimeIntervalSinceNow:3]];
loopCount --;
} while (loopCount);
}
void myRunLoopObserverCallBack(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info) {
NSLog(@"observer正在回調(diào)\n%@----%tu----%@", observer, activity, info);
}
- (void)doFireTimer {
NSLog(@"計(jì)時(shí)器回調(diào)");
}
當(dāng)為一個(gè)長(zhǎng)期存活的現(xiàn)場(chǎng)配置 runloop 時(shí),至少添加一個(gè) input source 去接收消息。盡管你可以僅僅使用一個(gè) 關(guān)聯(lián)的timer 就可以進(jìn)入 run loop,一旦 timer 啟動(dòng),通常都會(huì)被作廢掉,這將會(huì)硬氣 run loop 的退出。關(guān)聯(lián)一個(gè)重復(fù)執(zhí)行的 timer 定時(shí)器可以保持讓 runloop 在很長(zhǎng)的一段時(shí)期內(nèi)得以運(yùn)行,但是需要周期性的去啟動(dòng)定時(shí)器 timer 來喚醒你的線程,這是投票有效的另一種形式(這句莫名其妙,不懂是干嗎的)。相比之下, input source 會(huì)等待事件的發(fā)生,并保持線程處于睡眠狀態(tài)直到事件確實(shí)發(fā)生了。
開動(dòng) run loop
在應(yīng)用中,只有在子線程中才是有必要開啟 run loop 的,一個(gè) run loop 必須至少有一個(gè)用來監(jiān)視的 input source 。如果一個(gè)關(guān)聯(lián)的都沒有,run loop 將會(huì)立即退出。
下面有一些方法開啟 run loop:
- 無條件的
- 通過一套時(shí)間限制
- 在一個(gè)特別的 mode 下
無條件的進(jìn)入你的 run loop 是最簡(jiǎn)單的選項(xiàng),但這種也是最不可取的。無條件地運(yùn)行你的 run loop 將會(huì)使你的線程進(jìn)入進(jìn)入永久的循環(huán)中,這使你很難控制運(yùn)行循環(huán)本身。你可以添加和移除 input source 和 timer,但是只有一種方式去停止 run loop,那就是將它殺死。同時(shí)也不存在在自定義 mode 中運(yùn)行 run loop 的方法。
為了替代無條件的運(yùn)行 run loop ,更好的辦法是使用超時(shí)值來運(yùn)行 runloop。當(dāng)你使用超時(shí)值時(shí),run loop 會(huì)一直運(yùn)行直到在事件來臨時(shí) 或者 分配的時(shí)間結(jié)束時(shí)。當(dāng)你的事件到達(dá)時(shí),系統(tǒng)會(huì)分配一個(gè) handler 去處理它,并且之后 run loop 會(huì)退出。你可以用代碼重啟你的 run loop 以便處理下一個(gè)事件。如果不想繼續(xù)使用剛才分配時(shí)間結(jié)束的原則,也可以簡(jiǎn)單的重啟 runloop 或者使用這些時(shí)間去做任何你需要做的事。
除了使用超時(shí)值,你也可以使用指定的 mode 運(yùn)行 run loop。mode 和超時(shí)值不會(huì)互相排斥,并且都可以用來啟動(dòng)一個(gè)線程。
表 3-2 展示了一個(gè)線程入口的常用的例行程序。示例代碼的關(guān)鍵部分展示了一個(gè) run loop 的基礎(chǔ)架構(gòu)。本質(zhì)上,你將 input sources 和 timers 添加到你的 runloop 中,然后重復(fù)的調(diào)用其中一個(gè)例行程序來啟動(dòng) run loop 。每一次例行程序返回時(shí),你需要檢查一下是否滿足可能會(huì)退出線程的條件。示例使用了 Core Foundation 的框架的例行程序以便檢查返回結(jié)果并且可以決定如何退出 runloop。如果你是用的是 Cocoa ,你也可以使用類似的方式通過 NSRunLoop 的方法去運(yùn)行 runloop , 并且不需要檢查返回值。(使用 NSRunLoop 的方法的例子可以參考 表3-14.)
表 3-2 運(yùn)行 runloop
- (void)skeletionThreadMain {
// 如果你的應(yīng)用沒有使用垃圾回收 請(qǐng)?jiān)谶@里添加 自動(dòng)釋放池(ps:這示例代碼也太老了,誰還用垃圾回收啊)
BOOL done = NO;
// 給 runloop 添加 source 或timer,然后做一些其他的配置
do {
// 開啟 runloop 并且被一個(gè) source 被處理后要返回
/** SInt32 32位有符號(hào)整數(shù) */
SInt32 result = CFRunLoopRunInMode(kCFRunLoopDefaultMode, 10, YES);
// 如果 source 已經(jīng)顯式的停止了 runloop ,或者根本不存在任何 source 或 timer,將會(huì)退出。
if ((result == kCFRunLoopRunStopped) || (result == kCFRunLoopRunFinished)) {
done = YES;
// 在這里檢查任何其他符合退出的條件并且按需設(shè)置 done 變量的值。
}
} while (!done);
// 在這里清除代碼。確保釋放任何之前創(chuàng)建的自動(dòng)釋放池。
}
可以遞歸開啟 runloop,換句話說,你可以使用 input source 或者 timer 的例行程序來調(diào)用 CFRunLoopRun,CFRunLoopRunInMode或者任何 NSRunLoop 的 runloop 啟動(dòng)方法。這樣做你可以使用任何你想用的 mode 來運(yùn)行一個(gè) 嵌套的 run loop ,包括 通過外層 run loop 使用的 mode 。
退出 RunLoop
有兩種途徑可以讓 runloop 在處理事件之前退出:
- 使用超時(shí)值配置 runloop 運(yùn)行。
- 直接告訴 runloop 停止(ps:。。。這條太搞了)。
使用超時(shí)值無疑是更偏愛的方法,如果你能管理它,指定一個(gè)超時(shí)值使 runloop 結(jié)束所有他的正常處理的任務(wù), 包括在退出前向 runloop observer 發(fā)送通知。
使用 CFRunLoopStop 函數(shù)顯示地停止 runloop,產(chǎn)生的結(jié)果和超時(shí)相似。runloop 會(huì)發(fā)送任何 runloop 提醒通知然后才退出。不同的是你可以將這項(xiàng)技術(shù)應(yīng)用在你用無條件方式開啟的 runloop 上。
盡管移除一個(gè) runloop 的 input source 和 timer 可以造成 runloop 的退出,但這并不是一個(gè)可靠的方式來停止 runloop 。一些系統(tǒng)例行程序給 runloop 添加一些 input source 來處理必要的事件。你的代碼可能無法看出這些 input source,你可能不能移除這些用來防止 runloop 退出的 source。
線程安全 和 Run Loop 對(duì)象
線程安全大多取決于你用來操作 runloop 的API。Core Foundation 函數(shù) 一般來說都是線程安全的,所以可以被任何線程調(diào)用。假如你正在執(zhí)行一個(gè)修改 runloop 配置的操作,那么繼續(xù)吧,對(duì)擁有 runloop 的線程來說這樣做仍然是很好的作法。
Cocoa 的 NSRunLoop
類內(nèi)部不像 Core Foundation 中的接口那樣是線程安全的。如果你要使用 NSRunLoop 類去修改你的 runloop,你只能在 runloop 所在的線程中這樣做。先其他線程中的 runloop 中添加 input source 或 timer 會(huì)引起你的程序崩潰或出現(xiàn)不可預(yù)知的異常。
配置 run loop source
接下來的章節(jié)將展示如何在 Cocoa 和 Core Foundation 中設(shè)置不同類型的 input source。
定義一個(gè)自定義自定義 input source
創(chuàng)建一個(gè)自定義的 input source 你需要實(shí)現(xiàn)以下這些條件:
- 你想要你的 source 處理的信息
- 一段調(diào)度模塊的例行程序讓感興趣的客戶機(jī)了解如何連接你的 input source。
- 一段處理模塊例行程序用來處理任何客戶機(jī)發(fā)送的請(qǐng)求
- 一段取消模塊的例行程序用來銷毀你的 source
因?yàn)槟銊?chuàng)建了一個(gè)自定義的 input source 來處理自定義的信息,所以實(shí)際上的配置會(huì)設(shè)計(jì)的非常靈活。調(diào)度模塊,處理模塊和取消模塊的例行程序幾乎都是你的自定義 input source 的關(guān)鍵例行程序。剩下的大多數(shù) input source 行為都發(fā)生在這些例行處理程序之外。比如,由你來定義一個(gè)工具用來將數(shù)據(jù)傳到你的 input source并且傳遞你的 input source 的數(shù)據(jù)到其他線程中去。
插圖 3-2 展示了一個(gè)簡(jiǎn)單的自定義 input source 的配置。在本例中,應(yīng)用程序主線程維持引用了input source , input source 的緩沖模塊,還有安裝 input source 的 runloop。當(dāng)主線程有一個(gè)任務(wù)向切換到工作子線程中去,他會(huì)發(fā)送一個(gè)命令,命令緩沖區(qū)以及啟動(dòng)任務(wù)所需的任何線程的信息(因?yàn)橹骶€程和工作子線程的 input source 都有權(quán)限去訪問命令緩沖區(qū),訪問必須同步)一旦命令發(fā)送了,主線程會(huì)發(fā)送信號(hào)給 input source 來喚醒工作子線程的 runloop。一旦受到喚醒的命令, runloop 會(huì)調(diào)用 input source 的處理程序 去處理命令緩存器中緩存的命令。
圖 3-2 操作一個(gè)自定義 input source

接下來的章節(jié)將會(huì)解釋如何通過上圖實(shí)現(xiàn)一個(gè)自定義 input source 并展示你需要實(shí)現(xiàn)的關(guān)鍵代碼。
定義 input source
定義一個(gè)自定義 input source 需要使用 Core Foundation 的例行程序配置你的 runloop input source 并且 將它與你的 runloop 關(guān)聯(lián)。盡管基礎(chǔ)處理程序是基于 C-語言 函數(shù)的,但這不會(huì)阻止你使用 Objective-C 或者 C++ 去封裝它為面向?qū)ο蟮拇a。
插圖3-2中介紹的 input source 使用一個(gè) objective-C 對(duì)象去管理一個(gè)命令緩存器,并與 runloop 進(jìn)行協(xié)調(diào)。列表3-3 展示了這個(gè)對(duì)象的定義。RunLoopSource
對(duì)象管理一個(gè)命令緩沖器,并且使用命令緩存器接受來自其他線程的消息。該表也展示了 RunLoopContext
對(duì)象的定義,該對(duì)象僅僅是一個(gè)容器,用來傳遞一個(gè) RunLoopSource
對(duì)象和應(yīng)用主線程的 runloop 引用。
表 3-3 自定義 input source 對(duì)象的定義
@interface YCRunLoopSource : NSObject
{
CFRunLoopSourceRef runLoopSource;
NSMutableArray *commands;
}
- (id)init;
// 添加
- (void)addToCurrentRunLoop;
// 銷毀
- (void)invalidate;
// 處理方法
- (void)sourceFired;
// 用來注冊(cè)需要處理的命令的客戶機(jī)接口
- (void)addCommand:(NSInteger)command withData:(id)data;
- (void)fireAllCommandsOnRunLoop:(CFRunLoopSourceRef)runloop;
// 這些是CFRunLoopRef 的回調(diào)函數(shù)
/** 調(diào)度函數(shù) */
void RunLoopSourceScheduleRoutine(void *info, CFRunLoopRef r1, CFStringRef mode);
/** 處理函數(shù) */
void RunLoopSourcePerformRoutine (void *info);
/** 取消函數(shù) */
void RunLoopSourceCancelRoutine (void *info, CFRunLoopRef rl, CFStringRef mode);
@end
// RunLoopContext 是一個(gè) 在注冊(cè) input source 時(shí)使用的容器對(duì)象
@interface YCRunLoopContext : NSObject
{
CFRunLoopRef runLoop;
YCRunLoopSource *source;
}
/** 持有 runloop 和 source */
@property (readonly) CFRunLoopRef runLoop;
@property (readonly) YCRunLoopSource *source;
- (id)initWithSource:(YCRunLoopSource*)src andLoop:(CFRunLoopRef)loop;
@end
盡管 Objective-C 代碼管理著 input source 的自定義數(shù)據(jù)。關(guān)聯(lián)一個(gè) input source 到一個(gè)具備 基于 C-語言 的回調(diào)函數(shù)的 runloop 。其中第一個(gè)函數(shù)是當(dāng)你實(shí)際將 input source 添加到 runloop 中的時(shí)刻調(diào)用。流程將展示在 表 3-4 中。因?yàn)檫@個(gè) input source 僅只有一個(gè) 客戶機(jī)(主線程)。它使用調(diào)度者函數(shù)通過目標(biāo)線程 application 的代理發(fā)送消息在目標(biāo)線程注冊(cè)自己。當(dāng) application 的代理和 input source 進(jìn)行通信時(shí) ,會(huì)使用 RunLoopContext 對(duì)象中的 info
信息來完成這個(gè)事。
表 3-4 調(diào)度 run loop source
void RunLoopSourceScheduleRoutine(void *info, CFRunLoopRef r1, CFStringRef mode){
YCRunLoopSource *obj = (__bridge YCRunLoopSource *)info;
// 這里的 Appdelegate 是主線程的代理
AppDelegate *del = [AppDelegate sharedAppDelegate];
// 上下文對(duì)象中持有source自己
YCRunLoopContext *theContext = [[YCRunLoopContext alloc] initWithSource:obj andLoop:r1];
// 通過代理去注冊(cè) Source 自己
[del performSelectorOnMainThread:@selector(registerSource:) withObject:theContext waitUntilDone:NO];
}
其中最重要的回調(diào)例行程序是當(dāng)你的 input source 被信號(hào)激活時(shí)處理自定義數(shù)據(jù)的部分。表3-5中展示了與 RunLoopSource
對(duì)象關(guān)聯(lián)的執(zhí)行者回調(diào)例行程$序,這個(gè)函數(shù)僅僅轉(zhuǎn)發(fā)用來 sourceFired
方法工作的請(qǐng)求,該請(qǐng)求用來處理任何 command buffer
(命令緩沖區(qū))中存在的命令。
表3-5 input source 中的執(zhí)行者
void RunLoopSourcePerformRoutine (void *info)
{
RunLoopSource* obj = (RunLoopSource*)info;
[obj sourceFired];
}
如果你使用 CFRunLoopSourceInvalidate
函數(shù)將 input source 從 runloop 重移除。系統(tǒng)會(huì)調(diào)用你的 input source 中的取消者例行程序。你可以利用這個(gè)例行程序去通知客戶機(jī)你的 input source 不再可用并且他們應(yīng)該移除任何自己的相關(guān)的引用。表3-6 展示了取消者例行回調(diào)程序通過 RunLoopSource 對(duì)象進(jìn)行注冊(cè)。這個(gè)函數(shù)發(fā)送另一個(gè) RunLoopContext 對(duì)象給 application 代理。但是這讓代理去移除 runloop surce 的相關(guān)引用。
表3-6 銷毀一個(gè) input source
void RunLoopSourceCancelRoutine (void *info, CFRunLoopRef rl, CFStringRef mode)
{
RunLoopSource* obj = (RunLoopSource*)info;
AppDelegate* del = [AppDelegate sharedAppDelegate];
RunLoopContext* theContext = [[RunLoopContext alloc] initWithSource:obj andLoop:rl];
[del performSelectorOnMainThread:@selector(removeSource:)
withObject:theContext waitUntilDone:YES];
}
筆記:應(yīng)用代理方法 registerSource: 和 removeSource 方法在下面的章節(jié) 《協(xié)調(diào) input source 的客戶機(jī)》展示
為 runloop 安裝 input source
表3-7 展示了 RunLoopSource
類的 init
方法 和 addToCurrentRunLoop
方法。init
方法創(chuàng)建了 CFRunLoopSource 不透明類型的必須關(guān)聯(lián)到 runloop 的對(duì)象。它會(huì)傳遞 RunLoopSource
對(duì)象自己作為 山下文信息 以便于例行回調(diào)程序有一個(gè)指向?qū)ο蟮闹羔槨nput source 直到線程喚起 addToCurrentRunLoop
方法時(shí)才會(huì)執(zhí)行安裝,準(zhǔn)確將在 RunLoopSourceScheduleRoutine 回調(diào)函數(shù)調(diào)用時(shí)。 一旦 input source 安裝到 runloop 中,線程將會(huì)運(yùn)行自己的 runloop 去等待 input source 發(fā)出事件。
表3-7 安裝 run loop source
- (id)init {
// 創(chuàng)建上下文容器,其中會(huì)連接自己的 info,retain info release info,還會(huì)關(guān)聯(lián)三個(gè)例行程序。
CFRunLoopSourceContext context = {0, (__bridge void *)(self), NULL, NULL, NULL ,NULL, NULL, &RunLoopSourceScheduleRoutine, RunLoopSourceCancelRoutine, RunLoopSourcePerformRoutine};
/** 通過索引,上下文,和CFAllocator創(chuàng)建source */
runLoopSource = CFRunLoopSourceCreate(NULL, 0, &context);
commands = [[NSMutableArray alloc] init];
return self;
}
- (void)addToCurrentRunLoop{
CFRunLoopRef runLoop = CFRunLoopGetCurrent();
CFRunLoopAddSource(runLoop, runLoopSource, kCFRunLoopDefaultMode);
}
協(xié)調(diào) input source 的客戶機(jī)
對(duì)于你的 input source 會(huì)非常有用,你需要操作它并且從其他線程向它提供消息。input source 的要點(diǎn)是將其添加到線程并睡眠直到有事情要做時(shí)才喚醒。事實(shí)上很有必要讓其他線程了解 input surce 并且有方法可以和它交流(溝通數(shù)據(jù))。
通知你的 input source 客戶機(jī)的方法之一是發(fā)出注冊(cè)請(qǐng)求 當(dāng)你的 input source 第一次安裝到你的 runloop 中時(shí)。你可以向你的 input source 注冊(cè)盡可能多的客戶機(jī)。或者你僅僅只是簡(jiǎn)單的用一些中央機(jī)構(gòu),然后將你的 input source 聲明為感興趣的客戶端進(jìn)行注冊(cè)。表3-8 展示了 通過代理 和 調(diào)用喚起定義的 注冊(cè)方法 當(dāng) RunLoopSource 對(duì)象的調(diào)度者函數(shù)被調(diào)用時(shí)。這個(gè)方法將會(huì)收到 RunLoopSource 提供的 RunLoopContext 對(duì)象并且將它添加到他的 source 列表中。這個(gè)表也會(huì)展示 當(dāng) input source 從 他的 runloop 中被移除時(shí) 用來注銷的例行程序。
表 3-8 使用 application 的 代理 注銷并且移除 input source
#import "YCRunLoopSource.h"
#import "YCRunLoopContext.h"
@interface AppDelegate : NSObject
@property (nonatomic, strong) NSMutableArray *sourcesToPing;
/** 應(yīng)該是一個(gè)單例 */
+ (instancetype)sharedAppDelegate;
- (void)registerSource:(YCRunLoopContext *)context;
- (void)removeSource:(YCRunLoopContext *)context;
@end
static AppDelegate *_instance;
@implementation AppDelegate
+ (instancetype)sharedAppDelegate
{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
_instance = [[self alloc] init];
});
return _instance;
}
- (void)registerSource:(YCRunLoopContext *)context
{
[self.sourcesToPing addObject:context];
}
- (void)removeSource:(YCRunLoopContext *)context
{
id objToRemove = nil;
for (YCRunLoopContext *contextObj in self.sourcesToPing) {
if ([contextObj isEqual:context]) {
objToRemove = contextObj;
break;
}
}
if (objToRemove) {
[self.sourcesToPing removeObject:objToRemove];
}
}
- (NSMutableArray *)sourcesToPing {
if (_sourcesToPing == nil) {
_sourcesToPing = @[].mutableCopy;
}
return _sourcesToPing;
}
@end
Note:回調(diào)函數(shù)會(huì)在之前的表3-4和3-6中調(diào)用這些函數(shù)
信號(hào)激活 input source
釋放 input source 的數(shù)據(jù)之后,客戶機(jī)必須發(fā)信號(hào)給 source 并且喚醒它的 runloop。發(fā)信號(hào)給 source 是讓 runloop 知道 source 已經(jīng)準(zhǔn)備好被處理。因?yàn)榫€程可能會(huì)在發(fā)信號(hào)的時(shí)處于睡眠狀態(tài),所以那你必須顯式的讓 run loop 保持喚醒。除非如此,不然在處理 input source 時(shí)會(huì)出現(xiàn)延遲。
表 3-9 展示了 RunLoopSource
對(duì)象的 fireCommandsOnRunLoop
方法,客戶機(jī)會(huì)在它準(zhǔn)備好為 source 處理添加到 buffer 緩沖區(qū)中的 command 命令時(shí)調(diào)用這個(gè)方法。
表 3-9 喚醒 run loop
- (void)fireCommandsOnRunLoop:(CFRunLoopRef)runloop
{
CFRunLoopSourceSignal(runLoopSource);
CFRunLoopWakeUp(runloop);
}
Note:你不能通過向一個(gè)自定義 input source 發(fā)信息來處理一個(gè) SIGHUP 或者其他處理類型的信號(hào)
,Core Foundation 框架中用于喚醒 runloop 的函數(shù)不是信號(hào)安全的。并且不能作為你的應(yīng)用程序中內(nèi)置信號(hào)處理的例行程序使用。關(guān)于更多的關(guān)于信號(hào)處理程序,詳見 sigaction man 頁(yè)面。
配置 Timer Source
為了創(chuàng)建 timer source,所有你需要做的就是創(chuàng)建一個(gè) timer 對(duì)象,并且在你的 run loop 中調(diào)度它。在 Cocoa 中,你使用 NSTimer 類來創(chuàng)建一個(gè)新的 timer 對(duì)象。在 Core Foundation 框架中,你可以使用 CFRunLoopTimerRef 不透明類型來創(chuàng)建。NSTimer
類只是 Core Foundation 框架中的一個(gè)擴(kuò)展,是用來方便的提供一些功能,比如使用相同的方法創(chuàng)建和調(diào)度 timer 。
在 Cocoa 中,你能通過以下兩種類方法創(chuàng)建和調(diào)度 timer。
scheduledTimerWithTimeInterval:target:selector:userInfo:repeats:
scheduledTimerWithTimeInterval:invocation:repeats:
這些方法創(chuàng)建 timer 并且將它們添加到當(dāng)前線程的 run loop 中的 default mode(NSDefaultRunLoopMode) 中去。如果你使用的是 NSTimer 對(duì)象,那就可以手動(dòng)調(diào)度 timer 并且可以使用 NSRunLoop 的 addTimer:forMode: 手動(dòng)將它添加到 runloop 中去。這兩種技術(shù)都是基于同一種,但是通過timer 的配置給你不同級(jí)別的控制。比如你手動(dòng)創(chuàng)建 timer 并將它添加到 run loop 中,并添加到除 default mode 之外的其他 mode 中去。表3-10 展示了如何使用兩種技術(shù)創(chuàng)建 timer。第一個(gè) timer 初始化為 延遲一秒但是會(huì)在延遲后有規(guī)律的每個(gè)0.1秒觸發(fā)一次。第二個(gè) timer 會(huì)在 0.2 秒延遲后開始觸發(fā),并且在延遲結(jié)束后 每 0.2 秒觸發(fā)一次。
表3-10 使用 NSTimer 創(chuàng)建和調(diào)度 timer
NSRunLoop* myRunLoop = [NSRunLoop currentRunLoop];
// 創(chuàng)建并調(diào)度第一個(gè) timer
NSDate* futureDate = [NSDate dateWithTimeIntervalSinceNow:1.0];
NSTimer* myTimer = [[NSTimer alloc] initWithFireDate:futureDate
interval:0.1
target:self
selector:@selector(myDoFireTimer1:)
userInfo:nil
repeats:YES];
[myRunLoop addTimer:myTimer forMode:NSDefaultRunLoopMode];
// 創(chuàng)建并調(diào)動(dòng)第二個(gè) timer
[NSTimer scheduledTimerWithTimeInterval:0.2
target:self
selector:@selector(myDoFireTimer2:)
userInfo:nil
repeats:YES];
表3-11 展示了使用 Core Foundation 框架時(shí)需要配置的代碼。盡管實(shí)例代碼中沒有傳遞任何用戶自定義的信息的上下文結(jié)構(gòu),但是你可以使>用這個(gè)結(jié)構(gòu)去傳遞任何你的 timer 所需要自定義數(shù)據(jù)。關(guān)于更多該結(jié)構(gòu)的內(nèi)容可以瀏覽 CFRunLoopTimer 參考。
表 3-11 使用 Core Foundation 框架創(chuàng)建和調(diào)度一個(gè) timer
CFRunLoopRef runLoop = CFRunLoopGetCurrent();
CFRunLoopTimerContext context = {0, NULL, NULL, NULL, NULL};
CFRunLoopTimerRef timer = CFRunLoopTimerCreate(kCFAllocatorDefault, 0.1, 0.3, 0, 0,
&myCFTimerCallback, &context);
CFRunLoopAddTimer(runLoop, timer, kCFRunLoopCommonModes);
配置一個(gè)基于 port 的 input source
Cocoa 和 Core Foundation 都支持用于和線程間或者進(jìn)程間通信的基于 端口的對(duì)象。接下來的章節(jié)將會(huì)向你展示如何使用一些不同類型的 port 構(gòu)建 port 通信。
配置一個(gè)NSMachPort Object
使用 NSMachPort 對(duì)象創(chuàng)建一個(gè)本地連接。你創(chuàng)建一個(gè) port 對(duì)象并把它添加到你的主線程 run loop 中去。當(dāng)啟動(dòng)你的子線程時(shí),你要傳一些相同的對(duì)象到你的線程入口點(diǎn)函數(shù)中去。子線程可以使用相同的對(duì)象發(fā)送信息回到你的主線程中去。
實(shí)現(xiàn)主線程代碼
表 3-12 中展示了用于啟動(dòng)子工作線程的主線程代碼。因?yàn)?Cocoa 框架執(zhí)行很多介入步驟用于配置 port 和 run loop ,Cocoa 的 launchThread
方法相比于 Core Foundation 的等價(jià)功能表 3-17更加簡(jiǎn)潔明了。盡管如此,這兩個(gè)框架在這一模塊的功能表現(xiàn)基本都是相同的。其中一個(gè)存在的差異是與發(fā)送本地 port 到工作線程的方式不同,這個(gè)方法是直接發(fā)送 NSPort 對(duì)象的。
表 3-12 list3-12 Main Thread lauch method
- (void)launchThread {
NSPort *myPort = [NSMachPort port];
if (myPort) {
// 這個(gè)類處理即將過來的 port 信息
[myPort setDelegate:self];
// 將此端口作為 input source 安裝到當(dāng)前 run loop 中去
[[NSRunLoop currentRunLoop] addPort:myPort forMode:NSDefaultRunLoopMode];
// 開啟工作子線程,讓工作子線程去釋放 port
[NSThread detachNewThreadSelector:@selector(LaunchThreadWithPort:) toTarget:[MyWorkerClass class] withObject:myPort];
}
}
為了設(shè)置為線程間雙向通信信
道,在
登記信息中,你需要讓工作線程發(fā)送自己的本地 port 到主線程。接收登記信息是為了讓你的主線程知道開動(dòng)子線程的過程進(jìn)行的非常順利,同時(shí)也為我們?yōu)樘峁┝艘环N方法去向該線程發(fā)送更多信息。
表 3-13 展示了用于主線程的handlePortMessage:方法,這個(gè)方法會(huì)在線程到達(dá)自己的本地 port 時(shí)進(jìn)行調(diào)用。當(dāng)?shù)怯浶畔?check-in message)到達(dá)時(shí),該方法將直接從 port 信息中檢索子線程的 port 并保存以備后用。
表 3-13 處理 Mach port 信息
# define kCheckinMessage 100
// 處理工作線程的響應(yīng)的代理方法
- (void)handlePortMessage:(NSPortMessage *)portMessage
{
unsigned int message = [portMessage msgid];
// 定義遠(yuǎn)程端口
NSPort *distantPort = nil;
if (message == kCheckinMessage) {
// 獲取工作線程的通信 port
distantPort = [portMessage sendPort];
// 引用計(jì)數(shù)+1 并 保存工作端口以備后用
[self storeDistantPort:distantPort];
} else {
// 處理其他信息
}
}
- (void)storeDistantPort:(NSPort *)port {
// 保存遠(yuǎn)程端口
}
實(shí)現(xiàn)子線程代碼
對(duì)于工作子線程,你必須配置它并且是使用指定的端口進(jìn)行信息溝通并返回到主線程。
表 3-14 展示了用于設(shè)置工作線程的代碼。在創(chuàng)建一個(gè) qutorealease pool 之后,該方法會(huì)創(chuàng)建一個(gè)工作對(duì)象去驅(qū)動(dòng)線程執(zhí)行。該工作對(duì)象 的 sendCheckinMessage:
方法(表3-15 所示)為工作線程創(chuàng)建一個(gè)本地端口然后回復(fù)一個(gè) check-in 信息給主線程。
表 3-14 使用 Mach port 啟動(dòng)子線程
+(void)LaunchThreadWithPort:(id)inData
{
NSAutoreleasePool* pool = [[NSAutoreleasePool alloc] init];
// 設(shè)置本線程與主線程的連接
NSPort* distantPort = (NSPort*)inData;
MyWorkerClass* workerObj = [[self alloc] init];
[workerObj sendCheckinMessage:distantPort];
[distantPort release];
// 讓 run loop 處理這些邏輯
do
{
[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode
beforeDate:[NSDate distantFuture]];
}
while (![workerObj shouldExit]);
[workerObj release];
[pool release];
}
當(dāng)使用 NSMachPort
時(shí),本地和遠(yuǎn)端線程都可以使用相同的 port 對(duì)象 完成線程之間的單工通信(單向通信)。換句話說,通過一個(gè)線程創(chuàng)建的本地對(duì)象會(huì)成為另一個(gè)線程的遠(yuǎn)端 port 對(duì)象。(ps:現(xiàn)在總算明白本地就是當(dāng)前線程環(huán)境,遠(yuǎn)端就是其他線程環(huán)境)。
表 3-15展示了子線程的 check-in 例行程序 (登記信息例行程序)。這個(gè)方法設(shè)置了他自己的用于和以后進(jìn)行通訊的本地端口。并且回復(fù)一個(gè) check-in 登記信息給主線程。該方法使用 port 對(duì)象去接收 LaunchThreadWithport:
方法作為信息目標(biāo)。
表 3-15 使用 Mach port 發(fā)送 check-in 登記信息
// Worker thread check-in method
- (void)sendCheckinMessage:(NSPort*)outPort
{
// 保留(retain)并保存遠(yuǎn)端的 port 以備后用
[self setRemotePort:outPort];
// 創(chuàng)建和配置工作線程的端口(ps:當(dāng)前線程端口)
NSPort* myPort = [NSMachPort port];
[myPort setDelegate:self];
[[NSRunLoop currentRunLoop] addPort:myPort forMode:NSDefaultRunLoopMode];
// 創(chuàng)建 check-in 登記信息
NSPortMessage* messageObj = [[NSPortMessage alloc] initWithSendPort:outPort
receivePort:myPort components:nil];
if (messageObj)
{
// 完成配置信息 并 立即發(fā)送出去
[messageObj setMsgId:setMsgid:kCheckinMessage];
[messageObj sendBeforeDate:[NSDate date]];
}
配置一個(gè) NSMessagePort 對(duì)象
如果想要使用 [NSMessagePort](https://developer.apple.com/library/ios/documentation/Cocoa/Reference/Foundation/Classes/NSM essagePort_Class/index.html#//apple_ref/occ/cl/NSMessagePort) 對(duì)象創(chuàng)建一個(gè)本地連接,你不能在線程間僅僅值傳遞一個(gè) port 對(duì)> 象。遠(yuǎn)端信息端口必須通過名字獲取。
在Cocoa中,如果你想實(shí)現(xiàn)這個(gè)功能,需要使用一個(gè)指定的名字去注冊(cè)你的本地端口,然后向遠(yuǎn)端線程傳遞注冊(cè)的名字以便于他可以包含一
個(gè)合適的端口對(duì)象用于交流。表 3-16 展示了 port 創(chuàng)建方法和注冊(cè)方法 用于你想要使用 消息端口(message port)的地方。
表 3-16 注冊(cè)一個(gè) message port
NSPort* localPort = [[NSMessagePort alloc] init];
// 配置對(duì)象并將它添加到當(dāng)前 run loop 中去
[localPort setDelegate:self];
[[NSRunLoop currentRunLoop] addPort:localPort forMode:NSDefaultRunLoopMode];
// 使用指定的名字注冊(cè)端口。名字必須唯一。
NSString* localPortName = [NSString stringWithFormat:@"MyPortName"];
[[NSMessagePortNameServer sharedInstance] registerPort:localPort
name:localPortName];
在 Core Foundation 框架中配置一個(gè)基于端口的(Port-Based) input source
這個(gè)小結(jié)描述了如歌使用 Core Foundation 框架在你的應(yīng)用的主線程和輔助線程(worker thread)中創(chuàng)建一個(gè)雙向通信信道。
如表3-17 所示為應(yīng)用主線程啟動(dòng)輔助線程所使用的代碼。首先要做的是創(chuàng)建 CFMessagePortRef 不透明對(duì)象去監(jiān)聽從輔助線程發(fā)來的消息。輔助線程需要用來創(chuàng)建連接的端口名,以便于字符串值可以被發(fā)送到輔助線程的入口點(diǎn)函數(shù)。端口名在當(dāng)前用戶的上線文中通常必須是唯一的。否則,可能會(huì)出現(xiàn)運(yùn)行沖突。
表 3-17 給新線程關(guān)聯(lián)一個(gè) Core Foundation message port
#define kThreadStackSize (8 *4096)
OSStatus MySpawnThread()
{
// 創(chuàng)建一個(gè)本地端口用于接受響應(yīng)
CFStringRef myPortName;
CFMessagePortRef myPort;
CFRunLoopSourceRef rlSource;
CFMessagePortContext context = {0, NULL, NULL, NULL, NULL};
Boolean shouldFreeInfo;
// 用端口名 創(chuàng)建一個(gè)符合規(guī)范的字符串
myPortName = CFStringCreateWithFormat(NULL, NULL, CFSTR("com.myapp.MainThread"));
// 創(chuàng)建端口
myPort = CFMessagePortCreateLocal(NULL,
myPortName,
&MainThreadResponseHandler,
&context,
&shouldFreeInfo);
if (myPort != NULL)
{
// 端口已經(jīng)被成功創(chuàng)建
// 現(xiàn)在為他創(chuàng)建 run loop source
rlSource = CFMessagePortCreateRunLoopSource(NULL, myPort, 0);
if (rlSource)
{
// 為當(dāng)前 run loop 添加 source
CFRunLoopAddSource(CFRunLoopGetCurrent(), rlSource, kCFRunLoopDefaultMode);
// 一旦安裝結(jié)束,這些資源需要被釋放
CFRelease(myPort);
CFRelease(rlSource);
}
}
// 創(chuàng)建線程并且繼續(xù)處理任務(wù)
MPTaskID taskID;
return(MPCreateTask(&ServerThreadEntryPoint,
(void*)myPortName,
kThreadStackSize,
NULL,
NULL,
NULL,
0,
&taskID));
}
如果 port 端口已經(jīng)被安裝并且線程已經(jīng)啟動(dòng),主線程就可以繼續(xù)定期的執(zhí)行去等待輔助線程的 check-in 登記信息。一旦 check-in 登記信息到達(dá),它將會(huì)被指派到主線程的 MainThreadResponseHandler
函數(shù)中,如表 3-18 所示,這個(gè)函數(shù)提取輔助線程的端口名并且創(chuàng)建通信管道。
表 3-18 接收 check-in 登記信息
#define kCheckinMessage 100
// 主線程端口信息處理函數(shù)
CFDataRef MainThreadResponseHandler(CFMessagePortRef local,
SInt32 msgid,
CFDataRef data,
void* info)
{
if (msgid == kCheckinMessage)
{
CFMessagePortRef messagePort;
CFStringRef threadPortName;
CFIndex bufferLength = CFDataGetLength(data);
UInt8* buffer = CFAllocatorAllocate(NULL, bufferLength, 0);
CFDataGetBytes(data, CFRangeMake(0, bufferLength), buffer);
threadPortName = CFStringCreateWithBytes (NULL, buffer, bufferLength, kCFStringEncodingASCII, FALSE);
// 你必須通過一個(gè) port 名獲取遠(yuǎn)端信息
messagePort = CFMessagePortCreateRemote(NULL, (CFStringRef)threadPortName);
if (messagePort)
{
// 保留并保存線程的 comm 端口 以備后用
AddPortToListOfActiveThreads(messagePort);
// 如果端口在先前的 函數(shù) 中保留了(retain),在這里釋放資源
CFRelease(messagePort);
}
// 釋放資源
CFRelease(threadPortName);
CFAllocatorDeallocate(NULL, buffer);
}
else
{
// 處理其他信息
}
return NULL;
}
主線程配置完成后,唯一要做的就是為新創(chuàng)建的輔助線程創(chuàng)建它自己的 端口和 登記自己的 message。表 3-19 所示為輔助線程的入口點(diǎn)函數(shù)。函數(shù)提取了主線程的 port 名并且用它創(chuàng)建了一個(gè)遠(yuǎn)端的連接回復(fù)主線程。之后函數(shù)為自己創(chuàng)建一個(gè)本地端口,將端口 port 安裝到線程的 runloop 中去,然后給主線程發(fā)送一個(gè)包含本地端口名的 check-in 登記信息。
OSStatus ServerThreadEntryPoint(void* param)
{
// 創(chuàng)建連接到主線程的遠(yuǎn)端端口
CFMessagePortRef mainThreadPort;
CFStringRef portName = (CFStringRef)param;
mainThreadPort = CFMessagePortCreateRemote(NULL, portName);
// 釋放被用于參數(shù)傳遞的字符串
CFRelease(portName);
// 為輔助才女創(chuàng)建一個(gè)本地端口
CFStringRef myPortName = CFStringCreateWithFormat(NULL, NULL, CFSTR("com.MyApp.Thread-%d"), MPCurrentTaskID());
// 保存線程上下文信息中的端口,以便之后使用。
CFMessagePortContext context = {0, mainThreadPort, NULL, NULL, NULL};
Boolean shouldFreeInfo;
Boolean shouldAbort = TRUE;
CFMessagePortRef myPort = CFMessagePortCreateLocal(NULL,
myPortName,
&ProcessClientRequest,
&context,
&shouldFreeInfo);
if (shouldFreeInfo)
{
// 如果不能創(chuàng)建本地端口,則殺死線程
MPExit(0);
}
CFRunLoopSourceRef rlSource = CFMessagePortCreateRunLoopSource(NULL, myPort, 0);
if (!rlSource)
{
// 如果不能創(chuàng)建本地端口,則殺死線程
MPExit(0);
}
// 給 runloop 添加source
CFRunLoopAddSource(CFRunLoopGetCurrent(), rlSource, kCFRunLoopDefaultMode);
// 一旦線程安裝完畢,這些資源需要釋放
CFRelease(myPort);
CFRelease(rlSource);
// 打包端口名,并發(fā)送 check-in 信息。
CFDataRef returnData = nil;
CFDataRef outData;
CFIndex stringLength = CFStringGetLength(myPortName);
UInt8* buffer = CFAllocatorAllocate(NULL, stringLength, 0);
CFStringGetBytes(myPortName,
CFRangeMake(0,stringLength),
kCFStringEncodingASCII,
0,
FALSE,
buffer,
stringLength,
NULL);
outData = CFDataCreate(NULL, buffer, stringLength);
CFMessagePortSendRequest(mainThreadPort, kCheckinMessage, outData, 0.1, 0.0, NULL, NULL);
// 清除線程數(shù)據(jù)結(jié)構(gòu)
CFRelease(outData);
CFAllocatorDeallocate(NULL, buffer);
// 進(jìn)入 runloop
CFRunLoopRun();
}
一旦進(jìn)入 runloop,所有發(fā)送給線程端口的事件會(huì)被 ProcessClientRequest
函數(shù)處理。該函數(shù)的實(shí)現(xiàn)依賴于工作線程的類型,這里暫不做介紹。