1. 程式人生 > >深入理解Runloop,看我一篇就夠了

深入理解Runloop,看我一篇就夠了

前言

RunLoop 是 iOS 和 OSX 開發中非常基礎的一個概念,為了讓大家更加快速融入,請先一段程式碼:

+ (NSThread *)networkRequestThread {
    static NSThread *_networkRequestThread = nil;
    static dispatch_once_t oncePredicate;
    dispatch_once(&oncePredicate, ^{
        _networkRequestThread = [[NSThread alloc] initWithTarget:self
selector:@selector(networkRequestThreadEntryPoint:) object:nil]; [_networkRequestThread start]; }); return _networkRequestThread; } + (void)networkRequestThreadEntryPoint:(id)__unused object { @autoreleasepool { [[NSThread currentThread] setName:@"AFNetworking"]; NSRunLoop *runLoop = [NSRunLoop currentRunLoop]; [runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode]; [runLoop run]; } }

以上是AFN2.x的一段經典程式碼

  • 首先我們要明確一個概念,執行緒一般都是一次執行完畢任務,就銷燬了。
  • 而線上程中添加了runloop,並執行起來,實際上是添加了一個do,while迴圈,這樣這個執行緒的程式就一直卡在do,while迴圈上,這樣相當於執行緒的任務一直沒有執行完,所有執行緒一直不會銷燬。
  • 所有,一旦我們添加了一個runloop,並run了,我們如果要銷燬這個執行緒,必須停止runloop,至於停止的方式,我們接著往下看。

這裡建立了一個執行緒,取名為AFNetworking,因為添加了一個runloop,所以這個執行緒不會被銷燬,直到runloop停止

[runloop addPort: [NSMachPort port]
forMode: NSDefaultRunLoopMode];
  • 這行程式碼的目的是新增一個埠監聽這個埠的事件,這也是我們後面會講到的一種執行緒見的通訊方式-基於埠的通訊。
[runloop run];
  • runloop開始跑起來,但是要注意,這種runloop,只有一種方式能停止。
[NSRunloop currentRunloop] removePort: <#(nonnull NSPort)#> forMode: <#(nonull NSRunLoopMode)#>
  • 只有從runloop中移除我們之前新增的埠,這樣的runloop沒有任何事件,所有直接退出。

再次回到AFN2.x的這行原始碼上,因為他用的是run,而且並沒有記錄下自己新增的NSMachPort,所有顯然,它沒有打算退出這個runloop,這是一個常駐執行緒。事實上,看過AFN2.x原始碼的同學都會知道,這個thread需要常駐的原因,在此就不做贅述了。

接下來我們看看AFN3.x是怎麼用runloop的

需要開啟的時候:

CFRunLoopRun();

終止的時候:

CFRunloopStop(CFRunLoopGetCurrent());
  • 由於NSUrlSession參考了AFN2.x的優點,自己維護了一個執行緒池,做Request執行緒的排程與管理,所有在AFN3.x中,沒有了常駐執行緒,都是用的run,結束的時候stop。

再看RAC中runloop

do {
    [NSRunloop.mainRunloop runMode:NSDefaultRunLoopMode beforeDate: [NSDate dateWithTimeIntervalSinceNow: 0.1]];
} while(!done);
  • 大致講下這段程式碼實現的內容,自己用一個Bool值done去控制runloop的執行,每次只執行這個模式的runloop,0.1秒。0.1秒後開啟runloop的下次執行。

以上我們都大致分析一下,後面我們再來講為什麼

首先我們講講runloop的概念

runloop官方圖

  • Runloop,顧名思義就是跑圈,他的本質就是一個do,while迴圈,當有事做時就做事,沒事做時就休眠。至於怎麼做事,怎麼休眠,這個是由系統核心來排程的,我們後面會講到。

  • 每個執行緒都由一個Run Loop,主執行緒的Run Loop會在App執行的時自動執行,子執行緒需要手動獲取執行,第一次獲取時,才會去建立。

  • 每個Run Loop都會以一個模式mode來執行,可以使用NSRunLoop的方法執行在某個特定的mode。

- (BOOL)runMode:(NSString *)mode beforeDate:(NSDate *)limitDate;
  • Run Loop的處理兩大類事件源:Timer Source和Input Source(包括performSelector *方法簇、Port或者自定義的Input Source),每個事件源都會繫結在Run Loop的某個特定模式mode上,而且只有RunLoop在這個模式下執行的時候,才會觸發Timer和Input Source。

  • 最後,如果沒有和人事件源新增到Run Loop上,Run Loop就會立刻exit,這也是一開始AFN例子,為什麼需要繫結一個Port的原因。

我們先來談談RunLoop Mode

OS下Run Loop的主要執行模式mode有:
1)NSDefaultRunLoopMode:預設的執行模式,除了NSConnection物件的事件。
2)NSRunLoopCommonModes:是一組常用的模式集合,將一個input source關聯到這個模式集合上,等於將input source關聯到這個模式集合中的所有模式上。在iOS系統中NSRunLoopCommonMode包含NSDefaultRunLoopMode、NSTaskDeathCheckMode、UITrackingRunLoopMode。

  • 假如我有個timer要關聯到這些模式上,一個個註冊很麻煩,我可以用:
CFRunLoopAddCommonMode([[NSRunLoop currentRunLoop] getCFRunLoop],(__bridge CFStringRef) UITrackingRunLoopMode);

