NSRunloop簡(jiǎn)單細(xì)說(八)—— 幾個(gè)重要的問題(二)

版本記錄

版本號(hào) 時(shí)間
V1.0 2017.08.23

前言

NSRunloopOC Foundation框架中非常重要的一個(gè)類,很多時(shí)候我們會(huì)使用它,但是未必對(duì)其有深入的了解,接下來幾篇我就會(huì)帶著大家重新學(xué)習(xí)一下NSRunloop這個(gè)類,從簡(jiǎn)單到復(fù)雜,從基本到深化,我會(huì)一步步的走完。希望對(duì)大家有所幫助。感興趣的可以看我上一篇。
1. NSRunloop簡(jiǎn)單細(xì)說(一)—— 整體了解
2. NSRunloop簡(jiǎn)單細(xì)說(二)—— 獲取運(yùn)行循環(huán)及其模式
3. NSRunloop簡(jiǎn)單細(xì)說(三)—— 定時(shí)器和端口
4. NSRunloop簡(jiǎn)單細(xì)說(四)—— 開啟Runloop
5. NSRunloop簡(jiǎn)單細(xì)說(五)—— 調(diào)度和取消消息
6. NSRunloop簡(jiǎn)單細(xì)說(六)—— 幾種循環(huán)模式詳細(xì)解析
7. NSRunloop簡(jiǎn)單細(xì)說(七)—— 幾個(gè)重要的問題(一)

一、Timer Sources - 定時(shí)源

定時(shí)器來源將在預(yù)設(shè)的時(shí)間內(nèi)同步向線程傳送事件。 定時(shí)器是線程通知自己做某事的一種方式。 例如,一旦在來自用戶的連續(xù)關(guān)鍵筆劃之間經(jīng)過一定量的時(shí)間,搜索字段就可以使用定時(shí)器來啟動(dòng)自動(dòng)搜索。 使用這個(gè)延遲時(shí)間給用戶在開始搜索之前有機(jī)會(huì)輸入盡可能多的期望的搜索字符串。

雖然它生成基于時(shí)間的通知,但定時(shí)器不是實(shí)時(shí)機(jī)制。 與輸入源一樣,定時(shí)器與運(yùn)行循環(huán)的特定模式相關(guān)聯(lián)。 如果定時(shí)器未處于運(yùn)行循環(huán)當(dāng)前正在監(jiān)視的模式,則在您以其中一個(gè)定時(shí)器支持的模式運(yùn)行運(yùn)行循環(huán)之前,它不會(huì)觸發(fā)。 類似地,如果在運(yùn)行循環(huán)處于執(zhí)行處理程序例程的中間時(shí)定時(shí)器觸發(fā),則定時(shí)器等待直到下一次通過運(yùn)行循環(huán)來調(diào)用其處理程序例程。 如果運(yùn)行循環(huán)沒有運(yùn)行,則定時(shí)器不會(huì)被觸發(fā)。

您可以配置定時(shí)器一次或重復(fù)生成事件。 重復(fù)定時(shí)器根據(jù)預(yù)定的開啟時(shí)間自動(dòng)重新調(diào)度,而不是實(shí)際的開啟時(shí)間。 例如,如果定時(shí)器計(jì)劃在特定時(shí)間和之后的每5秒發(fā)射,則即使實(shí)際的發(fā)射時(shí)間延遲,預(yù)定的發(fā)射時(shí)間將始終落在原始的5秒時(shí)間間隔上。 如果開啟時(shí)間延遲太多,以至于它錯(cuò)過了一個(gè)或多個(gè)預(yù)定的開啟時(shí)間,則定時(shí)器在錯(cuò)過的時(shí)間段內(nèi)僅被觸發(fā)一次。 對(duì)于錯(cuò)過的周期,計(jì)時(shí)器將重新安排下次預(yù)定的開啟時(shí)間。


二、Run Loop Observers - 運(yùn)行循環(huán)觀察

