iOS 中 RunLoop 工作原理以及 NSRunloop 的使用

最近看了一下
深入研究 Runloop 與線程绷詹剩活
想到幾個(gè)問題又研究了一下。

為什么使用 RunLoop 會(huì)造成內(nèi)存泄漏

這個(gè)問題還需要看怎樣定義內(nèi)存泄漏。如果是像偎血,循環(huán)引用這種霸株,出乎程序員意料的方式造成內(nèi)存得不到釋放的情況才叫做內(nèi)存泄漏雕沉,那么 RunLoop 不應(yīng)該被稱為內(nèi)存泄漏。
RunLoop 占用內(nèi)存不釋放去件,還是跟單例對(duì)象占用內(nèi)存得不到釋放歸到一類比較好坡椒。因?yàn)椋琑unLoop 就是這樣設(shè)計(jì)的——一旦開啟 RunLoop 要跟隨程序結(jié)束而結(jié)束尤溜,這點(diǎn)是程序員應(yīng)該明確的倔叼。

RunLoop 為什么需要一直占用內(nèi)存

Operating a custom input source

使用文檔上的著名圖片來解釋,NSRunLoop 需要不斷接受另一個(gè)線程發(fā)送過來的消息宫莱,當(dāng)然不能退出嘍丈攒。既然不能退出,NSRunLoop 就會(huì)一直持有相關(guān)聯(lián)的對(duì)象授霸,導(dǎo)致內(nèi)存不能釋放巡验。另外 NSThread 啟動(dòng)子線程也是需要占用內(nèi)存的,而且不小碘耳,我記得是主線程1m显设,子線程0.5m。
接受消息的具體工作過程辛辨,網(wǎng)絡(luò)上有很多捕捂,就不再 Copy 過來了,想了解的可以看看這篇愉阎。
淺談NSRunloop工作原理和相關(guān)應(yīng)用

RunLoop 的啟動(dòng)

文檔上說了 RunLoop 不能手動(dòng)初始化绞蹦,它跟 NSThread 是一起的,屬于 NSThread 的 associateObject榜旦,如果要獲取一個(gè) NSThread 的 RunLoop幽七,必須調(diào)用 [NSRunLoop currentRunLoop]。
NSRunLoop 有三種啟動(dòng)方式:

  1. Unconditionally
  2. With a set time limit
  3. In a particular mode
    對(duì)應(yīng)的方法分別為:
  4. run
  5. runUntilDate
  6. runMode:beforeDate:

參照文檔以及 RunLoop 的源碼源碼在此
runMode:beforeDate:
這個(gè)方法才是啟動(dòng)一次 RunLoop! 為什么這樣說溅呢,稍后再解釋澡屡。
runUntilDate:
這個(gè)方法,會(huì)循環(huán)調(diào)用 runMode:beforeDate: 直到達(dá)到參數(shù) NSDate 所指定的時(shí)間咐旧,也就是超時(shí)時(shí)間驶鹉。
run
這個(gè)方法,可以看做是
[runloop runUntilDate:[NSDate distantFuture]];

RunLoop 啟動(dòng)后如何接收消息

RunLoop 啟動(dòng)后铣墨,必須注冊(cè) source(翻譯成消息源好不好)室埋。source 有兩種,一種是 NSPort 及其子類,另一種是 NSTimer姚淆。
使用 NSRunLoop 的消息機(jī)制進(jìn)行線程通信孕蝉,就依靠 NSPort 及其子類。關(guān)鍵方法在這里:

