RunLoop深度探究(五)

RunLoop深度探究(四)

我的博客鏈接:http://superyang.gitcafe.io/blog/2016/01/18/runloop-5/

使用 Run Loop 對象

一個 run loop 對象提供了一些主要接口用于向你的 run loop 中添加 input source ,timers, 和run loop observer,并且運行它。每一條線程有且只有一個run loop 與他相關聯。在 Cocoa 中,這個對象是 NSRunLoop 類的一個實例。在底層的應用中,它是指向 CFRunLoopRef 這種不透明類型的一個指針。

獲取 Run Loop 對象

你需要使用以下其中之一來獲取當前線程的 Run Loop :

盡管這兩種方法不是 toll-free bridged type(在Foundation 和 Core Foundation 中擁有等價替換接口的能力的類型)的類型,但是如果你需要可以從 NSRunLoop 對象里拿到 CFRunLoopRef 這種不透明類型(蘋果封裝在內部的C語言類型)。NSRunLoop 類定義了 getCFRunLoop 方法用來返回一個可以傳入到 Core Foundation 代碼中的 CFRunLoopRef 類型的C語言指針對象(結構體指針)。這兩種對象都可以來自于同一個 run loop,你可以根據你的需要來選擇具體使用 NSRunLoopCFRunLoopRef 這兩種對象的哪一種。

配置 Run Loop

在你運行一個子線程的 run loop 之前,你必須向其添加至少一個 input source 或者 timer。如果 run loop 沒有任何需要監視的 source, 它將會在你嘗試運行它的時候立即退出。請參考配置RunLoop Sounce(本文接下來的章節將有介紹)。

除了安裝 source,你還可以 run loop observer 并且使用他們檢測 runloop的處于不同執行階段。為了安裝 run loop observer ,你需要創建一個 CFRunLoopObserverRef 不透明類型的指針并使用 CFRunLoopAddObserver 函數將 Observer 添加到你的 run loop 中去,Run Loop Observer 必須使用 Core Foundation 框架接口創建,在 Cocoa 應用中也一樣。

表 3-1 展示了在線程 runloop 中,添加 run loop Observer 的主要代碼流程。本例的目的旨在告訴你如何創建一個 run loop Observer, 所以代碼只是簡單設置了一個run loop Observer 用來監視 run loop 的所有活動 。基本的處理代碼(沒有展示)僅僅是日志輸出 run loop 的各項活動行為 作為 timer 的事件回調。

表3-1 創建 runloop Observer