適宜的異步或同步事件發(fā)生后會(huì)觸發(fā)事件源的開始,與這些源相反,在運(yùn)行循環(huán)本身的執(zhí)行期間,運(yùn)行循環(huán)觀察器在特定的位置觸發(fā)。 您可以使用運(yùn)行循環(huán)觀察器來準(zhǔn)備您的線程來處理給定的事件,或者在進(jìn)入休眠之前準(zhǔn)備線程。 您可以在運(yùn)行循環(huán)中將運(yùn)行循環(huán)觀察器與以下事件相關(guān)聯(lián):

  • 運(yùn)行循環(huán)的入口。
  • 當(dāng)運(yùn)行循環(huán)即將處理定時(shí)器時(shí)。
  • 當(dāng)運(yùn)行循環(huán)即將處理輸入源時(shí)。
  • 當(dāng)運(yùn)行循環(huán)即將去休眠時(shí)。
  • 當(dāng)運(yùn)行循環(huán)已經(jīng)被喚醒,但在它已經(jīng)處理了喚醒它的事件之前。
  • 退出運(yùn)行循環(huán)。

您可以使用Core Foundation將運(yùn)行循環(huán)觀察器添加到應(yīng)用程序。 要?jiǎng)?chuàng)建一個(gè)運(yùn)行循環(huán)觀察器,您將創(chuàng)建一個(gè)CFRunLoopObserverRef不透明類型的新實(shí)例。 這種類型跟蹤您的自定義回調(diào)函數(shù)及其感興趣的活動(dòng)。

類似于定時(shí)器,可以使用一次或多次運(yùn)行循環(huán)觀察器。 單次觀察器在觸發(fā)之后將其從運(yùn)行循環(huán)中刪除,而重復(fù)的觀察器則不會(huì)被刪除,您指定觀察者在創(chuàng)建它時(shí)是否運(yùn)行一次或多次。


三、The Run Loop Sequence of Events - 事件的運(yùn)行循環(huán)序列

每次運(yùn)行它時(shí),線程的運(yùn)行循環(huán)將處理掛起的事件,并為任何附加的觀察者生成通知。 這樣做的順序是非常明確的,如下所示:

    1. 通知觀察者已輸入運(yùn)行循環(huán)。
    1. 通知觀察員,任何準(zhǔn)備好的定時(shí)器即將開啟。
    1. 通知觀察員,任何不是基于端口的輸入源即將啟動(dòng)。
    1. 任何可以啟動(dòng)的非基于端口的輸入源啟動(dòng)。
    1. 如果基于端口的輸入源準(zhǔn)備就緒并等待觸發(fā),請(qǐng)立即處理該事件。 轉(zhuǎn)到步驟9。
    1. 通知觀察者線程即將睡眠。
    1. 讓線程睡覺,直到發(fā)生以下事件之一:
    • 事件到達(dá)基于端口的輸入源。
    • 一個(gè)計(jì)時(shí)器觸發(fā)
    • 為運(yùn)行循環(huán)設(shè)置的超時(shí)值過期。
    • 運(yùn)行循環(huán)被明確喚醒。
    1. 通知觀察者線程剛剛醒來。
    1. 處理待處理的事件。
    • 如果用戶定義的定時(shí)器觸發(fā),則處理定時(shí)器事件并重新啟動(dòng)循環(huán)。 轉(zhuǎn)到步驟2。
    • 如果輸入源被觸發(fā),則傳遞該事件。
    • 如果運(yùn)行循環(huán)被明確喚醒但尚未超時(shí),請(qǐng)重新啟動(dòng)循環(huán)。 轉(zhuǎn)到步驟2。
    1. 通知觀察者運(yùn)行循環(huán)已經(jīng)退出。