- (void)setup{
    NSLog(@"%@",[[NSRunLoop currentRunLoop] currentMode]);
    //創(chuàng)建并啟動(dòng)一個(gè) thread
    self.thread = [[NSThread alloc]initWithTarget:self selector:@selector(threadTest) object:nil];
    [self.thread setName:@"Test Thread"];
    [self.thread start];

    //向 RunLoop 發(fā)送消息的簡便方法腌逢,系統(tǒng)會(huì)將消息傳遞到指定的 SEL 里面
    [self performSelector:@selector(receiveMsg) onThread:self.thread withObject:nil waitUntilDone:NO];
        
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        //直接去的子線程 RunLoop 的一個(gè) port降淮,并向其發(fā)送消息,這個(gè)是比較底層的 NSPort 方式進(jìn)行線程通信
        [self.port sendBeforeDate:[NSDate date] components:[@[[@"hello" dataUsingEncoding:NSUTF8StringEncoding]] mutableCopy] from:nil reserved:0];
    });
}
- (void)threadTest{
    NSLog(@"%@",@"child thread start");
    //threadTest 這個(gè)方法是在 Test Thread 這個(gè)線程里面運(yùn)行的搏讶。
    NSLog(@"%@",[NSThread currentThread]);
    //獲取這個(gè)線程的 RunLoop 并讓他運(yùn)行起來
    NSRunLoop* runloop = [NSRunLoop currentRunLoop];
    self.port = [[NSMachPort alloc]init];
    self.port.delegate = self;
    [runloop addPort:self.port forMode:NSRunLoopCommonModes];
    //約等于runUntilDate:[NSDate distantFuture]
    [runloop run];
}
- (void)receiveMsg{
    NSLog(@"%@",[NSThread currentThread]);
    NSLog(@"%@",@"receive msg");
}

#pragma NSMachPortDelegate
- (void)handleMachMessage:(void *)msg{
    NSLog(@"handle message thread:%@",[NSThread currentThread]);
}
  1. 使用 run 方法啟動(dòng) NSRunLoop佳鳖,使用 [self performSelector:@selector(receiveMsg) onThread:self.thread withObject:nil waitUntilDone:NO]; 發(fā)送消息,- (void)receiveMsg接受消息打印出來的內(nèi)容:
2017-04-25 17:03:19.322693 test[2553:585069] <NSThread: 0x174c76480>{number = 5, name = Test Thread}
2017-04-25 17:03:19.322921 test[2553:585069] receive msg
  1. 使用 run 方法啟動(dòng) NSRunLoop 使用[self.port sendBeforeDate:[NSDate date] components:[@[[@"hello" dataUsingEncoding:NSUTF8StringEncoding]] mutableCopy] from:nil reserved:0];發(fā)送消息媒惕,- (void)handleMachMessage:(void *)msg 接受消息打印出來的內(nèi)容:
2017-04-25 17:03:20.408981 test[2553:585069] handle message thread:<NSThread: 0x174c76480>{number = 5, name = Test Thread}

上面是分別使用高層方法和底層方法進(jìn)行消息通信系吩。

RunLoop 是如何隨時(shí)隨地接收消息的。

上文說過使用 run 方法啟動(dòng) RunLoop 后吓笙,會(huì)循環(huán)調(diào)用
runMode:beforeDate:
這個(gè)方法淑玫,我想,可以把這個(gè)方法稱為 1次消息循環(huán)面睛。
下面這段代碼是 CFRunloop 源碼中的一個(gè)函數(shù)絮蒿,我猜是1次消息循環(huán)的具體實(shí)現(xiàn)。大概的工作方式是這樣的:

  1. 判斷這次 RunLoop 的狀態(tài)(應(yīng)該對(duì)應(yīng)文檔中 NSRunLoop 的 Model)如果不是退出的狀態(tài)就繼續(xù)向下執(zhí)行叁鉴。
  2. 看看有沒有超時(shí)時(shí)間(這個(gè)時(shí)間應(yīng)該是runMode:beforeDate:這里的 date)添加一個(gè) timer土涝。
  3. 發(fā)送進(jìn)入 RunLoop 的消息。(每一次 RunLoop 開始前幌墓,都會(huì)發(fā)送通知)
  4. 如果 NSPort 中沒有消息但壮,線程進(jìn)入阻塞狀態(tài),交出 CPU常侣。
  5. 如果 NSPort 中有消息蜡饵,就會(huì)喚醒這個(gè) RunLoop 所在的線程,RunLoop 繼續(xù)運(yùn)行并處理這個(gè)消息胳施。
  6. 如果 NSPort 中還有消息溯祸,就回到3這一步(因?yàn)橛邢⑿枰幚恚€程不會(huì)進(jìn)入阻塞狀態(tài)舞肆,而是一直處理消息)
  7. NSPort 中消息處理完畢(或者 timer 超時(shí)時(shí)間到或者手動(dòng)退出這個(gè)消息循環(huán)焦辅,這兩種情況也會(huì)喚醒這個(gè)線程,并執(zhí)行這一步)RunLoop 退出椿胯。
    這樣筷登,一次消息循環(huán)結(jié)束。