- (void)threadMain {
    // 應用使用垃圾回收,所以不需要 自動釋放池 autorelease pool
    NSRunLoop *myRunLoop = [NSRunLoop currentRunLoop];
    // 創建一個 run loop observer 并且將他添加到當前 run loop 中去
    /*!
     *  @author 楊超, 16-01-13 15:01:45
     *
     *  @brief CFRunLoopObserverContext 用來配置 CFRunLoopObserver 對象行為的結構體
     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 結構體版本號,必須為0
     *  @param info 一個程序預定義的任意指針,可以再 run loop Observer 創建時為其關聯。這個指針將被傳到所有 context 多定義的所有回調中。
     *  @param retain 程序定義 info 指針的內存保留(retain)回調,可以為 NULL
     *  @param release 程序定義 info 指針的內存釋放(release)回調,可以為 NULL
     *  @param copyDescription 程序定于 info 指針的 copy 描述回調,可以為 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);
    }
    
    // 創建并安排好 timer
    [NSTimer scheduledTimerWithTimeInterval:0.1 target:self selector:@selector(doFireTimer) userInfo:nil repeats:YES];
    NSInteger loopCount = 10;
    do {
        // 3秒后運行 run loop 實際效果是每三秒進入一次當前 while 循環
        [myRunLoop runUntilDate:[NSDate dateWithTimeIntervalSinceNow:3]];
        loopCount --;
    } while (loopCount);
}

void myRunLoopObserverCallBack(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info) {
    NSLog(@"observer正在回調\n%@----%tu----%@", observer, activity, info);
}

- (void)doFireTimer {
    NSLog(@"計時器回調");
}

當為一個長期存活的現場配置 runloop 時,至少添加一個 input source 去接收消息。盡管你可以僅僅使用一個 關聯的timer 就可以進入 run loop,一旦 timer 啟動,通常都會被作廢掉,這將會硬氣 run loop 的退出。關聯一個重復執行的 timer 定時器可以保持讓 runloop 在很長的一段時期內得以運行,但是需要周期性的去啟動定時器 timer 來喚醒你的線程,這是投票有效的另一種形式(這句莫名其妙,不懂是干嗎的)。相比之下, input source 會等待事件的發生,并保持線程處于睡眠狀態直到事件確實發生了。

開動 run loop

在應用中,只有在子線程中才是有必要開啟 run loop 的,一個 run loop 必須至少有一個用來監視的 input source 。如果一個關聯的都沒有,run loop 將會立即退出。

下面有一些方法開啟 run loop:

  • 無條件的
  • 通過一套時間限制
  • 在一個特別的 mode 下

無條件的進入你的 run loop 是最簡單的選項,但這種也是最不可取的。無條件地運行你的 run loop 將會使你的線程進入進入永久的循環中,這使你很難控制運行循環本身。你可以添加和移除 input source 和 timer,但是只有一種方式去停止 run loop,那就是將它殺死。同時也不存在在自定義 mode 中運行 run loop 的方法。

為了替代無條件的運行 run loop ,更好的辦法是使用超時值來運行 runloop。當你使用超時值時,run loop 會一直運行直到在事件來臨時 或者 分配的時間結束時。當你的事件到達時,系統會分配一個 handler 去處理它,并且之后 run loop 會退出。你可以用代碼重啟你的 run loop 以便處理下一個事件。如果不想繼續使用剛才分配時間結束的原則,也可以簡單的重啟 runloop 或者使用這些時間去做任何你需要做的事。

除了使用超時值,你也可以使用指定的 mode 運行 run loop。mode 和超時值不會互相排斥,并且都可以用來啟動一個線程。

表 3-2 展示了一個線程入口的常用的例行程序。示例代碼的關鍵部分展示了一個 run loop 的基礎架構。本質上,你將 input sources 和 timers 添加到你的 runloop 中,然后重復的調用其中一個例行程序來啟動 run loop 。每一次例行程序返回時,你需要檢查一下是否滿足可能會退出線程的條件。示例使用了 Core Foundation 的框架的例行程序以便檢查返回結果并且可以決定如何退出 runloop。如果你是用的是 Cocoa ,你也可以使用類似的方式通過 NSRunLoop 的方法去運行 runloop , 并且不需要檢查返回值。(使用 NSRunLoop 的方法的例子可以參考 表3-14.)

表 3-2 運行 runloop

- (void)skeletionThreadMain {
    // 如果你的應用沒有使用垃圾回收 請在這里添加 自動釋放池(ps:這示例代碼也太老了,誰還用垃圾回收啊)
    
    BOOL done = NO;
    
    // 給 runloop 添加 source 或timer,然后做一些其他的配置
    
    do {
        // 開啟 runloop 并且被一個 source 被處理后要返回
        /** SInt32 32位有符號整數 */
        SInt32 result = CFRunLoopRunInMode(kCFRunLoopDefaultMode, 10, YES);
        
        // 如果 source 已經顯式的停止了 runloop ,或者根本不存在任何 source 或 timer,將會退出。
        if ((result == kCFRunLoopRunStopped) || (result == kCFRunLoopRunFinished)) {
            done = YES;
            // 在這里檢查任何其他符合退出的條件并且按需設置 done 變量的值。
        }
    } while (!done);
    
    // 在這里清除代碼。確保釋放任何之前創建的自動釋放池。
}

可以遞歸開啟 runloop,換句話說,你可以使用 input source 或者 timer 的例行程序來調用 CFRunLoopRun,CFRunLoopRunInMode或者任何 NSRunLoop 的 runloop 啟動方法。這樣做你可以使用任何你想用的 mode 來運行一個 嵌套的 run loop ,包括 通過外層 run loop 使用的 mode 。

退出 RunLoop

有兩種途徑可以讓 runloop 在處理事件之前退出:

  • 使用超時值配置 runloop 運行。
  • 直接告訴 runloop 停止(ps:。。。這條太搞了)。

