Run Loops

Run Loops

Run loops是线程基础机构中非常重要的一环。它是一个处理事件的循环,帮助你安排事件工作和协调接收到的事件。其目的是在工作时让线程处理,在没工作时让线程休眠。
runloop的不是完全自管理的,你必须在适当的时机启动它来处理接收到的事件。你可以使用Cocoa和Core Foundation中的run loop object来配置和管理线程的runloop。你不必手动创建一个runloop,因为包括主线程在内的每个线程都包含一个run loop object。但是你得手动启动子线程的runloop,而主线程的runloop在引用启动时就被启动运行了。

Run Loop解析

runloop的内部是一个do-while循环。它接收两种不同类型的sources(Input sourcesTimer sources)分发的事件。Input sources分发异步的事件,Timer sources分发同步的事件。
image.png
此外,在处理sources分发的事件时,runloop同时会产生关于runloop状态的通知。你可以注册runloop observers来接收这些通知。

Run Loop Modes

runloop mode是一个input sources、timers和observers的集合。每次你运行一个runloop时,你必须指定一个mode。同时只有在这个mode中的input sources、timers和observers才能得到处理。
每个mode都可以指定一个name用来区分。

Mode Name Description
Default NSDefaultRunLoopMode kCFRunLoopDefaultMode 默认mode,runloop中最常用的mode。
Connection NSConnectionReplyMode 用于监测NSConnection对象的回应,很少需要用到。
Modal NSModalPanelRunLoopMode 用于识别事件的modal。
Event tracking NSEventTrackingRunLoopMode 追踪触摸手势,限制其他接收到的事件,确保界面刷新不会卡顿。
Common modes NSRunLoopCommonModes kCFRunLoopCommonModes 常用的modes组合。默认包含default、modal和event tracking modes。
### Input Sources ###
input sources包含port-based sources和custom input sources。port-based sources监听应用的Mach ports。custom input sources监听事件的custom sources。
#### Port-Based Sources(source1) ####
在Cocoa中,你只需要使用NSPort来增加port到runloop中。这个port对象会为你处理需要的input sources的创建和配置。在Core Foundation中,你必须通过CFMachPortRef,CFMessagePortRef,CFSocketRef手动创建port和它的runloop source。
#### Custom Input Sources(source0) ####
你需要使用CFRunLoopSourceRef相关的函数来创建一个自定义输入源。你要通过回调函数来配置自定义输入源,从而处理接收到的事件和在从runloop中移除时关闭source。同时你必须定义事件的分发机制。相关的例子可以参考 Defining a Custom Input Source。它主要处理UIEvent、CFSocket这样的事件。
#### Cocoa Perform Selector Sources ####
Cocoa定义了一种自定义输入源,它允许你在任何线程上perform selector。与port-based sources类似,在目标线程上perform selector请求也是连续的,这样减轻了线程中多方法执行的同步问题。与port-based sources不同的是,perform selector在执行后自动从runloop中移除。
当然,perform selector要想在指定线程中执行,这个线程的runloop必须是启动的。
### Timer Sources ###
计时器用来通知线程做指定的一些事情。但是计时器的时间不是实时的,有时可能会延后。当这个线程的runloop不在计时器指定的mode中时,那么计时器将会被暂停,直到runloop重新回到计时器指定的mode中去。
### Run Loop Observers ###
一个计时器或者输入源开始时会给观察者发送通知。runloop中的通知包含以下:
- 进入runloop
- runloop即将处理计时器
- runloop即将处理输入源
- runloop即将进入睡眠
- runloop被唤醒
- runloop退出

run loop事件队列

image.png

什么时候使用Run Loop

只有当你创建一个子线程时你才需要启动runloop。对于子线程,你需要考虑是否需要启动runloop。比如,你需要执行一些耗时的操作时,就不需要启动runloop。启动runloop是为了能够与线程互动。在以下情况可能需要启动runloop:

  • 使用输入源来与其他线程通信
  • 在子线程上使用计时器
  • 使用performselector...方法
  • 需要子线程执行周期性任务