static int32_t __CFRunLoopRun(CFRunLoopRef rl, CFRunLoopModeRef rlm, CFTimeInterval seconds, Boolean stopAfterHandle, CFRunLoopModeRef previousMode) {
    int64_t startTSR = (int64_t)mach_absolute_time();

    if (__CFRunLoopIsStopped(rl)) {
        __CFRunLoopUnsetStopped(rl);
    return kCFRunLoopRunStopped;
    } else if (rlm->_stopped) {
    rlm->_stopped = false;
    return kCFRunLoopRunStopped;
    }

    mach_port_name_t dispatchPort = MACH_PORT_NULL;
    Boolean libdispatchQSafe = pthread_main_np() && ((HANDLE_DISPATCH_ON_BASE_INVOCATION_ONLY && NULL == previousMode) || (!HANDLE_DISPATCH_ON_BASE_INVOCATION_ONLY && 0 == _CFGetTSD(__CFTSDKeyIsInGCDMainQ)));
    if (libdispatchQSafe && (CFRunLoopGetMain() == rl) && CFSetContainsValue(rl->_commonModes, rlm->_name)) dispatchPort = _dispatch_get_main_queue_port_4CF();

    dispatch_source_t timeout_timer = NULL;
    struct __timeout_context *timeout_context = (struct __timeout_context *)malloc(sizeof(*timeout_context));
    if (seconds <= 0.0) { // instant timeout
        seconds = 0.0;
        timeout_context->termTSR = 0LL;
    } else if (seconds <= TIMER_INTERVAL_LIMIT) {
    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, DISPATCH_QUEUE_OVERCOMMIT);
    timeout_timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
        dispatch_retain(timeout_timer);
    timeout_context->ds = timeout_timer;
    timeout_context->rl = (CFRunLoopRef)CFRetain(rl);
    timeout_context->termTSR = startTSR + __CFTimeIntervalToTSR(seconds);
    dispatch_set_context(timeout_timer, timeout_context); // source gets ownership of context
    dispatch_source_set_event_handler_f(timeout_timer, __CFRunLoopTimeout);
        dispatch_source_set_cancel_handler_f(timeout_timer, __CFRunLoopTimeoutCancel);
        uint64_t nanos = (uint64_t)(seconds * 1000 * 1000 + 1) * 1000;
    dispatch_source_set_timer(timeout_timer, dispatch_time(DISPATCH_TIME_NOW, nanos), DISPATCH_TIME_FOREVER, 0);
    dispatch_resume(timeout_timer);
    } else { // infinite timeout
        seconds = 9999999999.0;
        timeout_context->termTSR = INT64_MAX;
    }

    Boolean didDispatchPortLastTime = true;
    int32_t retVal = 0;
    do {
        uint8_t msg_buffer[3 * 1024];
#if DEPLOYMENT_TARGET_MACOSX || DEPLOYMENT_TARGET_EMBEDDED
        mach_msg_header_t *msg = NULL;
#elif DEPLOYMENT_TARGET_WINDOWS
        HANDLE livePort = NULL;
        Boolean windowsMessageReceived = false;
#endif
    __CFPortSet waitSet = rlm->_portSet;

    rl->_ignoreWakeUps = false;

        if (rlm->_observerMask & kCFRunLoopBeforeTimers) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeTimers);
        if (rlm->_observerMask & kCFRunLoopBeforeSources) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeSources);

    __CFRunLoopDoBlocks(rl, rlm);

        Boolean sourceHandledThisLoop = __CFRunLoopDoSources0(rl, rlm, stopAfterHandle);
        if (sourceHandledThisLoop) {
            __CFRunLoopDoBlocks(rl, rlm);
    }

        Boolean poll = sourceHandledThisLoop || (0LL == timeout_context->termTSR);

        if (MACH_PORT_NULL != dispatchPort && !didDispatchPortLastTime) {
#if DEPLOYMENT_TARGET_MACOSX || DEPLOYMENT_TARGET_EMBEDDED
            msg = (mach_msg_header_t *)msg_buffer;
            if (__CFRunLoopServiceMachPort(dispatchPort, &msg, sizeof(msg_buffer), 0)) {
                goto handle_msg;
            }
#elif DEPLOYMENT_TARGET_WINDOWS
            if (__CFRunLoopWaitForMultipleObjects(NULL, &dispatchPort, 0, 0, &livePort, NULL)) {
                goto handle_msg;
            }
#endif
        }

        didDispatchPortLastTime = false;

    if (!poll && (rlm->_observerMask & kCFRunLoopBeforeWaiting)) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeWaiting);
    __CFRunLoopSetSleeping(rl);
    // do not do any user callouts after this point (after notifying of sleeping)

        // Must push the local-to-this-activation ports in on every loop
        // iteration, as this mode could be run re-entrantly and we don't
        // want these ports to get serviced.

        __CFPortSetInsert(dispatchPort, waitSet);

    __CFRunLoopModeUnlock(rlm);
    __CFRunLoopUnlock(rl);