使用超時值無疑是更偏愛的方法,如果你能管理它,指定一個超時值使 runloop 結束所有他的正常處理的任務, 包括在退出前向 runloop observer 發送通知。

使用 CFRunLoopStop 函數顯示地停止 runloop,產生的結果和超時相似。runloop 會發送任何 runloop 提醒通知然后才退出。不同的是你可以將這項技術應用在你用無條件方式開啟的 runloop 上。

盡管移除一個 runloop 的 input source 和 timer 可以造成 runloop 的退出,但這并不是一個可靠的方式來停止 runloop 。一些系統例行程序給 runloop 添加一些 input source 來處理必要的事件。你的代碼可能無法看出這些 input source,你可能不能移除這些用來防止 runloop 退出的 source。

線程安全 和 Run Loop 對象

線程安全大多取決于你用來操作 runloop 的API。Core Foundation 函數 一般來說都是線程安全的,所以可以被任何線程調用。假如你正在執行一個修改 runloop 配置的操作,那么繼續吧,對擁有 runloop 的線程來說這樣做仍然是很好的作法。

Cocoa 的 NSRunLoop 類內部不像 Core Foundation 中的接口那樣是線程安全的。如果你要使用 NSRunLoop 類去修改你的 runloop,你只能在 runloop 所在的線程中這樣做。先其他線程中的 runloop 中添加 input source 或 timer 會引起你的程序崩潰或出現不可預知的異常。

配置 run loop source

接下來的章節將展示如何在 Cocoa 和 Core Foundation 中設置不同類型的 input source。

定義一個自定義自定義 input source

創建一個自定義的 input source 你需要實現以下這些條件:

  • 你想要你的 source 處理的信息
  • 一段調度模塊的例行程序讓感興趣的客戶機了解如何連接你的 input source。
  • 一段處理模塊例行程序用來處理任何客戶機發送的請求
  • 一段取消模塊的例行程序用來銷毀你的 source

因為你創建了一個自定義的 input source 來處理自定義的信息,所以實際上的配置會設計的非常靈活。調度模塊,處理模塊和取消模塊的例行程序幾乎都是你的自定義 input source 的關鍵例行程序。剩下的大多數 input source 行為都發生在這些例行處理程序之外。比如,由你來定義一個工具用來將數據傳到你的 input source并且傳遞你的 input source 的數據到其他線程中去。

插圖 3-2 展示了一個簡單的自定義 input source 的配置。在本例中,應用程序主線程維持引用了input source , input source 的緩沖模塊,還有安裝 input source 的 runloop。當主線程有一個任務向切換到工作子線程中去,他會發送一個命令,命令緩沖區以及啟動任務所需的任何線程的信息(因為主線程和工作子線程的 input source 都有權限去訪問命令緩沖區,訪問必須同步)一旦命令發送了,主線程會發送信號給 input source 來喚醒工作子線程的 runloop。一旦受到喚醒的命令, runloop 會調用 input source 的處理程序 去處理命令緩存器中緩存的命令。

圖 3-2 操作一個自定義 input source

圖 3-2
圖 3-2

接下來的章節將會解釋如何通過上圖實現一個自定義 input source 并展示你需要實現的關鍵代碼。

定義 input source

定義一個自定義 input source 需要使用 Core Foundation 的例行程序配置你的 runloop input source 并且 將它與你的 runloop 關聯。盡管基礎處理程序是基于 C-語言 函數的,但這不會阻止你使用 Objective-C 或者 C++ 去封裝它為面向對象的代碼。

插圖3-2中介紹的 input source 使用一個 objective-C 對象去管理一個命令緩存器,并與 runloop 進行協調。列表3-3 展示了這個對象的定義。RunLoopSource 對象管理一個命令緩沖器,并且使用命令緩存器接受來自其他線程的消息。該表也展示了 RunLoopContext 對象的定義,該對象僅僅是一個容器,用來傳遞一個 RunLoopSource 對象和應用主線程的 runloop 引用。

表 3-3 自定義 input source 對象的定義

@interface YCRunLoopSource : NSObject
{
    CFRunLoopSourceRef runLoopSource;
    NSMutableArray *commands;
}

- (id)init;
// 添加
- (void)addToCurrentRunLoop;
// 銷毀
- (void)invalidate;

// 處理方法
- (void)sourceFired;