Run Loop实践

TableView中实现平滑滚动延迟加载图片

由于滚动时runloop的current mode是UITrackingRunLoopMode。如果此时图片在加载且较慢的话,会影响runloop的循环,导致滚动卡顿。因此可以利用CFRunLoopMode特性,将图片的加载放到NSDefaultRunLoopMode的mode里,这样在滚动的时候就不会加载图片,等滚动完current mode切换为NSDefaultRunLoopMode时再加载图片。

UIImage *downloadedImage = ...;
[self.avatarImageView performSelector:@selector(setImage:)
 withObject:downloadedImage
 afterDelay:0
 inModes:@[NSDefaultRunLoopMode]];

卡顿检测

通过分析上面讲过的runloop事件队列,可以发现runloop处理事件主要集中在step3(kCFRunLoopBeforeSources)之后以及step9(kCFRunLoopAfterWaiting)之后。所以可以创建一个observer来观察这两个状态,同时通过semphore来监测这两个状态的处理时间。如果好几次出现大于某个值则认为是出现了UI卡顿。

- (void)startMonitor {
    if (_runLoopObserver) {
        return;
    }
    self.semaphore = dispatch_semaphore_create(0);

    CFRunLoopObserverContext context = {
    0,
    (__bridge void*)self,
    NULL,
    NULL
    };
    _runLoopObserver = CFRunLoopObserverCreate(kCFAllocatorDefault, kCFRunLoopAllActivities, YES, 0, &runLoopObserverCallBack, &context);

    CFRunLoopAddObserver(CFRunLoopGetMain(), _runLoopObserver, kCFRunLoopCommonModes);

    dispatch_async(fluency_monitor_queue(), ^{
        while (YES) {
            long st = dispatch_semaphore_wait(self.semaphore, dispatch_time(DISPATCH_TIME_NOW, 500 * NSEC_PER_MSEC));
            if (st != 0) {
                if (!_runLoopObserver) {
                    _timeoutCount = 0;
                    self.semaphore = 0;
                    self.runLoopActivity = 0;
                    return;
                }

                if (self.runLoopActivity == kCFRunLoopBeforeSources || self.runLoopActivity == kCFRunLoopAfterWaiting) {
                    if (++_timeoutCount < 3) {
                        continue;
                    }
                    //出现了UI卡顿。可以打印堆栈信息看看哪个线程出了问题。

                }
            }
        }
    });
}

static void runLoopObserverCallBack(CFRunLoopObserverRef observer,
                                CFRunLoopActivity activity,
                                void* info) {
    StackMonitor *monitor = (__bridge StackMonitor*)info;
    monitor.runLoopActivity = activity;
    dispatch_semaphore_signal(monitor.semaphore);
}

Run Loop源码

CFRunLoopRef的代码是开源的,你可以在http://opensource.apple.com/tarballs/CF/里面下载到包含CFRunLoopRef在内的整个CoreFoundation源码。

CFRunLoop结构体如下,包含了对应的线程,modes等。

struct __CFRunLoop {
    CFRuntimeBase _base;
    pthread_mutex_t _lock;            /* locked for accessing mode list */
    __CFPort _wakeUpPort;            // used for CFRunLoopWakeUp
    Boolean _unused;
    volatile _per_run_data *_perRunData;              // reset for runs of the run loop
    pthread_t _pthread;        //runloop对应的线程
    uint32_t _winthread;
    CFMutableSetRef _commonModes;        //所有标记为common的mode集合
    CFMutableSetRef _commonModeItems;
    CFRunLoopModeRef _currentMode;        //当前运行的mode
    CFMutableSetRef _modes;
    struct _block_item *_blocks_head;
    struct _block_item *_blocks_tail;
    CFAbsoluteTime _runTime;
    CFAbsoluteTime _sleepTime;
    CFTypeRef _counterpart;
};

RunLoopMode包含了名称和source0、source1、observers、timers的集成等。RunLoop管理RunLoopMode,而RunLoopMode管理具体的事件。