#if DEPLOYMENT_TARGET_MACOSX || DEPLOYMENT_TARGET_EMBEDDED
        if (kCFUseCollectableAllocator) {
            objc_clear_stack(0);
            memset(msg_buffer, 0, sizeof(msg_buffer));
        }
        msg = (mach_msg_header_t *)msg_buffer;
        __CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), poll ? 0 : TIMEOUT_INFINITY);
#elif DEPLOYMENT_TARGET_WINDOWS
        // Here, use the app-supplied message queue mask. They will set this if they are interested in having this run loop receive windows messages.
        // Note: don't pass 0 for polling, or this thread will never yield the CPU.
        __CFRunLoopWaitForMultipleObjects(waitSet, NULL, poll ? 0 : TIMEOUT_INFINITY, rlm->_msgQMask, &livePort, &windowsMessageReceived);
#endif
        
        __CFRunLoopLock(rl);
        __CFRunLoopModeLock(rlm);

        // Must remove the local-to-this-activation ports in on every loop
        // iteration, as this mode could be run re-entrantly and we don't
        // want these ports to get serviced. Also, we don't want them left
        // in there if this function returns.

        __CFPortSetRemove(dispatchPort, waitSet);

    rl->_ignoreWakeUps = true;

        // user callouts now OK again
    __CFRunLoopUnsetSleeping(rl);
    if (!poll && (rlm->_observerMask & kCFRunLoopAfterWaiting)) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopAfterWaiting);

        handle_msg:;
    rl->_ignoreWakeUps = true;

#if DEPLOYMENT_TARGET_MACOSX || DEPLOYMENT_TARGET_EMBEDDED
        mach_port_t livePort = msg ? msg->msgh_local_port : MACH_PORT_NULL;
#endif
#if DEPLOYMENT_TARGET_WINDOWS
        if (windowsMessageReceived) {
            // These Win32 APIs cause a callout, so make sure we're unlocked first and relocked after
            __CFRunLoopModeUnlock(rlm);
        __CFRunLoopUnlock(rl);

            if (rlm->_msgPump) {
                rlm->_msgPump();
            } else {
                MSG msg;
                if (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE | PM_NOYIELD)) {
                    TranslateMessage(&msg);
                    DispatchMessage(&msg);
                }
            }
            
            __CFRunLoopLock(rl);
        __CFRunLoopModeLock(rlm);
        sourceHandledThisLoop = true;
        } else