// 用來注冊需要處理的命令的客戶機接口
- (void)addCommand:(NSInteger)command withData:(id)data;
- (void)fireAllCommandsOnRunLoop:(CFRunLoopSourceRef)runloop;

// 這些是CFRunLoopRef 的回調函數
/** 調度函數 */
void RunLoopSourceScheduleRoutine(void *info, CFRunLoopRef r1, CFStringRef mode);
/** 處理函數 */
void RunLoopSourcePerformRoutine (void *info);
/** 取消函數 */
void RunLoopSourceCancelRoutine (void *info, CFRunLoopRef rl, CFStringRef mode);

@end

// RunLoopContext 是一個 在注冊 input source 時使用的容器對象

@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 的自定義數據。關聯一個 input source 到一個具備 基于 C-語言 的回調函數的 runloop 。其中第一個函數是當你實際將 input source 添加到 runloop 中的時刻調用。流程將展示在 表 3-4 中。因為這個 input source 僅只有一個 客戶機(主線程)。它使用調度者函數通過目標線程 application 的代理發送消息在目標線程注冊自己。當 application 的代理和 input source 進行通信時 ,會使用 RunLoopContext 對象中的 info 信息來完成這個事。

表 3-4 調度 run loop source

void RunLoopSourceScheduleRoutine(void *info, CFRunLoopRef r1, CFStringRef mode){
    YCRunLoopSource *obj = (__bridge YCRunLoopSource *)info;
    // 這里的 Appdelegate 是主線程的代理  
    AppDelegate *del = [AppDelegate sharedAppDelegate];
    
    // 上下文對象中持有source自己
    YCRunLoopContext *theContext = [[YCRunLoopContext alloc] initWithSource:obj andLoop:r1];
    // 通過代理去注冊 Source 自己 
    [del performSelectorOnMainThread:@selector(registerSource:) withObject:theContext waitUntilDone:NO];
    
}

其中最重要的回調例行程序是當你的 input source 被信號激活時處理自定義數據的部分。表3-5中展示了與 RunLoopSource 對象關聯的執行者回調例行程$序,這個函數僅僅轉發用來 sourceFired 方法工作的請求,該請求用來處理任何 command buffer (命令緩沖區)中存在的命令。

表3-5 input source 中的執行者

void RunLoopSourcePerformRoutine (void *info)
{
    RunLoopSource*  obj = (RunLoopSource*)info;
    [obj sourceFired];
}

如果你使用 CFRunLoopSourceInvalidate 函數將 input source 從 runloop 重移除。系統會調用你的 input source 中的取消者例行程序。你可以利用這個例行程序去通知客戶機你的 input source 不再可用并且他們應該移除任何自己的相關的引用。表3-6 展示了取消者例行回調程序通過 RunLoopSource 對象進行注冊。這個函數發送另一個 RunLoopContext 對象給 application 代理。但是這讓代理去移除 runloop surce 的相關引用。

表3-6 銷毀一個 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];
}

筆記:應用代理方法 registerSource: 和 removeSource 方法在下面的章節 《協調 input source 的客戶機》展示

為 runloop 安裝 input source

表3-7 展示了 RunLoopSource 類的 init 方法 和 addToCurrentRunLoop 方法。init 方法創建了 CFRunLoopSource 不透明類型的必須關聯到 runloop 的對象。它會傳遞 RunLoopSource 對象自己作為 山下文信息 以便于例行回調程序有一個指向對象的指針。input source 直到線程喚起 addToCurrentRunLoop 方法時才會執行安裝,準確將在 RunLoopSourceScheduleRoutine 回調函數調用時。 一旦 input source 安裝到 runloop 中,線程將會運行自己的 runloop 去等待 input source 發出事件。

表3-7 安裝 run loop source

- (id)init {
    // 創建上下文容器,其中會連接自己的 info,retain info release info,還會關聯三個例行程序。
    CFRunLoopSourceContext context = {0, (__bridge void *)(self), NULL, NULL, NULL ,NULL, NULL, &RunLoopSourceScheduleRoutine, RunLoopSourceCancelRoutine, RunLoopSourcePerformRoutine};
    /** 通過索引,上下文,和CFAllocator創建source */
    runLoopSource = CFRunLoopSourceCreate(NULL, 0, &context);
    commands = [[NSMutableArray alloc] init];
    return  self;
}