struct __CFRunLoopMode {
    CFRuntimeBase _base;
    pthread_mutex_t _lock;    /* must have the run loop locked before locking this */
    CFStringRef _name;    //mode名称
    Boolean _stopped;    //mode是否被终止
    char _padding[3];
    CFMutableSetRef _sources0;    //source0(custom input source)
    CFMutableSetRef _sources1;    //source1(port-based input source)
    CFMutableArrayRef _observers;    //通知者
    CFMutableArrayRef _timers;    //计时器
    CFMutableDictionaryRef _portToV1SourceMap;    //记录source1port的字典
    __CFPortSet _portSet;
    CFIndex _observerMask;      //观察的runloop的状态
#if USE_DISPATCH_SOURCE_FOR_TIMERS
    dispatch_source_t _timerSource;
    dispatch_queue_t _queue;
    Boolean _timerFired; // set to true by the source when a timer has fired
    Boolean _dispatchTimerArmed;
#endif
#if USE_MK_TIMER_TOO
    mach_port_t _timerPort;
    Boolean _mkTimerArmed;
#endif
#if DEPLOYMENT_TARGET_WINDOWS
    DWORD _msgQMask;
    void (*_msgPump)(void);
#endif
    uint64_t _timerSoftDeadline; /* TSR */
    uint64_t _timerHardDeadline; /* TSR */
};

CFRunLoopSource结构体如下:

struct __CFRunLoopSource {
    CFRuntimeBase _base;
    uint32_t _bits;
    pthread_mutex_t _lock;
    CFIndex _order;            /* immutable */
    CFMutableBagRef _runLoops;    //添加该source的runloop
    union {
    CFRunLoopSourceContext version0;    /* immutable, except invalidation */
        CFRunLoopSourceContext1 version1;    /* immutable, except invalidation */
    } _context;
};

CFRunLoopObserver,观察runloop的状态,并抛出回调。

struct __CFRunLoopObserver {
    CFRuntimeBase _base;
    pthread_mutex_t _lock;
    CFRunLoopRef _runLoop;      //观察的runloop
    CFIndex _rlCount;
    CFOptionFlags _activities;        /* immutable */     //状态
    CFIndex _order;            /* immutable */
    CFRunLoopObserverCallBack _callout;    /* immutable */
    CFRunLoopObserverContext _context;    /* immutable, except invalidation */
};

CFRunLoopActivity的6种状态。

typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
    kCFRunLoopEntry = (1UL << 0), //即将进入run loop
    kCFRunLoopBeforeTimers = (1UL << 1), //即将处理timer
    kCFRunLoopBeforeSources = (1UL << 2),//即将处理source
    kCFRunLoopBeforeWaiting = (1UL << 5),//即将进入休眠
    kCFRunLoopAfterWaiting = (1UL << 6),//被唤醒但是还没开始处理事件
    kCFRunLoopExit = (1UL << 7),//run loop已经退出
    kCFRunLoopAllActivities = 0x0FFFFFFFU
};

CFRunLoopTimer可以在设定的时间点抛出回调,另外它包含了mode集合,因此可以被添加到任何mode中去。

struct __CFRunLoopTimer {
    CFRuntimeBase _base;
    uint16_t _bits;
    pthread_mutex_t _lock;
    CFRunLoopRef _runLoop;    //添加该timer的runloop
    CFMutableSetRef _rlModes;    //mode集合
    CFAbsoluteTime _nextFireDate;    //下一次计时器开始时间
    CFTimeInterval _interval;        /* immutable */    //理想时间间隔
    CFTimeInterval _tolerance;          /* mutable */    //时间偏差
    uint64_t _fireTSR;            /* TSR units */
    CFIndex _order;            /* immutable */
    CFRunLoopTimerCallBack _callout;    /* immutable */
    CFRunLoopTimerContext _context;    /* immutable, except invalidation */
};

在Core Foundation中通过以下两个API来启动runloop。第一个使用default mode,第二个使用指定的mode。

  • void CFRunLoopRun(void)
  • SInt32 CFRunLoopRunInMode(CFStringRef mode, CFTimeInterval seconds, Boolean returnAfterSourceHandled);