因?yàn)槎〞r(shí)器和輸入源的觀察者通知在這些事件實(shí)際發(fā)生之前傳遞,所以通知時(shí)間和實(shí)際事件的時(shí)間之間可能存在差距。 如果這些事件之間的時(shí)間是至關(guān)重要的,您可以使用睡眠和睡眠喚醒通知來幫助您將實(shí)際事件之間的時(shí)間相關(guān)聯(lián)。

因?yàn)槎〞r(shí)器和其他周期性事件在運(yùn)行運(yùn)行循環(huán)時(shí)傳遞,規(guī)避該循環(huán)會(huì)破壞這些事件的傳遞。 一個(gè)典型的例子就是,當(dāng)您通過輸入循環(huán)并重復(fù)地從應(yīng)用程序請(qǐng)求事件來實(shí)現(xiàn)鼠標(biāo)跟蹤時(shí),這種行為就會(huì)發(fā)生。 因?yàn)槟拇a直接抓取事件,而不是讓應(yīng)用程序正常發(fā)送這些事件,所以活動(dòng)計(jì)時(shí)器將無法啟動(dòng),直到您的鼠標(biāo)跟蹤程序退出并返回控制應(yīng)用程序。

運(yùn)行循環(huán)可以使用運(yùn)行循環(huán)對(duì)象顯式喚醒。 其他事件也可能導(dǎo)致運(yùn)行循環(huán)被喚醒。 例如,添加另一個(gè)非基于端口的輸入源喚醒運(yùn)行循環(huán),以便可以立即處理輸入源,而不是等待直到發(fā)生其他一些事件。


四、什么時(shí)候使用Run Loop?

您唯一需要顯式運(yùn)行運(yùn)行循環(huán)就是在為應(yīng)用程序創(chuàng)建輔助線程的時(shí)候,您的應(yīng)用程序主線程的運(yùn)行循環(huán)是程序的一個(gè)關(guān)鍵部分。 因此,app框架提供了運(yùn)行主應(yīng)用程序循環(huán)并自動(dòng)啟動(dòng)該循環(huán)的代碼。 iOS中的UIApplication(或OS X中的NSApplication)的運(yùn)行方法作為正常啟動(dòng)順序的一部分啟動(dòng)應(yīng)用程序的主循環(huán)。 如果您使用Xcode模板項(xiàng)目來創(chuàng)建應(yīng)用程序,則不應(yīng)顯式地調(diào)用這些例程。

對(duì)于輔助線程,你也可以理解為子線程。您需要確定是否需要運(yùn)行循環(huán),如果是,請(qǐng)自行配置并啟動(dòng)它。 在所有情況下,您不需要啟動(dòng)線程的運(yùn)行循環(huán)。 例如,如果使用線程執(zhí)行一些長(zhǎng)時(shí)間運(yùn)行和預(yù)定的任務(wù),那么可以避免啟動(dòng)運(yùn)行循環(huán)。 運(yùn)行循環(huán)旨在用于與線程進(jìn)行更多交互的情況。 例如,如果您計(jì)劃執(zhí)行以下操作,則需要啟動(dòng)運(yùn)行循環(huán):

  • 使用端口或自定義輸入源與其他線程通信。
  • 在線程上使用計(jì)時(shí)器。
  • 在Cocoa應(yīng)用程序中使用任何performSelector ...方法。
  • 保持線程執(zhí)行定期任務(wù)

如果您選擇使用運(yùn)行循環(huán),則配置和設(shè)置很簡(jiǎn)單。 與所有線程編程一樣,您應(yīng)該有一個(gè)在適當(dāng)情況下退出輔助線程的計(jì)劃,讓它明確的退出要比強(qiáng)制它退出要好的多。


五、使用 Run Loop對(duì)象

運(yùn)行循環(huán)對(duì)象提供了將輸入源,計(jì)時(shí)器和運(yùn)行循環(huán)觀察器添加到運(yùn)行循環(huán)然后運(yùn)行的主接口。 每個(gè)線程都有一個(gè)與之相關(guān)聯(lián)的運(yùn)行循環(huán)對(duì)象。 在Cocoa中,此對(duì)象是NSRunLoop類的一個(gè)實(shí)例。 在低級(jí)應(yīng)用程序中,它是指向CFRunLoopRef不透明類型的指針。