- (void)addToCurrentRunLoop{
    CFRunLoopRef runLoop = CFRunLoopGetCurrent();
    CFRunLoopAddSource(runLoop, runLoopSource, kCFRunLoopDefaultMode);
}

協調 input source 的客戶機

對于你的 input source 會非常有用,你需要操作它并且從其他線程向它提供消息。input source 的要點是將其添加到線程并睡眠直到有事情要做時才喚醒。事實上很有必要讓其他線程了解 input surce 并且有方法可以和它交流(溝通數據)。

通知你的 input source 客戶機的方法之一是發出注冊請求 當你的 input source 第一次安裝到你的 runloop 中時。你可以向你的 input source 注冊盡可能多的客戶機。或者你僅僅只是簡單的用一些中央機構,然后將你的 input source 聲明為感興趣的客戶端進行注冊。表3-8 展示了 通過代理 和 調用喚起定義的 注冊方法 當 RunLoopSource 對象的調度者函數被調用時。這個方法將會收到 RunLoopSource 提供的 RunLoopContext 對象并且將它添加到他的 source 列表中。這個表也會展示 當 input source 從 他的 runloop 中被移除時 用來注銷的例行程序。
表 3-8 使用 application 的 代理 注銷并且移除 input source

 #import "YCRunLoopSource.h"
 #import "YCRunLoopContext.h"
@interface AppDelegate : NSObject
@property (nonatomic, strong) NSMutableArray *sourcesToPing;

/** 應該是一個單例 */
+ (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:回調函數會在之前的表3-4和3-6中調用這些函數

信號激活 input source

釋放 input source 的數據之后,客戶機必須發信號給 source 并且喚醒它的 runloop。發信號給 source 是讓 runloop 知道 source 已經準備好被處理。因為線程可能會在發信號的時處于睡眠狀態,所以那你必須顯式的讓 run loop 保持喚醒。除非如此,不然在處理 input source 時會出現延遲。

表 3-9 展示了 RunLoopSource 對象的 fireCommandsOnRunLoop 方法,客戶機會在它準備好為 source 處理添加到 buffer 緩沖區中的 command 命令時調用這個方法。

表 3-9 喚醒 run loop

- (void)fireCommandsOnRunLoop:(CFRunLoopRef)runloop
{
    CFRunLoopSourceSignal(runLoopSource);
    CFRunLoopWakeUp(runloop);
}

Note:你不能通過向一個自定義 input source 發信息來處理一個 SIGHUP 或者其他處理類型的信號,Core Foundation 框架中用于喚醒 runloop 的函數不是信號安全的。并且不能作為你的應用程序中內置信號處理的例行程序使用。關于更多的關于信號處理程序,詳見 sigaction man 頁面。

配置 Timer Source

為了創建 timer source,所有你需要做的就是創建一個 timer 對象,并且在你的 run loop 中調度它。在 Cocoa 中,你使用 NSTimer 類來創建一個新的 timer 對象。在 Core Foundation 框架中,你可以使用 CFRunLoopTimerRef 不透明類型來創建。NSTimer 類只是 Core Foundation 框架中的一個擴展,是用來方便的提供一些功能,比如使用相同的方法創建和調度 timer 。
在 Cocoa 中,你能通過以下兩種類方法創建和調度 timer。
scheduledTimerWithTimeInterval:target:selector:userInfo:repeats:
scheduledTimerWithTimeInterval:invocation:repeats:

這些方法創建 timer 并且將它們添加到當前線程的 run loop 中的 default mode(NSDefaultRunLoopMode) 中去。如果你使用的是 NSTimer 對象,那就可以手動調度 timer 并且可以使用 NSRunLoopaddTimer:forMode: 手動將它添加到 runloop 中去。這兩種技術都是基于同一種,但是通過timer 的配置給你不同級別的控制。比如你手動創建 timer 并將它添加到 run loop 中,并添加到除 default mode 之外的其他 mode 中去。表3-10 展示了如何使用兩種技術創建 timer。第一個 timer 初始化為 延遲一秒但是會在延遲后有規律的每個0.1秒觸發一次。第二個 timer 會在 0.2 秒延遲后開始觸發,并且在延遲結束后 每 0.2 秒觸發一次。

表3-10 使用 NSTimer 創建和調度 timer

NSRunLoop* myRunLoop = [NSRunLoop currentRunLoop];
 
// 創建并調度第一個 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];
 
// 創建并調動第二個 timer
[NSTimer scheduledTimerWithTimeInterval:0.2
                        target:self
                        selector:@selector(myDoFireTimer2:)
                        userInfo:nil
                        repeats:YES];

表3-11 展示了使用 Core Foundation 框架時需要配置的代碼。盡管實例代碼中沒有傳遞任何用戶自定義的信息的上下文結構,但是你可以使>用這個結構去傳遞任何你的 timer 所需要自定義數據。關于更多該結構的內容可以瀏覽 CFRunLoopTimer 參考

表 3-11 使用 Core Foundation 框架創建和調度一個 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);