將UITrackingRunLoopMode或者其他模式新增到這個NSRunLoopCommonModes模式中,然後只需要將Timer關聯到NSRunLoopCommonModes,即可以實現RunLoop執行在這個模式集合中任意一個模式時,這個Timer都可以觸發。

  • 當然,預設情況下NSRunLoopCommonModes包含了NSDefaultRunLoopMode和UITrackingRunLoopMode。我指的是如果有其他自定義的Mode。

  • 注意: 讓Run Loop執行在NSRunLoopCommonModes下是沒有意義的,因為一個時刻Run Loop只能執行在一個特定模式下,而不可能是個模式集合。

3)UITrackingRunLoopMode:用於跟蹤觸控事件觸發的模式(例如UIScrollView上下滾動), 主執行緒當觸控事件觸發會設定為這個模式,可以用來在控制元件事件觸發過程中設定Timer。
4) GSEventReceiveRunLoopMode:用於接受系統事件,屬於內部的Run Loop模式。
5)自定義Mode:可以設定自定義的執行模式Mode,你也可以用CFRunLoopAddCommonMode新增到NSRUnLoopCommonModes中。

總結一下

Run Loop 執行時只能以一種固定的模式執行,如果我們需要它切換模式,只有停掉它,再重新開其它
執行時它只會監控這個模式下新增的Timer Source和Input Source,如果這個模式下沒有相應的事件源,RunLoop的執行也會立刻返回的。注意Run Loop不能在執行在NSRunLoopCommonModes模式,因為NSRunLoopCommonModes其實是個模式集合,而不是一個具體的模式,我可以新增事件源的時候使用NSRunLoopCommonModes,只要Run Loop執行在NSRunLoopCommonModes中任何一個模式,這個事件源都可以被觸發。

Run Loop執行介面

  • 要操作Run Loop,Foundation層和Core Foundation層都有相應的介面可以操作Run Loop:Foundation層對應的是NSRunLoop,Core Foundation層對應的是CFRunLoopRef;
    Foundation層對應的是NSRunLoop,Core Foundation層對應的是CFRunLoopRef;

*兩組介面差不多,不過功能上還是有許多區別的:
例如CF層可以新增自定義的Input Source事件源、(CFRunLoopSourceRef)RunLoop觀察者Observer(CFRunLoopObserverRef),很多類似功能的介面特性也是不一樣的。
NSRunLoop的執行介面:

// 執行NSRunLoop,執行模式為預設的NSDefaultRunLoopMode模式,沒有超時限制
- (void)run;
// 執行NSRunLoop:引數為時間期限,執行模式為預設的NSDefaultRunLoopMode模式
- (void)runUntilDate:(NSDate *)limitDate;
// 執行NSRunLoop:引數為執行模式、時間期限,返回值為YES表示處理事件後返回的,NO表示是超時或者停止執行導致返回的。
- (BOOL)runMode:(NSString *)mode beforeDate:(NSDtate *)limitDate;

CFRunLoopRef的執行介面:

// 執行CFRunLoopRef
void CFRunLoopRun();
// 執行CFRunLoopRef:引數為執行模式、時間和是否在處理Input Source後退出標誌,返回值是exit原因
SInt32 CFRunLoopRunInMode(mode, second, returnAfterSourceHandled);
// 停止執行CFRunLoop
void CFRunLoopStop(CFRunLoopRef rl);
// 喚醒CFRunLoopRef
void CFRunLoopWakeUp(CFRunLoopRef rl);

首先,詳細講解下NSRunLoop的三個執行介面:

First

- (void)run; // 無條件執行
  • 不建議使用,因為這個介面會導致Run Loop永久性的在NSDefaultRunLoopMode模式。

  • 即使用CFRunLoopStop(runloopRef);也無法停止Run Loop的執行,除非能移除這個runloop上的所有事件源,包括定時器和source時間,不然這個子執行緒就無法停止,只能永久執行下去。

Second

- (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 ……,然後下次根據我們的Done值來判斷是否再去執行runloop。

Third

// 有一個超時時間限制,而且設定執行模式
- (BOOL)runMode:(NSString *)mode beforeDate:(NSDate *)limitDate;
  • 從方法上來看,比上面多了一個引數,可以設定執行模式。

  • 由一點需要注意:這種執行方式是可以被CFRunLoopStop(runloopRef)所停止的(大家可以自己寫個例子試試)。

  • 除此之外,這個方法和第二個方法還有一個很大的區別,就是這樣去執行runloop會多一種退出方式。這裡我指的退出方式是除了timer觸發以外的事件,都會導致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(@"執行緒結束");
    });
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW,(int64_t)(2*NSEC_PER_SEC)), dispatch_get_main_queue(), ^ {
        // 在我們開啟的非同步執行緒呼叫方法
        [self performSelector:@selector(recieveMsg) onThread: self.thread withObject: nil waitUntilDone: NO];
    });
}

- (void)recieveMsg {
    NSLog(@"收到訊息了,在這個執行緒:%@", [NSThread currentThread]);
}

輸出結果如下:

2016-11-22 14:04:15.250 TestRunloop3[70591:1742754] 執行緒開始
2016-11-22 14:04:17.250 TestRunloop3[70591:1742754] 收到訊息了,在這個執行緒:<NSThread: 0x600000263c80>{number = 3, name = (null)}
2016-11-22 14:04:17.250 TestRunloop3[70591:1742754] 執行緒結束
  • 在這裡我們用了performSelector: onThread…這個方法去進行執行緒間的通訊,這只是其中最簡單的方式。但是缺點也很明顯,就是在去呼叫這個執行緒的時候,如果執行緒已經不存在了,程式就會crash。後面我們會仔細講各種執行緒間的通訊。

  • 我們看到,我們收到一個訊息,這個訊息是一個 非timer得事件,所有runloop處理完就退出,這裡為什麼會這樣呢,我們可以看看runloop的原始碼:

/// RunLoop的實現
int CFRunLoopRunSpecific(runloop, modeName, seconds, stopAfterHandle) {

    /// 首先根據modeName找到對應mode
    CFRunLoopModeRef currentMode = __CFRunLoopFindMode(runloop, modeName, false);
    /// 如果mode裡沒有source/timer/observer, 直接返回。
    if (__CFRunLoopModeIsEmpty(currentMode)) return;

    /// 1. 通知 Observers: RunLoop 即將進入 loop。
    __CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopEntry);

    /// 內部函式,進入loop
    __CFRunLoopRun(runloop, currentMode, seconds, returnAfterSourceHandled) {

        Boolean sourceHandledThisLoop = NO;
        int retVal = 0;
        do {

            /// 2. 通知 Observers: RunLoop 即將觸發 Timer 回撥。
            __CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeTimers);
            /// 3. 通知 Observers: RunLoop 即將觸發 Source0 (非port) 回撥。
            __CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeSources);
            /// 執行被加入的block
            __CFRunLoopDoBlocks(runloop, currentMode);

            /// 4. RunLoop 觸發 Source0 (非port) 回撥。
            sourceHandledThisLoop = __CFRunLoopDoSources0(runloop, currentMode, stopAfterHandle);
            /// 執行被加入的block
            __CFRunLoopDoBlocks(runloop, currentMode);

            /// 5. 如果有 Source1 (基於port) 處於 ready 狀態,直接處理這個 Source1 然後跳轉去處理訊息。
            if (__Source0DidDispatchPortLastTime) {
                Boolean hasMsg = __CFRunLoopServiceMachPort(dispatchPort, &msg)
                if (hasMsg) goto handle_msg;
            }

            /// 6.通知 Observers: RunLoop 的執行緒即將進入休眠(sleep)。
            if (!sourceHandledThisLoop) {
                __CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeWaiting);
            }

            /// 7. 呼叫 mach_msg 等待接受 mach_port 的訊息。執行緒將進入休眠, 直到被下面某一個事件喚醒。
            /// ? 一個基於 port 的Source 的事件。
            /// ? 一個 Timer 到時間了
            /// ? RunLoop 自身的超時時間到了
            /// ? 被其他什麼呼叫者手動喚醒
            __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 到時間了,觸發這個Timer的回撥。
            if (msg_is_timer) {
                __CFRunLoopDoTimers(runloop, currentMode, mach_absolute_time())
            } 

            /// 10.2 如果有dispatch到main_queue的block,執行block。
            else if (msg_is_dispatch) {
                __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(msg);
            } 

            /// 10.3 如果一個 Source1 (基於port) 發出事件了,處理這個事件
            else {
                CFRunLoopSourceRef source1 = __CFRunLoopModeFindSourceForMachPort(runloop, currentMode, livePort);
                sourceHandledThisLoop = __CFRunLoopDoSource1(runloop, currentMode, source1, msg);
                if (sourceHandledThisLoop) {
                    mach_msg(reply, MACH_SEND_MSG, reply);
                }
            }

            /// 執行加入到Loop的block
            __CFRunLoopDoBlocks(runloop, currentMode);


            if (sourceHandledThisLoop && stopAfterHandle) {
                /// 進入loop時引數說處理完事件就返回。
                retVal = kCFRunLoopRunHandledSource;
            } else if (timeout) {
                /// 超出傳入引數標記的超時時間了
                retVal = kCFRunLoopRunTimedOut;
            } else if (__CFRunLoopIsStopped(runloop)) {
                /// 被外部呼叫者強制停止了
                retVal = kCFRunLoopRunStopped;
            } else if (__CFRunLoopModeIsEmpty(runloop, currentMode)) {
                /// source/timer/observer一個都沒有了
                retVal = kCFRunLoopRunFinished;
            }

            /// 如果沒超時,mode裡沒空,loop也沒被停止,那繼續loop。
        } while (retVal == 0);
    }

    /// 11. 通知 Observers: RunLoop 即將退出。
    __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopExit);
}

程式碼一長串,但是標了註釋,應該大致能看明白,大概講一下:

  • 函式的主體是一個do, while迴圈,用一個變數retVal,來控制迴圈的執行。預設為0,無限迴圈。

  • 剛進入迴圈1,2,3,4,5在做一件事,就是檢查是否有事件需要處理,如果有的話,直接跳到9去處理事件。

  • 處理完事件之後,到第10行,會去判斷4種是否應該跳出迴圈的情況,給出變數retVal賦一個不為0的值,來跳出迴圈。

  • 如果走到6,則判斷沒有事情做,那麼runloop就睡眠了,停在第7行,這一行類似sync這樣的同步機制(其實不是,只是舉個例子。。),把程式阻塞在這一行,直到有訊息返回值,才繼續往下進行。這一阻塞操作是系統核心掛起來的,阻塞了當前的執行緒,當有訊息返回時,因為當前執行緒是被阻塞的,系統核心會再開闢一條新的執行緒去返回這個訊息。然後程式繼續往下進行。

  • 走到第8、9,通知Observers,然後處理事件。

  • 到10,去判斷是否退出迴圈的條件,如果滿足條件退出迴圈,runloop結束。反之,又從新開始迴圈,從2開始。
    這就是一個完整的runloop處理事件的流程。