void CFRunLoopRun(void) {    /* DOES CALLOUT */
    int32_t result;
//do-while循环
    do {
        //DefaultMode方式启动CFRunLoopRunSpecific
        result = CFRunLoopRunSpecific(CFRunLoopGetCurrent(),kCFRunLoopDefaultMode, 1.0e10, false);
    CHECK_FOR_FORK();
    } while (kCFRunLoopRunStopped != result && kCFRunLoopRunFinished != result);
}

SInt32 CFRunLoopRunInMode(CFStringRef modeName, CFTimeInterval seconds, Boolean returnAfterSourceHandled) {     /* DOES CALLOUT */
    CHECK_FOR_FORK();
    return CFRunLoopRunSpecific(CFRunLoopGetCurrent(), modeName, seconds, returnAfterSourceHandled);
}

通过源码可知,两种方式内部都是运行了CFRunLoopRunSpecific函数。我们来看一下实现。

SInt32 CFRunLoopRunSpecific(CFRunLoopRef rl, CFStringRef modeName, CFTimeInterval seconds, Boolean returnAfterSourceHandled) {     /* DOES CALLOUT */
    CHECK_FOR_FORK();
    //1.如果已经释放掉r1,返回kCFRunLoopRunFinished
    if (__CFRunLoopIsDeallocating(rl)) return kCFRunLoopRunFinished;
    //2.对r1上锁
    __CFRunLoopLock(rl);
    //3.根据modeName找到当前的mode
    CFRunLoopModeRef currentMode = __CFRunLoopFindMode(rl, modeName, false);
    //4.如果没有找到或者mode为空,退出runloop
    if (NULL == currentMode || __CFRunLoopModeIsEmpty(rl, currentMode, rl->_currentMode)) {
    Boolean did = false;
    if (currentMode) __CFRunLoopModeUnlock(currentMode);
    __CFRunLoopUnlock(rl);
    return did ? kCFRunLoopRunHandledSource : kCFRunLoopRunFinished;
    }
    volatile _per_run_data *previousPerRun = __CFRunLoopPushPerRunData(rl);
    //5.取出先前的mode
    CFRunLoopModeRef previousMode = rl->_currentMode;
    //6.设置当前的mode
    rl->_currentMode = currentMode;
    //7.初始化一个kCFRunLoopRunFinished的result
    int32_t result = kCFRunLoopRunFinished;
    //8.如果当前_observerMask为kCFRunLoopEntry,通知observer,然后进入__CFRunLoopRun
    if (currentMode->_observerMask & kCFRunLoopEntry ) __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopEntry);
    result = __CFRunLoopRun(rl, currentMode, seconds, returnAfterSourceHandled, previousMode);
    //9.如果当前_observerMask为kCFRunLoopExit,通知observer,退出
    if (currentMode->_observerMask & kCFRunLoopExit ) __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopExit);

        __CFRunLoopModeUnlock(currentMode);
        __CFRunLoopPopPerRunData(rl, previousPerRun);
    rl->_currentMode = previousMode;
    __CFRunLoopUnlock(rl);
    return result;
}

由上面的代码可知,mode必须存在,且包含modeItem,这个runloop才能运行。在进入和退出runloop之前通知observer。另外,runloop的核心函数是__CFRunLoopRun。我们来看一下它的逻辑。

static int32_t __CFRunLoopRun(CFRunLoopRef rl, CFRunLoopModeRef rlm, CFTimeInterval seconds, Boolean stopAfterHandle, CFRunLoopModeRef previousMode) {
    ........
    //2.通知observer,即将触发计时器回调
    if (rlm->_observerMask & kCFRunLoopBeforeTimers) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeTimers);
    //3.通知observer,即将触发source0回调
    if (rlm->_observerMask & kCFRunLoopBeforeSources) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeSources);
    //执行加入当前runloop的block