配置一個基于 port 的 input source

Cocoa 和 Core Foundation 都支持用于和線程間或者進程間通信的基于 端口的對象。接下來的章節將會向你展示如何使用一些不同類型的 port 構建 port 通信。

配置一個NSMachPort Object

使用 NSMachPort 對象創建一個本地連接。你創建一個 port 對象并把它添加到你的主線程 run loop 中去。當啟動你的子線程時,你要傳一些相同的對象到你的線程入口點函數中去。子線程可以使用相同的對象發送信息回到你的主線程中去。

實現主線程代碼

表 3-12 中展示了用于啟動子工作線程的主線程代碼。因為 Cocoa 框架執行很多介入步驟用于配置 port 和 run loop ,Cocoa 的 launchThread 方法相比于 Core Foundation 的等價功能表 3-17更加簡潔明了。盡管如此,這兩個框架在這一模塊的功能表現基本都是相同的。其中一個存在的差異是與發送本地 port 到工作線程的方式不同,這個方法是直接發送 NSPort 對象的。

表 3-12 list3-12 Main Thread lauch method

- (void)launchThread {
    NSPort *myPort = [NSMachPort port];
    if (myPort) {
        // 這個類處理即將過來的 port 信息
        [myPort setDelegate:self];
        // 將此端口作為 input source 安裝到當前 run loop 中去
        [[NSRunLoop currentRunLoop] addPort:myPort forMode:NSDefaultRunLoopMode];
        // 開啟工作子線程,讓工作子線程去釋放 port
        [NSThread detachNewThreadSelector:@selector(LaunchThreadWithPort:) toTarget:[MyWorkerClass class] withObject:myPort];
    }
}

為了設置為線程間雙向通信信
道,在
登記信息中,你需要讓工作線程發送自己的本地 port 到主線程。接收登記信息是為了讓你的主線程知道開動子線程的過程進行的非常順利,同時也為我們為提供了一種方法去向該線程發送更多信息。

表 3-13 展示了用于主線程的handlePortMessage:方法,這個方法會在線程到達自己的本地 port 時進行調用。當登記信息(check-in message)到達時,該方法將直接從 port 信息中檢索子線程的 port 并保存以備后用。

表 3-13 處理 Mach port 信息


# define kCheckinMessage 100

// 處理工作線程的響應的代理方法
- (void)handlePortMessage:(NSPortMessage *)portMessage
{
    unsigned int message = [portMessage msgid];
    // 定義遠程端口
    NSPort *distantPort = nil;
    if (message == kCheckinMessage) {
        // 獲取工作線程的通信 port
        distantPort = [portMessage sendPort];
        
// 引用計數+1 并 保存工作端口以備后用
        [self storeDistantPort:distantPort];
    } else {
        // 處理其他信息
    }
}

- (void)storeDistantPort:(NSPort *)port {
    // 保存遠程端口
}

實現子線程代碼

對于工作子線程,你必須配置它并且是使用指定的端口進行信息溝通并返回到主線程。

表 3-14 展示了用于設置工作線程的代碼。在創建一個 qutorealease pool 之后,該方法會創建一個工作對象去驅動線程執行。該工作對象 的 sendCheckinMessage: 方法(表3-15 所示)為工作線程創建一個本地端口然后回復一個 check-in 信息給主線程。
表 3-14 使用 Mach port 啟動子線程