回到上述的例子這種模式下的runloop:

- (BOOL) runMode:(NSString *)mode beforeDate:(NSDate *)limitDate;

我們讓執行緒執行了一個事件,結果執行完,runloop就退出了,原因是這樣的:

if (sourceHandledThisLoop && stopAfterHandle) {
    /// 進入loop時引數處理完畢事件就返回
    retVal = kCFRunLoopHandledSource;
}
  • 這種形式開啟的runloop, stopAfterHandle這個引數為YES,而sourceHandledThisLoop這個引數在如下程式碼中被賦值為YES:
/// 10.3 如果一個Source1(基於port)發出了事件,處理這個事件
else {
    CFRunLoopSourceRef source1 = __CFRunLoopModeFindSourceForMachPort(runloop, currentMode, livePort);
    sourceHandledThisLoop = __CFRunLoopDoSource1(runloop, currentMode, source1, msg);
    if (sourceHandledThisLoop) {
        mach_msg(reply, MACH_SEND_MSG, reply);
    }
}

所以在這裡我們觸發了事件之後,runloop被退出了,這個時候我們也明白了為什麼timer並不會導致runloop的退出。

接下來我們分析一下Core Foundation中執行的runloop的藉口

/// 執行CFRunLoopRef
void CFRunLoopRun()
/// 執行CFRunLoopRef: 引數為執行模式、時間和是否在處理Input Source後退出標示,返回值是exit原因
SInt32 CFRunLoopRunInMode (mode, seconds, returnAfterSourceHandled);
/// 停止執行 CFRunLoopRef
void CFRunLoopStop(CFRunLoopRef rl);
/// 喚醒 CFRunLoopRef
void CFRunLoopWakeUp(CFRunLoopRef rl);

First

void CFRunLoopRun();
  • 執行在預設的kCFRunLoopDefaultMode模式下,知道CFRunLoopStop介面呼叫停止這個RunLoop,或者RunLoop的所有事件源被刪除。

  • NSRunLoop是基於CFRunLoop來封裝的,NSRunLoop是執行緒不安全的,而CFRunLoop是執行緒安全的。

  • 在這裡我們可以看到和上面NSRunLoop有一個直觀的區別是:CFRunLoop能直接停止掉所有的CFRunLoop執行起來的runloop,其實之前講到的:

- (BOOL)runMode:(NSString *)mode beforeDate:(NSDate *)limitDate;

這種方式執行起來的runloop也能用CFRunLoopStop 停止掉的,原因是它完全是基於下面這種方式封裝的:

SInt32 CFRunLoopRunInMode(mode, seconds, returnAfterSourceHandled);

可以看到引數幾乎一模一樣,前者預設returnAfterSourceHandled引數為YES,當觸發一個非timer事件後,runloop就終止了。

  • 這裡比較簡單,就不舉例贅述了。

Second

SInt32 CFRunLoopRunInMode(mode, seconds, returnAfterSourceHandled);
  • 這裡有3個引數,1個返回值

  • 其中第一個引數是指RunLoop執行的模式(例如kCFRunLoopDefaultMode或者kCFRunLoopCommonModes),第二個引數是執行事件,第三個引數是是否在處理事件後讓Run Loop退出返回,NSRunLoop的第三種開啟runloop的方法,綜上所述,我們知道,實際上就是設定stopAfterHande這個引數為YES

  • 關於返回值,我們知道呼叫runloop執行,程式碼是停在這一行不返回的,當返回的時候runloop就結束了,所有這個返回值就是runloop結束原因的返回,為一個列舉值,具體原因如下:

enum {
    kCFRunLoopRunFinished = 1, // Run Loop結束,沒有Timer或者其他Input Source
    kCFRunLoopRunStopped = 2, // Run Loop被停止,使用CFRunLoopStop停止Run Loop
    kCFRunLoopRunTimedOut = 3, // Run Loop超時
    kCFRunLoopRunHandledSource = 4, // Run Loop處理完事件,注意Timer事件的觸發是不會讓Run Loop退出返回的,即使CFRunLoopRunInMode的第三個引數是YES也不行
}