__CFRunLoopDoBlocks(rl, rlm);

    //4.处理source0事件
    Boolean sourceHandledThisLoop = __CFRunLoopDoSources0(rl, rlm, stopAfterHandle);
    if (sourceHandledThisLoop) {
        //执行加入当前runloop的block
        __CFRunLoopDoBlocks(rl, rlm);
    }

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

    if (MACH_PORT_NULL != dispatchPort && !didDispatchPortLastTime) {
#if DEPLOYMENT_TARGET_MACOSX || DEPLOYMENT_TARGET_EMBEDDED || DEPLOYMENT_TARGET_EMBEDDED_MINI
        msg = (mach_msg_header_t *)msg_buffer;
        //5.接收dispatchPort端口的source1事件,如果接收到,前往第9步处理msg
        if (__CFRunLoopServiceMachPort(dispatchPort, &msg, sizeof(msg_buffer), &livePort, 0, &voucherState, NULL)) {

            goto handle_msg;
        }
#elif DEPLOYMENT_TARGET_WINDOWS
        if (__CFRunLoopWaitForMultipleObjects(NULL, &dispatchPort, 0, 0, &livePort, NULL)) {
            goto handle_msg;
        }
#endif
    }

    didDispatchPortLastTime = false;

    //6.通知observer,runloop即将休眠
if (!poll && (rlm->_observerMask & kCFRunLoopBeforeWaiting)) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeWaiting);
    //runloop进入休眠
__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);

    CFAbsoluteTime sleepStart = poll ? 0.0 : CFAbsoluteTimeGetCurrent();

#if DEPLOYMENT_TARGET_MACOSX || DEPLOYMENT_TARGET_EMBEDDED || DEPLOYMENT_TARGET_EMBEDDED_MINI
#if USE_DISPATCH_SOURCE_FOR_TIMERS
    //7.do-while循环,用于接收等待端口的消息。
    // • 一个内核端口的source事件
    // • 一个计时器到时间了
    // • RunLoop自身的超时时间到了
    // • 被其他什么调用者手动唤醒
    //满足以上四个条件中一个,退出休眠
    do {
        if (kCFUseCollectableAllocator) {
            // objc_clear_stack(0);
            // <rdar://problem/16393959>
            memset(msg_buffer, 0, sizeof(msg_buffer));
        }
        msg = (mach_msg_header_t *)msg_buffer;

        __CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort, poll ? 0 : TIMEOUT_INFINITY, &voucherState, &voucherCopy);

        if (modeQueuePort != MACH_PORT_NULL && livePort == modeQueuePort) {
            // Drain the internal queue. If one of the callout blocks sets the timerFired flag, break out and service the timer.
            while (_dispatch_runloop_root_queue_perform_4CF(rlm->_queue));
            if (rlm->_timerFired) {
                // Leave livePort as the queue port, and service timers below
                rlm->_timerFired = false;
                break;
            } else {
                if (msg && msg != (mach_msg_header_t *)msg_buffer) free(msg);
            }
        } else {
            // Go ahead and leave the inner loop.
            break;
        }
    } while (1);
#else
    if (kCFUseCollectableAllocator) {
        // objc_clear_stack(0);
        // <rdar://problem/16393959>
        memset(msg_buffer, 0, sizeof(msg_buffer));
    }
    msg = (mach_msg_header_t *)msg_buffer;
    __CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort, poll ? 0 : TIMEOUT_INFINITY, &voucherState, &voucherCopy);
#endif

#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.
    __CFRunLoopWaitForMultipleObjects(waitSet, NULL, poll ? 0 : TIMEOUT_INFINITY, rlm->_msgQMask, &livePort, &windowsMessageReceived);
#endif

    __CFRunLoopLock(rl);
    __CFRunLoopModeLock(rlm);

    rl->_sleepTime += (poll ? 0.0 : (CFAbsoluteTimeGetCurrent() - sleepStart));

    // 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);

    __CFRunLoopSetIgnoreWakeUps(rl);

    // user callouts now OK again
__CFRunLoopUnsetSleeping(rl);
    //8.通知observer,runloop被唤醒