1. Getting a Run Loop Object - 獲取 Run Loop對(duì)象

要獲取當(dāng)前線程的運(yùn)行循環(huán),請(qǐng)使用以下之一:

  • 在Cocoa應(yīng)用程序中,使用NSRunLoopcurrentRunLoop類方法來檢索NSRunLoop對(duì)象。
  • 使用CFRunLoopGetCurrent函數(shù)

雖然它們不是自由的橋接類型,但是在需要時(shí)可以從NSRunLoop對(duì)象獲取CFRunLoopRef不透明類型。 NSRunLoop類定義了一個(gè)getCFRunLoop方法,該方法返回可以傳遞給Core Foundation例程的CFRunLoopRef類型。 因?yàn)閮蓚€(gè)對(duì)象引用相同的運(yùn)行循環(huán),所以可以根據(jù)需要將NSRunLoop對(duì)象和CFRunLoopRef opaque類型的調(diào)用混合。

2. Configuring the Run Loop - Run Loop的配置

在子線程上運(yùn)行運(yùn)行循環(huán)之前,必須至少添加一個(gè)輸入源或計(jì)時(shí)器。 如果運(yùn)行循環(huán)沒有任何來源進(jìn)行監(jiān)視,則當(dāng)您嘗試運(yùn)行它時(shí)立即退出。

除了安裝源之外,您還可以安裝運(yùn)行循環(huán)觀察器并使用它們來檢測(cè)運(yùn)行循環(huán)的不同執(zhí)行階段。 要安裝運(yùn)行循環(huán)觀察器,請(qǐng)創(chuàng)建CFRunLoopObserverRef opaque類型,并使用CFRunLoopAddObserver函數(shù)將其添加到運(yùn)行循環(huán)中。 必須使用Core Foundation創(chuàng)建運(yùn)行循環(huán)觀察器,即使對(duì)于Cocoa應(yīng)用程序也是如此。

下面代碼顯示了將運(yùn)行循環(huán)觀察器連接到其運(yùn)行循環(huán)的線程的主例程。 該示例的目的是向您展示如何創(chuàng)建運(yùn)行循環(huán)觀察器,因此該代碼只需設(shè)置一個(gè)運(yùn)行循環(huán)觀察器來監(jiān)視所有運(yùn)行循環(huán)活動(dòng)。 在處理定時(shí)器請(qǐng)求時(shí),基本處理程序例程(未顯示)只記錄運(yùn)行循環(huán)活動(dòng)。

//創(chuàng)建運(yùn)行循環(huán)觀察器

- (void)threadMain
{
    // The application uses garbage collection, so no autorelease pool is needed.
    NSRunLoop* myRunLoop = [NSRunLoop currentRunLoop];
 
    // Create a run loop observer and attach it to the run loop.
    CFRunLoopObserverContext  context = {0, self, NULL, NULL, NULL};
    CFRunLoopObserverRef    observer = CFRunLoopObserverCreate(kCFAllocatorDefault,
            kCFRunLoopAllActivities, YES, 0, &myRunLoopObserver, &context);
 
    if (observer)
    {
        CFRunLoopRef    cfLoop = [myRunLoop getCFRunLoop];
        CFRunLoopAddObserver(cfLoop, observer, kCFRunLoopDefaultMode);
    }
 
    // Create and schedule the timer.
    [NSTimer scheduledTimerWithTimeInterval:0.1 target:self
                selector:@selector(doFireTimer:) userInfo:nil repeats:YES];
 
    NSInteger    loopCount = 10;
    do
    {
        // Run the run loop 10 times to let the timer fire.
        [myRunLoop runUntilDate:[NSDate dateWithTimeIntervalSinceNow:1]];
        loopCount--;
    }
    while (loopCount);
}