看到這,我們發現我們忽略了NSRunLoop第三種開啟方式的返回值。
- (Bool)runMode:(NSString *)mode beforeDate:(NSDate *)limitDate;
它其實就是基於CFRunLoopRunInMode封裝的,它的返回值為一個Bool值,如果是PerformSelector *事件或者其他Input Source事件觸發處理後,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];

        //最後一個引數,是否處理完事件返回,結束runLoop
        SInt32 result = CFRunLoopRunInMode(kCFRunLoopDefaultMode, 100, YES);
        /*
         kCFRunLoopRunFinished = 1, //Run Loop結束,沒有Timer或者其他Input Source
         kCFRunLoopRunStopped = 2, //Run Loop被停止,使用CFRunLoopStop停止Run Loop
         kCFRunLoopRunTimedOut = 3, //Run Loop超時
         kCFRunLoopRunHandledSource = 4 ////Run Loop處理完事件,注意Timer事件的觸發是不會讓Run Loop退出返回的,即使CFRunLoopRunInMode的第三個引數是YES也不行
         */
        switch (result) {
            case kCFRunLoopRunFinished:
                NSLog(@"kCFRunLoopRunFinished");

                break;
            case kCFRunLoopRunStopped:
                NSLog(@"kCFRunLoopRunStopped");

            case kCFRunLoopRunTimedOut:
                NSLog(@"kCFRunLoopRunTimedOut");

            case kCFRunLoopRunHandledSource:
                NSLog(@"kCFRunLoopRunHandledSource");
            default:
                break;
        }

        NSLog(@"end thread.......");

    });

}

- (void)doTimerTask1:(NSTimer *)timer
{

    count++;
    if (count == 2) {
        [timer invalidate];
    }
    NSLog(@"do timer task count:%d",count);
}

輸出結果如下:

2016-11-23 09:19:28.342 TestRunloop3[88598:1971412] starting thread.......
2016-11-23 09:19:29.347 TestRunloop3[88598:1971412] do timer task count:1
2016-11-23 09:19:30.345 TestRunloop3[88598:1971412] do timer task count:2
2016-11-23 09:19:30.348 TestRunloop3[88598:1971412] kCFRunLoopRunFinished
2016-11-23 09:19:30.348 TestRunloop3[88598:1971412] end thread.......
  • 很清楚的可以看到,當timer被置無效的時候,runloop裡面沒有了任何事件源,所以退出了,退出原因為:kCFRunLoopRunFinished,執行緒也就結束了。

總結一下

  • runloop的執行方法一共有5種:包括NSRunLoop的3種,CFRunLoop的2種;
    而取消的方式一共為3種:
    1)移除掉runloop種的所有事件源(timer和source)。
    2)設定一個超時時間。
    3)只要CFRunLoop執行起來就可以用:void CFRunLoopStop(CFRunLoopRef rl); 去停止。
    除此之外用 NSRunLoop下面這個方法也能使用void CFRunLoopStop(CFRunLoopRef rl); 停止:
[NSRunLoop currentRunLoop] runMode:<#(nonull NSRunLoopMode)#> beforeDate:<#(nonull NSDate)#>
  • 實現過程中,可以根據需求,我們可以設定一個自己的Bool值,來控制runloop的開始於停止,類似下面這樣:
while(!cancel) {
    CFRunLoopRunInMode(kCFRunLoopDefaultMode, 1, YES);
}
  • 每次runloop只執行1秒就停止,然後開始下一次runloop。

  • 這裡最後一個引數設定為YES,當有非timer事件進來,也會立即開始下一次runloop。

  • 當然每次進來我們都可以去修改Mode的值,這樣我們呢可以讓runloop每次都執行在不同的模式下。

  • 當我們不需要runloop的時候,可以直接將cancel設定為YES即可。

當然,這裡只是提供一個思路,具體有需求,可以根據實際需要。

基於runloop的執行緒通訊

首先明確一個概念,執行緒間的通訊(不僅限於通訊,幾乎所有iOS事件都是如此),實際上是各種輸入源,觸發runloop去處理對應的事件,所以我們先來講講輸入源:
輸入源非同步的傳送訊息給你的執行緒。事件來源取決於輸入源的種類:

  • 基於埠的輸入源和自定義輸入源。基於埠的輸入源監聽程式相應的埠。自定義輸入源則監聽自定義的事件源。

當你建立輸入源,你需要將其分配給run loop中的一個或多個模式。模式只會在特定事件影響監聽的源。大多數情況下,runloop執行在預設模式下,但是你也可以使其執行在自定義模式。若其一源在當前模式下不被監聽,那麼任何其生成的訊息只在run loop執行在其關聯的模式下才會被傳遞。

1)基於埠的輸入源:
在runloop中,被定義名為source1。Cocoa和Core Foundation內建支援使用埠相關的物件和函式來建立的基於埠物件,並使用NSPort的方法吧埠新增到runloop。埠物件會自己處理建立和配置輸入源。

在Core Foundation,你必須人工建立埠和它的run loop源。在兩種情況下,你都可以使用埠相關的函式(CFMachPortRef, CFMessagePortRef, CFSockerRef)來建立合適的物件。

這裡用Cocoa裡的舉個例子,Cocoa裡用來執行緒傳值得NSMachPort,它的父類是NSPort。
首先我們看下面:

NSPort *port1 = [[NSPort alloc]init];
NSPort *port2 = [[NSMachPort alloc]init];
NSPort *port3 = [NSPort port];
NSPort *port4 = [NSMachPort port];

我們可以打斷點看到如下:
port圖。png
* 發現我們怎麼建立,都返回給我們的是NSMachPort的例項,這應該是NSProt內部做了一個訊息的轉發,這就有點像是一個抽象類,它本身只是定義一些公有屬性和方法,然後利用整合它的子類去實現(只是我個人猜測。。)

繼續看我們寫的一個利用NSMachPort來執行緒通訊的例項:

- (void)testDemo3
{
    //宣告兩個埠   隨便怎麼寫建立方法,返回的總是一個NSMachPort例項
    NSMachPort *mainPort = [[NSMachPort alloc]init];
    NSPort *threadPort = [NSMachPort port];
    //設定執行緒的埠的代理回撥為自己
    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傳送一條訊息,第一個引數:傳送時間。msgid 訊息標識。
        //components,傳送訊息附帶引數。reserved:為頭部預留的位元組數(從官方文件上看到的,猜測可能是類似請求頭的東西...)
        [threadPort sendBeforeDate:[NSDate date] msgid:1000 components:array from:mainPort reserved:0];

    });

}

//這個NSMachPort收到訊息的回撥,注意這個引數,可以先給一個id。如果用文件裡的NSPortMessage會發現無法取值
- (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
  • 我們跨越執行緒,確實從主執行緒往另外一個執行緒傳送了訊息。

  • 這裡我們要注意幾個點:
    1)- (void)handlePortMessage:(id)message 這裡這個代理的引數,從.h中去複製過來的為NSPortMessage型別的一個物件,但是我們發現蘋果只是在.h中@class過來,我們無法呼叫它的任何方法。所有我們用id宣告,然後通過kvc去取它的屬性。
    2)關於下面這個傳值型別的問題:
    NSMutableArray *array = [NSMutableArray arrayWithArray: @[mainPort, data]];
    在此我困惑了好一會兒。。之前我是往數組裡新增的是String或者其他型別的物件,但是發現引數傳過去之後,變成了nil。於是查了半天資料,依然沒有結果。於是翻看官方文件,終於在方法描述裡看到了(其實很醒目,然後作者英文水平有限。。)

    The components array consists of a series of instances
    of some subclass of NSData, and instances of some
    subclass of NSPort; since one subclass of NSPort does
    not necessarily know how to transport an instance of
    another subclass of NSPort (or could do it even if it
    knew about the other subclass), all of the instances
    of NSPort in the components array and the ‘receivePort’
    argument MUST be of the same subclass of NSPort that
    receives this message. If multiple DO transports are
    being used in the same program, this requires some care.

從這段描述中我們可以看出,這個傳引數組裡面只能裝兩種型別的資料,一種是NSPort的子類,一種是NSData的子類。所有我們如果要用這種方式傳值必須得先把資料轉成NSData型別的才行。

2)Cocoa 執行Selector的源

  • 除了基於埠的源,Cocoa定義了自定義輸入源,允許你在任何執行緒執行selector。它被稱為source0,和基於埠的源一樣,執行selector請求會在目標執行緒上序列化,減緩許多線上程上允許多個方法容易引起的同步問題。不像基於源的埠,一個selector執行完後會自動從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這個引數,顧名思義,就是是否等到結束。
    1)如果這個值設為YES,那麼就需要等到這個方法執行完,執行緒才能繼續往下去執行。它會阻塞提交的執行緒。
    2)如果為NO的話,這個呼叫的方法會非同步的實行,不會阻塞提交的執行緒。
    3)自定義輸入源:

  • 為了自定義輸入源,必須使用Core Foundation裡面的CGRunLoopSourceRef型別相關的函式來建立。你可以使用回撥函式來配置自定義輸入源。CoreFoundation會在配置源的不同地方呼叫回撥函式,處理輸入時間,在源從runloop移除的時候清理它。除了定義在事件到達時自定義輸入源的行為,你也必須定義訊息傳遞機制。源的這部分執行在單獨的執行緒裡面,並負責在資料等待處理的時候將資料傳遞給源並通知它處理資料。訊息傳遞機制的定義取決於你,但是最好不要過於複雜。
    建立自定義的輸入源包括定義下面內容:
    1.輸入源要處理的資訊;
    2.使感興趣的客戶端知道如何和輸入源互動的排程歷程;
    3.處理其他任何客戶端傳送請求的歷程;
    4.使輸入源失效的取消歷程。

  • 由於建立輸入源來處理自定義訊息,實際配置選是靈活的。排程歷程,處理歷程和取消歷程都是建立自定義輸入源的關鍵歷程。二輸入源其他的大部分行為都發生在這些歷程的外部。比如,由於你決定資料傳輸到輸入源的機制,還有輸入源的其他執行緒的通訊機制也是由你決定。

下圖中,程式的主執行緒維護了一個輸入源的引用,輸入源所需的自定義命令緩衝區和輸入源所在的runloop。當主執行緒有任務需要分發給工作執行緒時候,主執行緒會給命令緩衝區傳送命令和必須的資訊來通知工作執行緒開始執行任務.(因為主執行緒和輸入源所在工作執行緒都是可以訪問命令緩衝區的,因此這些方法必須是同步的),一旦命令傳送出去,主執行緒會通知輸入源並喚醒工作執行緒的runloop。而一收到喚醒命令,runloop會呼叫輸入源的處理程式,由它來執行命令緩衝區的響應命令。
自定義輸入源
這樣一來,我們來寫一個例項來講講自定義的輸入源(自定義的輸入源,只有用CF來實現):

CFRunLoopRef _runLoopRef;
CFRunLoopSourceRef _source;
CFRunLoopSourceContext _source_context;

首先我們宣告3個成員變數,這是我們自定義輸入源所需要的3個引數。具體我們舉例之後再說:

- (void)testDemo4
{
    dispatch_async(dispatch_get_global_queue(0, 0), ^{

        NSLog(@"starting thread.......");

        _runLoopRef = CFRunLoopGetCurrent();
        //初始化_source_context。
        bzero(&_source_context, sizeof(_source_context));
        //這裡建立了一個基於事件的源,綁定了一個函式
        _source_context.perform = fire;
        //引數
        _source_context.info = "hello";
        //建立一個source
        _source = CFRunLoopSourceCreate(NULL, 0, &_source_context);
        //將source新增到當前RunLoop中去
        CFRunLoopAddSource(_runLoopRef, _source, kCFRunLoopDefaultMode);

        //開啟runloop 第三個引數設定為YES,執行完一次事件後返回
        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);
            //喚醒執行緒,執行緒喚醒後發現由事件需要處理,於是立即處理事件
            CFRunLoopWakeUp(_runLoopRef);
        }else {
            NSLog(@"RunLoop 正在處理事件");
            //新增輸入事件,當前正在處理一個事件,當前事件處理完成後,立即處理當前新輸入的事件
            CFRunLoopSourceSignal(_source);
        }
    });

}

//此輸入源需要處理的後臺事件
static void fire(void* info){

    NSLog(@"我現在正在處理後臺任務");

    printf("%s",info);
}

輸出結果如下:

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] 我現在正在處理後臺任務
hello
2016-11-24 10:42:31.663 TestRunloop3[4683:238183] end thread.......
  • 例中可見我們建立一個自定義的輸入源,綁定了一個函式,一個引數,並且這個輸入源,實現了執行緒間的通訊.

  • 大概說一下:

    a)CFRunLoopRef _runLoopRef;CF的runLoop。
    b)CFRunLoopSourceContext _source_context; 注意到例中用了一個C函式bzero(&_source_context, sizeof(_source_context)); 來初始化。其實它的本質是一個結構體:

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)); 所以這個函式其實就是把所有的內容先置為0.

    • 我們在這裡綁定了兩個引數一個是signal觸發的函式,一個是函式的引數,至於其他引數的用途,可以看看蘋果官方文件的說明:

    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; 這個是自定義輸入源中最重要的一個引數。它用來連線runloop和CFRunLoopSourceContext的一些配置選項,注意我們自定義的輸入源,必須由我們手動觸發。需要先CFRunLoopSourceSignal(_source); 在看當前runloop是否在休眠中,來看是否需要呼叫CFRunLoopWakeUp(_runLoopRef); (一般都是要呼叫的)。

4)定時源:

  • 定時源在預設的時間點同步方式傳遞訊息。定時器是執行緒通知自己做某事的一種方法。

  • 儘管定時器可以產生基於時間的通知,但他並不實時機制。和輸入源一樣,定時器也和runloop的特點模式相關。如果定時器所在的模式當前未被runloop監視,那麼定時器將不會開始知道runloop執行在響應的模式下。類似的,如果定時器在runloop處理某一事件期間開始,定時器會一直等待直到下次runloop開始響應的處理程式,如果runloop不運行了,那麼定時器也永遠不啟動。

  • 配置定時源:
    Cocoa中可以使用以下NSTimer類方法來建立一個定時器:

[NSTimer scheduledTimerWithTimeInterval:<#(NSTimeInterval)#> target:<#(nonnull id)#> selector:<#(nonnull SEL)#> userInfo:<#(nullable id)#> repeats:<#(BOOL)#>
[NSTimer timerWithTimeInterval:
            
           

相關推薦

深入理解Runloop

前言 RunLoop 是 iOS 和 OSX 開發中非常基礎的一個概念,為了讓大家更加快速融入,請先一段程式碼: + (NSThread *)networkRequestThread { static NSThread *_networkRequ

效能測試:深入理解執行緒數併發量TPS

併發數,執行緒數,吞吐量,每秒事務數(TPS)都是效能測試領域非常關鍵的資料和指標。 那麼他們之間究竟是怎樣的一個對應關係和內在聯絡? 測試時,我們經常容易將執行緒數等同於表述為併發數,這一表述正確嗎? 本文就將對效能領域的這些關鍵概念做一次探討。 文章可能會比較長,希望您保持耐心看完。   1.

理解Sharding jdbc原理

相比於Spring基於AbstractRoutingDataSource實現的分庫分表功能,Sharding jdbc在單庫單表擴充套件到多庫多表時,相容性方面表現的更好一點。例如,spring實現的分庫分表sql寫法如下: select id, name, price,

產品設計教程:如何理解 px,dp,dpi, pt

先聊聊熟悉的幾個單位 圍繞著各種螢幕做設計和開發的人會碰到下面幾個單位:in, pt, px, dpi,dip/dp, sp 下面先簡單回顧下前四個單位: "in" inches的縮寫,英寸。就是螢幕的物理長度單位。一英寸等於2.54cm。比如Android手機

Azure IOT 設備固件更新技巧

trigger 物聯網平臺 搭建 href ice 有效 面板 調用 創建 嫌長不看版 今天為大家準備的硬菜是:在 Azure IoT 中心創建 Node.js 控制臺應用,進行端到端模擬固件更新,為基於 Intel Edison 的設備安裝新版固件的流程。通過創建模擬設備

想做好PPT折線圖

12月 image 菊花 -c 強調 spa any border 線圖 配圖主題無關今天鄭少跟大家聊聊折線圖的使用方法,或者你有疑問,折線圖很簡單,插入修改數據不就好了嗎?如果你要是這樣想的,恭喜你,有可能你會做出下面這樣的效果。如果你要是稍微懂一點折線圖的使用方法,你就

Linux 問題故障定位

1. 背景 有時候會遇到一些疑難雜症,並且監控外掛並不能一眼立馬發現問題的根源。這時候就需要登入伺服器進一步深入分析問題的根源。那麼分析問題需要有一定的技術經驗積累,並且有些問題涉及到的領域非常廣,才能定位到問題。所以,分析問題和踩坑是非常鍛鍊一個人的成長和提升自我能力。如果我們有一套好的分析工具,那將是事

C語言從入門到精通

影響 內容 當前 位置 replace 雙精度 下標 寄存器變量 一個 No.1 計算機與程序設計語言的關系 計算機系統由硬件系統和軟件系統構成,硬件相當於人類的肉體,而軟件相當於人類的靈魂,如果脫離了靈魂,人類就是一具行屍走肉 No.2 C語言的特點 代碼簡潔,靈活性高

【MYSQL學習筆記02】MySQL的高階應用之Explain(完美詳細版

版權宣告:本文為博主原創文章,未經博主允許不得轉載。 https://blog.csdn.net/wx1528159409 最近學習MySQL的高階應用Explain,寫一篇學習心得與總結,目錄腦圖如下: 一、Explain基本概念 1. Explain定義 · 我們知道M

抖音內容運營全解剖 !

抖音的火爆已經不用多說,作為短視訊的頭部APP,抖音已經從微信手中奪走不少使用者時間,成為新的“時間黑洞”。 比如:“中毒了,我每天晚上要刷2個小時”,“下一站,逃離微信,上抖音”… 一個企業運營抖音的目的是什麼? 答案顯而易見,無非就是做品牌營銷、擴大品牌影響力。 在短視訊領域積累

百萬併發下的Nginx優化

本文作者主要分享在 Nginx 效能方面的實踐經驗,希望能給大家帶來一些系統化思考,幫助大家更有效地去做 Nginx。 優化方法論 我重點分享如下兩個問題: 保持併發連線數,怎麼樣做到記憶體有效使用。 在高併發的同時保持高吞吐量的重要要點。 實現層面主要是三方面優化,主要聚焦

Java子類繼承父類類的載入順序

1. 程式碼 package parent; public class Child extends Father { static { System.out.println("

中後臺產品的表格設計(原型規範下載)

中後臺產品的表格設計,看這一篇就夠了(原型規範下載) 2018年4月16日luodonggan 中後臺產品的表格設計,看這一篇就夠了(原型規範下載) 經過了將近一年的後臺產品經歷,踩了很多坑,試了很多錯,也學習到了很多東西,目前也形成了自己的一套規範。本文將其中的部分收穫彙總成文,

Linux 常用指令 —— 摘自《Linux Probe》

touch:用於建立空白檔案或設定檔案的時間,ps:黑客可以用touch指令來修改檔案的最後修改時間,以隱藏自己的修改行為。 mkdir:用於建立空白的目錄,如mkdir path,可以結合引數-p來遞迴建立檔案目錄,如mkdir -p a/b/c/d/e cp:用於複製檔案或目錄,如cp 1.txt p

樹狀陣列(Binary Indexed Tree)

定義 根據維基百科的定義: A Fenwick tree or binary indexed tree is a data structure that can efficiently update elements and calculate pr

Cookie介紹及在Android中的使用總結超詳細

Cookie介紹 cookie的起源 早期Web開發面臨的最大問題之一是如何管理狀態。簡言之,伺服器端沒有辦法知道兩個請求是否來自於同一個瀏覽器。那時的辦法是在請求的頁面中插入一個token,並且在下一次請求中將這個token返回(至伺服器)。這就需要在form中插入一個包含toke

BlockingQueue深入解析-BlockingQueue

BlockingQueue深入解析-BlockingQueue看這一篇就夠了 轉載:https://www.cnblogs.com/WangHaiMing/p/8798709.html 本篇將詳細介紹BlockingQueue,以下是涉及的主要內容: BlockingQueue

關於Kaggle入門

這次醞釀了很久想給大家講一些關於Kaggle那點兒事,幫助對資料科學(Data Science)有興趣的同學們更好的瞭解這個專案,最好能親身參與進來,體會一下學校所學的東西和想要解決一個實際的問題所需要的能力的差距。雖然不是Data Science出身,但本著嚴謹的科研態

並查集(Union-Find Algorithm)

動態連線(Dynamic connectivity)的問題 所謂的動態連線問題是指在一組可能相互連線也可能相互沒有連線的物件中,判斷給定的兩個物件是否聯通的一類問題。這類問題可以有如下抽象: 有一組構成不相交集合的物件 union: 聯通兩個物件

Android 必須知道2018年流行的框架庫及開發語言

導語2017 已經悄悄的走了,2018 也已經匆匆的來了,我們在總結過去的同時,也要展望一下未來,來規劃一下今年要學哪些新技術。這幾年優秀Android的開源庫不斷推出,新技術層出不窮,需要我們不斷去了解和掌握,在提高自身開發水平的同時,我們需要付出更多學習精力和時間。俗話說