if (!poll && (rlm->_observerMask & kCFRunLoopAfterWaiting)) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopAfterWaiting);

    //9.处理接收到的事件
    handle_msg:;
    __CFRunLoopSetIgnoreWakeUps(rl);

#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;

        // To prevent starvation of sources other than the message queue, we check again to see if any other sources need to be serviced
        // Use 0 for the mask so windows messages are ignored this time. Also use 0 for the timeout, because we're just checking to see if the things are signalled right now -- we will wait on them again later.
        // NOTE: Ignore the dispatch source (it's not in the wait set anymore) and also don't run the observers here since we are polling.
        __CFRunLoopSetSleeping(rl);
        __CFRunLoopModeUnlock(rlm);
        __CFRunLoopUnlock(rl);

        __CFRunLoopWaitForMultipleObjects(waitSet, NULL, 0, 0, &livePort, NULL);

        __CFRunLoopLock(rl);
        __CFRunLoopModeLock(rlm);            
        __CFRunLoopUnsetSleeping(rl);
        // If we have a new live port then it will be handled below as normal
    }

#endif
    if (MACH_PORT_NULL == livePort) {
        CFRUNLOOP_WAKEUP_FOR_NOTHING();
        // handle nothing
    } else if (livePort == rl->_wakeUpPort) {
        CFRUNLOOP_WAKEUP_FOR_WAKEUP();
        // do nothing on Mac OS
#if DEPLOYMENT_TARGET_WINDOWS
        // Always reset the wake up port, or risk spinning forever
        ResetEvent(rl->_wakeUpPort);
#endif
    }
#if USE_DISPATCH_SOURCE_FOR_TIMERS
    else if (modeQueuePort != MACH_PORT_NULL && livePort == modeQueuePort) {
        CFRUNLOOP_WAKEUP_FOR_TIMER();
        if (!__CFRunLoopDoTimers(rl, rlm, mach_absolute_time())) {
            // Re-arm the next timer, because we apparently fired early
            __CFArmNextTimerInMode(rlm, rl);
        }
    }
#endif
#if USE_MK_TIMER_TOO
    //9.1如果计时器时间到了,触发回调
    else if (rlm->_timerPort != MACH_PORT_NULL && livePort == rlm->_timerPort) {
        CFRUNLOOP_WAKEUP_FOR_TIMER();
        // On Windows, we have observed an issue where the timer port is set before the time which we requested it to be set. For example, we set the fire time to be TSR 167646765860, but it is actually observed firing at TSR 167646764145, which is 1715 ticks early. The result is that, when __CFRunLoopDoTimers checks to see if any of the run loop timers should be firing, it appears to be 'too early' for the next timer, and no timers are handled.
        // In this case, the timer port has been automatically reset (since it was returned from MsgWaitForMultipleObjectsEx), and if we do not re-arm it, then no timers will ever be serviced again unless something adjusts the timer list (e.g. adding or removing timers). The fix for the issue is to reset the timer here if CFRunLoopDoTimers did not handle a timer itself. 9308754
        if (!__CFRunLoopDoTimers(rl, rlm, mach_absolute_time())) {
            // Re-arm the next timer
            __CFArmNextTimerInMode(rlm, rl);
        }
    }