配置長(zhǎng)時(shí)間存在的線程的運(yùn)行循環(huán)時(shí),最好至少添加一個(gè)輸入源來接收消息。 雖然可以只連接一個(gè)計(jì)時(shí)器來進(jìn)入運(yùn)行循環(huán),但一旦定時(shí)器觸發(fā),通常會(huì)使其無效,這將導(dǎo)致運(yùn)行循環(huán)退出。 安裝重復(fù)的計(jì)時(shí)器可以使運(yùn)行循環(huán)在較長(zhǎng)的時(shí)間內(nèi)運(yùn)行,但是會(huì)涉及到定時(shí)喚醒定時(shí)器來喚醒你的線程,這實(shí)際上是另一種形式的輪詢。 相比之下,輸入源等待事件發(fā)生,保持線程睡著,直到它發(fā)生。

3. Starting the Run Loop - 開啟運(yùn)行循環(huán)

啟動(dòng)運(yùn)行循環(huán)僅對(duì)應(yīng)用程序中的輔助線程是必需的。 運(yùn)行循環(huán)必須至少有一個(gè)輸入源或定時(shí)器來監(jiān)視。 如果沒有附加,運(yùn)行循環(huán)將立即退出。

啟動(dòng)運(yùn)行循環(huán)有幾種方法,包括:

  • 無條件
  • 設(shè)定時(shí)限
  • 在特定模式下

無條件進(jìn)入你的運(yùn)行循環(huán)是最簡(jiǎn)單的選擇,但它也是不太可取的。 無條件地運(yùn)行您的運(yùn)行循環(huán)將線程放入永久循環(huán),這使您對(duì)運(yùn)行循環(huán)本身的控制能力變的很弱。 您可以添加和刪除輸入源和計(jì)時(shí)器,但停止運(yùn)行循環(huán)的唯一方法是將其刪除。 也沒有辦法在自定義模式下運(yùn)行運(yùn)行循環(huán)。

相對(duì)無條件地運(yùn)行運(yùn)行循環(huán),最好用超時(shí)值運(yùn)行運(yùn)行循環(huán)。 當(dāng)您使用超時(shí)值時(shí),運(yùn)行循環(huán)將運(yùn)行,直到事件到達(dá)或分配的時(shí)間到期。 如果一個(gè)事件到達(dá),則將該事件分派到處理程序進(jìn)行處理,然后運(yùn)行循環(huán)退出。 您的代碼可以重新啟動(dòng)運(yùn)行循環(huán)來處理下一個(gè)事件。 如果分配的時(shí)間到期,您可以簡(jiǎn)單地重新啟動(dòng)運(yùn)行循環(huán),或者使用時(shí)間來進(jìn)行所需的內(nèi)務(wù)管理。

除了超時(shí)值之外,您還可以使用特定模式運(yùn)行運(yùn)行循環(huán)。 模式和超時(shí)值不是互斥的,并且可以在啟動(dòng)運(yùn)行循環(huán)時(shí)使用。 模式限制將事件傳遞到運(yùn)行循環(huán)的源的類型。

下面代碼顯示了線程主入口例程。 該示例的關(guān)鍵部分顯示了運(yùn)行循環(huán)的基本結(jié)構(gòu)。 實(shí)質(zhì)上,您將輸入源和計(jì)時(shí)器添加到運(yùn)行循環(huán)中,然后重復(fù)調(diào)用其中一個(gè)例程來啟動(dòng)運(yùn)行循環(huán)。 每次運(yùn)行循環(huán)例程返回時(shí),您都會(huì)檢查是否出現(xiàn)任何可能需要退出線程的條件。 該示例使用Core Foundation運(yùn)行循環(huán)例程,以便它可以檢查返回結(jié)果并確定運(yùn)行循環(huán)退出的原因。 您也可以使用NSRunLoop類的方法以類似方式運(yùn)行運(yùn)行循環(huán),如果您使用Cocoa并且不需要檢查返回值。