+(void)LaunchThreadWithPort:(id)inData
{
    NSAutoreleasePool*  pool = [[NSAutoreleasePool alloc] init];
 
    // 設置本線程與主線程的連接 
    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];
}

當使用 NSMachPort 時,本地和遠端線程都可以使用相同的 port 對象 完成線程之間的單工通信(單向通信)。換句話說,通過一個線程創建的本地對象會成為另一個線程的遠端 port 對象。(ps:現在總算明白本地就是當前線程環境,遠端就是其他線程環境)。

表 3-15展示了子線程的 check-in 例行程序 (登記信息例行程序)。這個方法設置了他自己的用于和以后進行通訊的本地端口。并且回復一個 check-in 登記信息給主線程。該方法使用 port 對象去接收 LaunchThreadWithport: 方法作為信息目標。

表 3-15 使用 Mach port 發送 check-in 登記信息

// Worker thread check-in method
- (void)sendCheckinMessage:(NSPort*)outPort
{
    // 保留(retain)并保存遠端的 port 以備后用
    [self setRemotePort:outPort];
 
    // 創建和配置工作線程的端口(ps:當前線程端口)  
    NSPort* myPort = [NSMachPort port];
    [myPort setDelegate:self];
    [[NSRunLoop currentRunLoop] addPort:myPort forMode:NSDefaultRunLoopMode];
 
    // 創建 check-in 登記信息
    NSPortMessage* messageObj = [[NSPortMessage alloc] initWithSendPort:outPort
                                         receivePort:myPort components:nil];
 
    if (messageObj)
    {
        // 完成配置信息 并 立即發送出去
        [messageObj setMsgId:setMsgid:kCheckinMessage];
        [messageObj sendBeforeDate:[NSDate date]];
    }

配置一個 NSMessagePort 對象

如果想要使用 [NSMessagePort](https://developer.apple.com/library/ios/documentation/Cocoa/Reference/Foundation/Classes/NSM essagePort_Class/index.html#//apple_ref/occ/cl/NSMessagePort) 對象創建一個本地連接,你不能在線程間僅僅值傳遞一個 port 對> 象。遠端信息端口必須通過名字獲取。
在Cocoa中,如果你想實現這個功能,需要使用一個指定的名字去注冊你的本地端口,然后向遠端線程傳遞注冊的名字以便于他可以包含一
個合適的端口對象用于交流。表 3-16 展示了 port 創建方法和注冊方法 用于你想要使用 消息端口(message port)的地方。

表 3-16 注冊一個 message port

NSPort* localPort = [[NSMessagePort alloc] init];

// 配置對象并將它添加到當前 run loop 中去
[localPort setDelegate:self];
[[NSRunLoop currentRunLoop] addPort:localPort forMode:NSDefaultRunLoopMode];

// 使用指定的名字注冊端口。名字必須唯一。
NSString* localPortName = [NSString stringWithFormat:@"MyPortName"];
[[NSMessagePortNameServer sharedInstance] registerPort:localPort
name:localPortName];

在 Core Foundation 框架中配置一個基于端口的(Port-Based) input source

這個小結描述了如歌使用 Core Foundation 框架在你的應用的主線程和輔助線程(worker thread)中創建一個雙向通信信道。

如表3-17 所示為應用主線程啟動輔助線程所使用的代碼。首先要做的是創建 CFMessagePortRef 不透明對象去監聽從輔助線程發來的消息。輔助線程需要用來創建連接的端口名,以便于字符串值可以被發送到輔助線程的入口點函數。端口名在當前用戶的上線文中通常必須是唯一的。否則,可能會出現運行沖突。

表 3-17 給新線程關聯一個 Core Foundation message port

#define kThreadStackSize        (8 *4096)
 
OSStatus MySpawnThread()
{
    // 創建一個本地端口用于接受響應
    CFStringRef myPortName;
    CFMessagePortRef myPort;
    CFRunLoopSourceRef rlSource;
    CFMessagePortContext context = {0, NULL, NULL, NULL, NULL};
    Boolean shouldFreeInfo;
 
    // 用端口名 創建一個符合規范的字符串
    myPortName = CFStringCreateWithFormat(NULL, NULL, CFSTR("com.myapp.MainThread"));
 
    // 創建端口
    myPort = CFMessagePortCreateLocal(NULL,
                myPortName,
                &MainThreadResponseHandler,
                &context,
                &shouldFreeInfo);
 
    if (myPort != NULL)
    {
        // 端口已經被成功創建
        // 現在為他創建 run loop source
        rlSource = CFMessagePortCreateRunLoopSource(NULL, myPort, 0);
 
        if (rlSource)
        {
            // 為當前 run loop 添加 source
            CFRunLoopAddSource(CFRunLoopGetCurrent(), rlSource, kCFRunLoopDefaultMode);
 
            // 一旦安裝結束,這些資源需要被釋放
            CFRelease(myPort);
            CFRelease(rlSource);
        }
    }
 
    // 創建線程并且繼續處理任務
    MPTaskID        taskID;
    return(MPCreateTask(&ServerThreadEntryPoint,
                    (void*)myPortName,
                    kThreadStackSize,
                    NULL,
                    NULL,
                    NULL,
                    0,
                    &taskID));
}

如果 port 端口已經被安裝并且線程已經啟動,主線程就可以繼續定期的執行去等待輔助線程的 check-in 登記信息。一旦 check-in 登記信息到達,它將會被指派到主線程的 MainThreadResponseHandler 函數中,如表 3-18 所示,這個函數提取輔助線程的端口名并且創建通信管道。

表 3-18 接收 check-in 登記信息

#define kCheckinMessage 100
 
// 主線程端口信息處理函數
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);
 
        // 你必須通過一個 port 名獲取遠端信息
        messagePort = CFMessagePortCreateRemote(NULL, (CFStringRef)threadPortName);
 
        if (messagePort)
        {
        // 保留并保存線程的 comm 端口 以備后用
            AddPortToListOfActiveThreads(messagePort);
 
            // 如果端口在先前的 函數 中保留了(retain),在這里釋放資源
            CFRelease(messagePort);
        }
 
        // 釋放資源
        CFRelease(threadPortName);
        CFAllocatorDeallocate(NULL, buffer);
    }
    else
    {
        // 處理其他信息
    }
 
    return NULL;
}