#endif
    //9.2如果是dispatch到main_queue的block,执行block
    else if (livePort == dispatchPort) {
        CFRUNLOOP_WAKEUP_FOR_DISPATCH();
        __CFRunLoopModeUnlock(rlm);
        __CFRunLoopUnlock(rl);
        _CFSetTSD(__CFTSDKeyIsInGCDMainQ, (void *)6, NULL);
#if DEPLOYMENT_TARGET_WINDOWS
        void *msg = 0;
#endif
        __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(msg);
        _CFSetTSD(__CFTSDKeyIsInGCDMainQ, (void *)0, NULL);
        __CFRunLoopLock(rl);
        __CFRunLoopModeLock(rlm);
        sourceHandledThisLoop = true;
        didDispatchPortLastTime = true;
    } else {
        CFRUNLOOP_WAKEUP_FOR_SOURCE();

        // If we received a voucher from this mach_msg, then put a copy of the new voucher into TSD. CFMachPortBoost will look in the TSD for the voucher. By using the value in the TSD we tie the CFMachPortBoost to this received mach_msg explicitly without a chance for anything in between the two pieces of code to set the voucher again.
        voucher_t previousVoucher = _CFSetTSD(__CFTSDKeyMachMessageHasVoucher, (void *)voucherCopy, os_release);

        // Despite the name, this works for windows handles as well
        CFRunLoopSourceRef rls = __CFRunLoopModeFindSourceForMachPort(rl, rlm, livePort);
        if (rls) {
            //9.3如果有source1事件,处理source1事件
#if DEPLOYMENT_TARGET_MACOSX || DEPLOYMENT_TARGET_EMBEDDED || DEPLOYMENT_TARGET_EMBEDDED_MINI
    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
    }

        // Restore the previous voucher
        _CFSetTSD(__CFTSDKeyMachMessageHasVoucher, previousVoucher, os_release);

    }
#if DEPLOYMENT_TARGET_MACOSX || DEPLOYMENT_TARGET_EMBEDDED || DEPLOYMENT_TARGET_EMBEDDED_MINI
    if (msg && msg != (mach_msg_header_t *)msg_buffer) free(msg);
#endif

        //执行加入到loop中的block
    __CFRunLoopDoBlocks(rl, rlm);

    if (sourceHandledThisLoop && stopAfterHandle) {
        retVal = kCFRunLoopRunHandledSource;
        } else if (timeout_context->termTSR < mach_absolute_time()) {   //runloop超时
            retVal = kCFRunLoopRunTimedOut;
    } else if (__CFRunLoopIsStopped(rl)) {  //runloop被stop
            __CFRunLoopUnsetStopped(rl);
        retVal = kCFRunLoopRunStopped;
    } else if (rlm->_stopped) {     //mode被stop
        rlm->_stopped = false;
        retVal = kCFRunLoopRunStopped;
    } else if (__CFRunLoopModeIsEmpty(rl, rlm, previousMode)) {     //mode为空
        retVal = kCFRunLoopRunFinished;
    }

#if DEPLOYMENT_TARGET_MACOSX || DEPLOYMENT_TARGET_EMBEDDED || DEPLOYMENT_TARGET_EMBEDDED_MINI
        voucher_mach_msg_revert(voucherState);
        os_release(voucherCopy);
#endif
        //如果没超时,mode没空,loop没被停止,继续loop
    } while (0 == retVal);

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

    return retVal;
}

其主要执行步骤如下:

  1. 通知observer,即将进入runloop。(前面已执行)
  2. 通知observer,即将触发计时器回调
  3. 通知observer,即将触发source0回调
  4. 处理source0事件
  5. 接收dispatchPort端口的source1事件,如果接收到,前往第9步处理msg
  6. 通知observer,runloop即将休眠
  7. do-while循环,用于接收等待端口的消息来终止休眠
    7.1 内核端口的source事件
    7.2 计时器到时间了
    7.3 RunLoop自身的超时时间到了
    7.4 被手动唤醒
  8. 通知observer,runloop被唤醒
  9. 处理接收到的事件
    9.1 如果计时器时间到了,触发回调重新进入循环
    9.2 如果是dispatch到main_queue的block(手动唤醒),执行block
    9.3 如果有source1事件,处理source1事件
  10. 如果此时runloop超时或者被停止,mode为空或者被停止,则退出循环,否则再次进入步骤2

总结

本文主要通过源码源码分析了runloop的循环机制以及事件机制。同时介绍了几种runloop的应用场景。

参考

Run Loops
深入理解RunLoop
RunLoop系列之源码分析
iOS开发优化篇之卡顿检测
CFRunLoop

Author: MrHook
Link: https://bigjar.github.io/2018/01/29/Run-Loops/
Copyright Notice: All articles in this blog are licensed under CC BY-NC-SA 4.0 unless stating additionally.