#endif
        if (MACH_PORT_NULL == livePort) {
            // handle nothing
        } else if (livePort == rl->_wakeUpPort) {
            // do nothing on Mac OS
#if DEPLOYMENT_TARGET_WINDOWS
            // Always reset the wake up port, or risk spinning forever
            ResetEvent(rl->_wakeUpPort);
#endif
        } else if (livePort == rlm->_timerPort) {
#if DEPLOYMENT_TARGET_WINDOWS
            // We use a manual reset timer to ensure that we don't miss timers firing because the run loop did the wakeUpPort this time
            // The only way to reset a timer is to reset the timer using SetWaitableTimer. We don't want it to fire again though, so we set the timeout to a large negative value. The timer may be reset again inside the timer handling code.
            LARGE_INTEGER dueTime;
            dueTime.QuadPart = LONG_MIN;
            SetWaitableTimer(rlm->_timerPort, &dueTime, 0, NULL, NULL, FALSE);
#endif
        __CFRunLoopDoTimers(rl, rlm, mach_absolute_time());
        } else if (livePort == dispatchPort) {
        __CFRunLoopModeUnlock(rlm);
        __CFRunLoopUnlock(rl);
            _CFSetTSD(__CFTSDKeyIsInGCDMainQ, (void *)6, NULL);
#if DEPLOYMENT_TARGET_MACOSX || DEPLOYMENT_TARGET_EMBEDDED
        _dispatch_main_queue_callback_4CF(msg);
#elif DEPLOYMENT_TARGET_WINDOWS
            _dispatch_main_queue_callback_4CF();
#endif
            _CFSetTSD(__CFTSDKeyIsInGCDMainQ, (void *)0, NULL);
        __CFRunLoopLock(rl);
        __CFRunLoopModeLock(rlm);
        sourceHandledThisLoop = true;
            didDispatchPortLastTime = true;
        } else {
            // Despite the name, this works for windows handles as well
            CFRunLoopSourceRef rls = __CFRunLoopModeFindSourceForMachPort(rl, rlm, livePort);
            if (rls) {
#if DEPLOYMENT_TARGET_MACOSX || DEPLOYMENT_TARGET_EMBEDDED
        mach_msg_header_t *reply = NULL;
        sourceHandledThisLoop = __CFRunLoopDoSource1(rl, rlm, rls, msg, msg->msgh_size, &reply) || sourceHandledThisLoop;
        if (NULL != reply) {
            (void)mach_msg(reply, MACH_SEND_MSG, reply->msgh_size, 0, MACH_PORT_NULL, 0, MACH_PORT_NULL);
            CFAllocatorDeallocate(kCFAllocatorSystemDefault, reply);
        }
#elif DEPLOYMENT_TARGET_WINDOWS
                sourceHandledThisLoop = __CFRunLoopDoSource1(rl, rlm, rls) || sourceHandledThisLoop;
#endif
        }
        } 
#if DEPLOYMENT_TARGET_MACOSX || DEPLOYMENT_TARGET_EMBEDDED
        if (msg && msg != (mach_msg_header_t *)msg_buffer) free(msg);
#endif
        
    __CFRunLoopDoBlocks(rl, rlm);

    if (sourceHandledThisLoop && stopAfterHandle) {
        retVal = kCFRunLoopRunHandledSource;
        } else if (timeout_context->termTSR < (int64_t)mach_absolute_time()) {
            retVal = kCFRunLoopRunTimedOut;
    } else if (__CFRunLoopIsStopped(rl)) {
            __CFRunLoopUnsetStopped(rl);
        retVal = kCFRunLoopRunStopped;
    } else if (rlm->_stopped) {
        rlm->_stopped = false;
        retVal = kCFRunLoopRunStopped;
    } else if (__CFRunLoopModeIsEmpty(rl, rlm, previousMode)) {
        retVal = kCFRunLoopRunFinished;
    }
    } while (0 == retVal);

    if (timeout_timer) {
        dispatch_source_cancel(timeout_timer);
        dispatch_release(timeout_timer);
    } else {
        free(timeout_context);
    }

    return retVal;
}

而根據(jù)源碼得知,run 和 runUntilDate 這兩個(gè)方法會(huì)不停的運(yùn)行上面這個(gè)消息循環(huán)函數(shù)從而做到一直監(jiān)聽消息哩盲。