//開啟運(yùn)行循環(huán)
- (void)skeletonThreadMain
{
    // Set up an autorelease pool here if not using garbage collection.
    BOOL done = NO;
 
    // Add your sources or timers to the run loop and do any other setup.
 
    do
    {
        // Start the run loop but return after each source is handled.
        SInt32    result = CFRunLoopRunInMode(kCFRunLoopDefaultMode, 10, YES);
 
        // If a source explicitly stopped the run loop, or if there are no
        // sources or timers, go ahead and exit.
        if ((result == kCFRunLoopRunStopped) || (result == kCFRunLoopRunFinished))
            done = YES;
 
        // Check for any other exit conditions here and set the
        // done variable as needed.
    }
    while (!done);
 
    // Clean up code here. Be sure to release any allocated autorelease pools.
}

可以遞歸運(yùn)行一個(gè)運(yùn)行循環(huán)。 換句話說,您可以調(diào)用CFRunLoopRun,CFRunLoopRunInMode或任何NSRunLoop方法從輸入源或計(jì)時(shí)器的處理程序中啟動(dòng)運(yùn)行循環(huán)。 當(dāng)這樣做時(shí),您可以使用任何要運(yùn)行嵌套運(yùn)行循環(huán)的模式,包括外部運(yùn)行循環(huán)使用的模式。

4. Exiting the Run Loop - 退出運(yùn)行循環(huán)

在處理事件之前,有兩種方法使運(yùn)行循環(huán)退出:

  • 配置運(yùn)行循環(huán)以超時(shí)值運(yùn)行。
  • 告訴運(yùn)行循環(huán)停止。

使用超時(shí)值當(dāng)然是首選,如果您可以管理它。 指定超時(shí)值可讓運(yùn)行循環(huán)在退出之前完成其所有正常處理,包括傳遞運(yùn)行循環(huán)觀察器的通知。

使用CFRunLoopStop函數(shù)顯式停止運(yùn)行循環(huán)會(huì)產(chǎn)生類似于超時(shí)的結(jié)果。 運(yùn)行循環(huán)發(fā)出任何剩余的運(yùn)行循環(huán)通知,然后退出。 不同之處在于,您可以在無條件啟動(dòng)的運(yùn)行循環(huán)中使用此技術(shù)。

雖然刪除運(yùn)行循環(huán)的輸入源和定時(shí)器也可能導(dǎo)致運(yùn)行循環(huán)退出,但這不是停止運(yùn)行循環(huán)的可靠方法。 一些系統(tǒng)例程將輸入源添加到運(yùn)行循環(huán)以處理所需的事件。 因?yàn)槟愕拇a可能不知道這些輸入源,它將無法刪除它們,這將阻止運(yùn)行循環(huán)退出。

5. Thread Safety and Run Loop Objects - 線程安全和 Run Loop對(duì)象

線程安全性取決于您用來操作運(yùn)行循環(huán)的API。 Core Foundation中的功能通常是線程安全的,可以從任何線程調(diào)用。 但是,如果您正在執(zhí)行更改運(yùn)行循環(huán)的配置的操作,那么盡可能從擁有運(yùn)行循環(huán)的線程執(zhí)行此操作仍然是最佳做法。

Cocoa NSRunLoop類并不像Core Foundation對(duì)象那樣固有線程安全。 如果您正在使用NSRunLoop類來修改運(yùn)行循環(huán),那么只能從擁有該運(yùn)行循環(huán)的同一線程執(zhí)行此操作。 將輸入源或計(jì)時(shí)器添加到屬于不同線程的運(yùn)行循環(huán)可能會(huì)導(dǎo)致您的代碼崩潰或出現(xiàn)意想不到的方式行為。

后記

未完,待續(xù)~~~

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容