主線程配置完成后,唯一要做的就是為新創建的輔助線程創建它自己的 端口和 登記自己的 message。表 3-19 所示為輔助線程的入口點函數。函數提取了主線程的 port 名并且用它創建了一個遠端的連接回復主線程。之后函數為自己創建一個本地端口,將端口 port 安裝到線程的 runloop 中去,然后給主線程發送一個包含本地端口名的 check-in 登記信息。

OSStatus ServerThreadEntryPoint(void* param)
{
    // 創建連接到主線程的遠端端口
    CFMessagePortRef mainThreadPort;
    CFStringRef portName = (CFStringRef)param;
 
    mainThreadPort = CFMessagePortCreateRemote(NULL, portName);
 
    // 釋放被用于參數傳遞的字符串
    CFRelease(portName);
 
    // 為輔助才女創建一個本地端口
    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)
    {
        // 如果不能創建本地端口,則殺死線程
        MPExit(0);
    }
 
    CFRunLoopSourceRef rlSource = CFMessagePortCreateRunLoopSource(NULL, myPort, 0);
    if (!rlSource)
    {
        // 如果不能創建本地端口,則殺死線程
        MPExit(0);
    }
 
    // 給 runloop 添加source
    CFRunLoopAddSource(CFRunLoopGetCurrent(), rlSource, kCFRunLoopDefaultMode);
 
    // 一旦線程安裝完畢,這些資源需要釋放
    CFRelease(myPort);
    CFRelease(rlSource);
 
    // 打包端口名,并發送 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);
 
    // 清除線程數據結構
    CFRelease(outData);
    CFAllocatorDeallocate(NULL, buffer);
 
    // 進入 runloop
    CFRunLoopRun();
}

一旦進入 runloop,所有發送給線程端口的事件會被 ProcessClientRequest 函數處理。該函數的實現依賴于工作線程的類型,這里暫不做介紹。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,663評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,125評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,506評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,614評論 1 307
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,402評論 6 404
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,934評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,021評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,168評論 0 287
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,690評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,596評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,784評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,288評論 5 357
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,027評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,404評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,662評論 1 280
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,398評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,743評論 2 370

推薦閱讀更多精彩內容