RunLoop 怎樣才能退出呢

上面提到過前方,可以手動(dòng)結(jié)束一次消息循環(huán)狈醉,結(jié)束的函數(shù)就是這個(gè) CFRunLoopStop()。但是要注意惠险,這個(gè)函數(shù)只是結(jié)束一次消息循環(huán)舔糖,跟 runMode:beforeDate: 中 timer 超時(shí)的效果是一樣的。也就是停止等待 NSPort 傳遞消息莺匠,喚醒線程,獲取 CPU 時(shí)間片完成消息循環(huán)剩下的任務(wù)十兢。
然而 run 和 runUntilDate 這兩個(gè)方法還會(huì)啟動(dòng)下一次消息循環(huán)趣竣,這也就是用 run 方法啟動(dòng) RunLoop 后,無法 terminatel 的原因旱物。runUntileDate 會(huì)在 date 時(shí)間到達(dá)時(shí)不再啟動(dòng)新的消息循環(huán)遥缕,從而讓 RunLoop 真正退出。

只要明確了一次消息循環(huán)的過程宵呛,并且知道了 RunLoop 不斷執(zhí)行的原理是一次又一次啟動(dòng)消息循環(huán)单匣,那么,退出 RunLoop 的方式應(yīng)該明確了宝穗。

  1. 使用 run 啟動(dòng) RunLoop户秤,直到程序結(jié)束再退出。主線程的 RunLoop 一定是這種方式啟動(dòng)的逮矛。
  2. 使用 runUntileDate 啟動(dòng) RunLoop鸡号,date 時(shí)間到退出。這里的 date 會(huì)當(dāng)做參數(shù)傳到下面這個(gè)方法中须鼎,當(dāng) date 時(shí)間到鲸伴,RunLoop 也會(huì)因?yàn)槌瑫r(shí)而結(jié)束一次循環(huán)。
  3. 使用 runMode:beforeDate: 這種方式啟動(dòng)一次消息循環(huán)晋控,并且自己編寫代碼來控制一次消息循環(huán)結(jié)束后是否啟動(dòng)下一次消息循環(huán)汞窗。也就是類似這樣的代碼:
BOOL shouldKeepRunning = YES;        // global
NSRunLoop *theRL = [NSRunLoop currentRunLoop];
while (shouldKeepRunning && [theRL runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]]);
  1. 還有一種方式,就是文檔上說過赡译,如果 RunLoop 中沒有 NSPort仲吏,就不會(huì)啟動(dòng)下一次 RunLoop 了。如果沒有 NSPort捶朵,RunLoop 進(jìn)入阻塞狀態(tài)后誰負(fù)責(zé)喚醒呢蜘矢?當(dāng)然不能啟動(dòng)了對(duì)吧。但是文檔也說了综看,這種方式結(jié)束 RunLoop循環(huán)品腹,通常情況下由于無法持有所有 NSPort 的引用而做不到從 RunLoop 中移除所有 NSPort,是不推薦的方式红碑。
    注意上一段中一次 RunLoop 與 RunLoop 循環(huán)的區(qū)別舞吭,RunLoop 循環(huán)指的是一次 RunLoop 運(yùn)行結(jié)束時(shí)再啟動(dòng)一次 RunLoop泡垃,也就是 runUntileDate: 做的事情。
    RunLoop 模式可以節(jié)省 CPU 的并等待消息的關(guān)鍵就在于 RunLoop 會(huì)進(jìn)入阻塞狀態(tài)等待 NSPort 的消息將它喚醒羡鸥,底層是操作系統(tǒng)最基本的功能——線程同步蔑穴。通常情況下,這種消息是操作系統(tǒng)產(chǎn)生(例如觸摸屏幕等事件)惧浴,也可以由其它線程產(chǎn)生

看到這里存和,應(yīng)該明白 NSRunLoop 的意義以及線程保活的原理了衷旅。其實(shí)就是建立在操作系統(tǒng)線程同步的基礎(chǔ)上捐腿,比較底層的一種可以節(jié)省 CPU 又能實(shí)現(xiàn)進(jìn)程間通信的一種方式。這種 RunLoop 功能在各種操作系統(tǒng)上都有類似的實(shí)現(xiàn)柿顶,比如 Android 和 Windows茄袖。

NSRunLoop 的測試

  1. run啟動(dòng),NSMachPort 發(fā)送消息
    文檔上說了嘁锯,iOS 只支持 NSMachPort
- (void)setup{
    NSLog(@"%@",[[NSRunLoop currentRunLoop] currentMode]);
    
    
    self.thread = [[NSThread alloc]initWithTarget:self selector:@selector(threadTest) object:nil];
    [self.thread setName:@"Test Thread"];
    [self.thread start];

    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        [self.port sendBeforeDate:[NSDate date] components:[@[[@"hello" dataUsingEncoding:NSUTF8StringEncoding]] mutableCopy] from:nil reserved:0];
    });
    
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(6 * NSEC_PER_SEC)), dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        [self.port sendBeforeDate:[NSDate date] components:[@[[@"hello" dataUsingEncoding:NSUTF8StringEncoding]] mutableCopy] from:nil reserved:0];
    });
}

- (void)threadTest{
    NSLog(@"%@",@"child thread start");
    NSLog(@"%@",[NSThread currentThread]);
    NSRunLoop* runloop = [NSRunLoop currentRunLoop];
    self.port = [[NSMachPort alloc]init];
    self.port.delegate = self;
    [runloop addPort:self.port forMode:NSRunLoopCommonModes];
    [runloop run];
}

\#pragma NSMachPortDelegate
- (void)handleMachMessage:(void *)msg{
    NSLog(@"handle message thread:%@",[NSThread currentThread]);
}

輸出

2017-04-25 18:01:34.011602 test[2633:602345] kCFRunLoopDefaultMode
2017-04-25 18:01:34.013161 test[2633:604579] child thread start
2017-04-25 18:01:34.014156 test[2633:604579] <NSThread: 0x171660280>{number = 5, name = Test Thread}
2017-04-25 18:01:35.098691 test[2633:604579] handle message thread:<NSThread: 0x171660280>{number = 5, name = Test Thread}
2017-04-25 18:01:36.213378 test[2633:604579] handle message thread:<NSThread: 0x171660280>{number = 5, name = Test Thread}

1s 和2s 發(fā)送的消息都能收到宪祥。

  1. runUntilDate:啟動(dòng)并發(fā)送消息
    還是上面的代碼
    如果
[runloop runUntilDate:[NSDate dateWithTimeIntervalSinceNow:4]];

RunLoop 運(yùn)行時(shí)間只有4s,收不到6s 時(shí)發(fā)送的消息家乘,如果將運(yùn)行時(shí)間改長久可以收到了蝗羊。輸出

2017-04-25 18:05:57.176821 test[2657:607056] kCFRunLoopDefaultMode
2017-04-25 18:05:57.178433 test[2657:607261] child thread start
2017-04-25 18:05:57.186123 test[2657:607261] <NSThread: 0x17106b0c0>{number = 5, name = Test Thread}
2017-04-25 18:06:00.477071 test[2657:607261] handle message thread:<NSThread: 0x17106b0c0>{number = 5, name = Test Thread}

這里需要注意的是,如果[runloop runUntilDate:[NSDate dateWithTimeIntervalSinceNow:4]];時(shí)間改為5烤低,是有可能受到6s 發(fā)送的消息的肘交,這里可能是多線程運(yùn)行時(shí),線程運(yùn)行時(shí)機(jī)并不能精確確定導(dǎo)致的扑馁。

  1. runMode:beforeDate: 啟動(dòng)并發(fā)送消息
[runloop runMode:NSRunLoopCommonModes beforeDate:[NSDate dateWithTimeIntervalSinceNow:15]];

輸出:

2017-04-25 18:11:22.310154 test[2668:609276] kCFRunLoopDefaultMode
2017-04-25 18:11:22.312849 test[2668:609542] child thread start
2017-04-25 18:11:22.314500 test[2668:609542] <NSThread: 0x1758639c0>{number = 5, name = Test Thread}

無論如何也收不到第二個(gè)消息涯呻,這是由于接受第一個(gè)消息是,RunLoop 被喚醒腻要,處理完消息后直接退出了复罐,NSThread 線程也會(huì)結(jié)束。

深入研究 Runloop 與線程毙奂遥活
http://www.cocoabuilder.com/archive/cocoa/305204-runloop-not-being-stopped-by-cfrunloopstop.html
https://opensource.apple.com/source/CF/CF-635/CFRunLoop.c.auto.html

另外效诅,NSObject 有一個(gè)方法
performSelector: onThread: withObject: waitUntilDone: modes:
waitUntilDone這個(gè)參數(shù)可以指定在子線程運(yùn)行時(shí)阻塞當(dāng)前線程。具體實(shí)現(xiàn)原理可以參考下面這兩個(gè)鏈接趟济。
這里雖然是 Linux 的實(shí)現(xiàn)乱投,但是 iOS Linux Windows 都是遵循 POSIX 標(biāo)準(zhǔn)的。
之前介紹 thread join和detach的區(qū)別但是不詳細(xì) (詳細(xì)介紹)
pthread_join和pthread_detach的用法(轉(zhuǎn))

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末顷编,一起剝皮案震驚了整個(gè)濱河市戚炫,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌媳纬,老刑警劉巖双肤,帶你破解...
    沈念sama閱讀 207,248評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件施掏,死亡現(xiàn)場離奇詭異,居然都是意外死亡茅糜,警方通過查閱死者的電腦和手機(jī)七芭,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,681評(píng)論 2 381
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來蔑赘,“玉大人狸驳,你說我怎么就攤上這事∷跞” “怎么了锌历?”我有些...
    開封第一講書人閱讀 153,443評(píng)論 0 344
  • 文/不壞的土叔 我叫張陵,是天一觀的道長峦筒。 經(jīng)常有香客問我,道長窗慎,這世上最難降的妖魔是什么物喷? 我笑而不...
    開封第一講書人閱讀 55,475評(píng)論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮遮斥,結(jié)果婚禮上峦失,老公的妹妹穿的比我還像新娘。我一直安慰自己术吗,他們只是感情好尉辑,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,458評(píng)論 5 374
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著较屿,像睡著了一般隧魄。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上隘蝎,一...
    開封第一講書人閱讀 49,185評(píng)論 1 284
  • 那天购啄,我揣著相機(jī)與錄音,去河邊找鬼嘱么。 笑死狮含,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的曼振。 我是一名探鬼主播几迄,決...
    沈念sama閱讀 38,451評(píng)論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼冰评!你這毒婦竟也來了映胁?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,112評(píng)論 0 261
  • 序言:老撾萬榮一對(duì)情侶失蹤集索,失蹤者是張志新(化名)和其女友劉穎屿愚,沒想到半個(gè)月后汇跨,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,609評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡妆距,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,083評(píng)論 2 325
  • 正文 我和宋清朗相戀三年穷遂,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片娱据。...
    茶點(diǎn)故事閱讀 38,163評(píng)論 1 334
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡蚪黑,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出中剩,到底是詐尸還是另有隱情忌穿,我是刑警寧澤,帶...
    沈念sama閱讀 33,803評(píng)論 4 323
  • 正文 年R本政府宣布结啼,位于F島的核電站掠剑,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏郊愧。R本人自食惡果不足惜朴译,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,357評(píng)論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望属铁。 院中可真熱鬧眠寿,春花似錦、人聲如沸焦蘑。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,357評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽例嘱。三九已至狡逢,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間拼卵,已是汗流浹背甚侣。 一陣腳步聲響...
    開封第一講書人閱讀 31,590評(píng)論 1 261
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留间学,地道東北人殷费。 一個(gè)月前我還...
    沈念sama閱讀 45,636評(píng)論 2 355
  • 正文 我出身青樓,卻偏偏與公主長得像低葫,于是被迫代替她去往敵國和親详羡。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,925評(píng)論 2 344

推薦閱讀更多精彩內(nèi)容