APM - iOS 卡顿监控 Runloop

247 阅读11分钟

简介

Runloop是一种Event Loop的循环机制。

en.wikipedia.org/wiki/Event_…

function loop() { 

    initialize(); 

    do { 

        var message = get_next_message(); 

        process_message(message); 

    } while (message != quit); 

}

Runloop与线程

Runloop与线程是紧密关联的,包括主线程在内的线程都有对应的Runloop对象,我们无法创建Runloop对象,但是可以获取。

主线程的Runloop会在启动的时候启动,其他线程的Runloop默认不会启动,需要我们手动启动。

Runloop的结构

在Runloop中包含若干个Mode,每个Mode中又包含了若干个Source/Observer/Timer。Mode之间是相互切换的关系。比如初次启动的时候是UIInitializationRunLoopMode,启动完成后切换成NSDefaultRunLoopMode,UI滑动的时候切换成NSEventTrackingRunLoopMode。

  • Mode

    • NSRunLoopCommonModes

    • NSDefaultRunLoopMode

    • NSEventTrackingRunLoopMode

    • 其他

      • NSConnectionReplyMode
      • NSModalPanelRunLoopMode
      • UIInitializationRunLoopMode
  • Source

    • Source0
    • Source1(基于mach_port)
  • Timer



<CFRunLoop 0x600000cd4180 [0x1ba9abc60]>{wakeup port = 0xc03, stopped = false, ignoreWakeUps = true, 

current mode = (none),

common modes = <CFBasicHash 0x6000037d5410 [0x1ba9abc60]>{type = mutable set, count = 1,

entries =>

        2 : <CFString 0x1babe4400 [0x1ba9abc60]>{contents = "kCFRunLoopDefaultMode"}

}

,

common mode items = <CFBasicHash 0x6000037d54d0 [0x1ba9abc60]>{type = mutable set, count = 2,

entries =>

        1 : <CFRunLoopObserver 0x6000008d8140 [0x1ba9abc60]>{valid = Yes, activities = 0xfffffff, repeats = Yes, order = -9223372036854775808, callout = myRunloopBeginCallback (0x10291d248), context = <CFRunLoopObserver context 0x600002cd4580>}

        2 : <CFRunLoopObserver 0x6000008d81e0 [0x1ba9abc60]>{valid = Yes, activities = 0xfffffff, repeats = Yes, order = 9223372036854775807, callout = myRunloopEndCallback (0x10291d3b4), context = <CFRunLoopObserver context 0x600002cd4580>}

}

,

modes = <CFBasicHash 0x6000037d5440 [0x1ba9abc60]>{type = mutable set, count = 2,

entries =>

        0 : <CFRunLoopMode 0x6000002d8270 [0x1ba9abc60]>{name = UIInitializationRunLoopMode, port set = 0xf03, queue = 0x6000017d4700, source = 0x6000017d4800 (not fired), timer port = 0x1503, 

        sources0 = (null),

        sources1 = (null),

        observers = (

    "<CFRunLoopObserver 0x6000008d8280 [0x1ba9abc60]>{valid = Yes, activities = 0xfffffff, repeats = Yes, order = -9223372036854775808, callout = myInitialModeRunloopBeginCallback (0x10291d458), context = <CFRunLoopObserver context 0x600002cd4580>}",

    "<CFRunLoopObserver 0x6000008d8320 [0x1ba9abc60]>{valid = Yes, activities = 0xfffffff, repeats = Yes, order = 9223372036854775807, callout = myInitialModeRunloopEndCallback (0x10291d5e4), context = <CFRunLoopObserver context 0x600002cd4580>}"

),

        timers = (null),

        currently 705171653 (47641810350801) / soft deadline in: 7.68612351e+11 sec (@ -1) / hard deadline in: 7.68612351e+11 sec (@ -1)

},



        2 : <CFRunLoopMode 0x6000002d81a0 [0x1ba9abc60]>{name = kCFRunLoopDefaultMode, port set = 0xd03, queue = 0x6000017d4500, source = 0x6000017d4680 (not fired), timer port = 0x1103, 

        sources0 = (null),

        sources1 = (null),

        observers = (

    "<CFRunLoopObserver 0x6000008d8140 [0x1ba9abc60]>{valid = Yes, activities = 0xfffffff, repeats = Yes, order = -9223372036854775808, callout = myRunloopBeginCallback (0x10291d248), context = <CFRunLoopObserver context 0x600002cd4580>}",

    "<CFRunLoopObserver 0x6000008d81e0 [0x1ba9abc60]>{valid = Yes, activities = 0xfffffff, repeats = Yes, order = 9223372036854775807, callout = myRunloopEndCallback (0x10291d3b4), context = <CFRunLoopObserver context 0x600002cd4580>}"

),

        timers = (null),

        currently 705171653 (47641810359288) / soft deadline in: 7.68612351e+11 sec (@ -1) / hard deadline in: 7.68612351e+11 sec (@ -1)

},



}

}

Runloop的流程

*图中左侧为Source1(port)

在添加了BeginObserver和EndObserver之后,我们可以打印如下

kCFRunLoopEntry
kCFRunLoopBeforeTimers
kCFRunLoopBeforeSources
kCFRunLoopBeforeTimers
kCFRunLoopBeforeSources
kCFRunLoopBeforeTimers
kCFRunLoopBeforeSources
kCFRunLoopBeforeTimers
kCFRunLoopBeforeSources
kCFRunLoopBeforeTimers
kCFRunLoopBeforeSources
kCFRunLoopBeforeTimers
kCFRunLoopBeforeSources
kCFRunLoopBeforeTimers
kCFRunLoopBeforeSources
kCFRunLoopBeforeTimers
kCFRunLoopBeforeSources
kCFRunLoopBeforeWaiting


kCFRunLoopAfterWaiting
kCFRunLoopBeforeTimers
kCFRunLoopBeforeSources
kCFRunLoopBeforeWaiting


kCFRunLoopAfterWaiting
kCFRunLoopBeforeTimers
kCFRunLoopBeforeSources
kCFRunLoopBeforeTimers
kCFRunLoopBeforeSources
kCFRunLoopBeforeTimers
kCFRunLoopBeforeSources
kCFRunLoopBeforeWaiting

从上面打印出来的信息可以看到,如果有Source1的情况下,暂时不会进去waiting,而是不断处理Timer和Source。

// CF-1153.18

void CFRunLoopRun(void) {   /* DOES CALLOUT */

    int32_t result;

    do {

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

}



SInt32 CFRunLoopRunSpecific(CFRunLoopRef rl, CFStringRef modeName, CFTimeInterval seconds, Boolean returnAfterSourceHandled) {     /* DOES CALLOUT */

    CHECK_FOR_FORK();

    if (__CFRunLoopIsDeallocating(rl)) return kCFRunLoopRunFinished;

    __CFRunLoopLock(rl);

    CFRunLoopModeRef currentMode = __CFRunLoopFindMode(rl, modeName, false);

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

    CFRunLoopModeRef previousMode = rl->_currentMode;

    rl->_currentMode = currentMode;

    int32_t result = kCFRunLoopRunFinished;



    if (currentMode->_observerMask & kCFRunLoopEntry ) __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopEntry);

    result = __CFRunLoopRun(rl, currentMode, seconds, returnAfterSourceHandled, previousMode);

    if (currentMode->_observerMask & kCFRunLoopExit ) __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopExit);



        __CFRunLoopModeUnlock(currentMode);

        __CFRunLoopPopPerRunData(rl, previousPerRun);

    rl->_currentMode = previousMode;

    __CFRunLoopUnlock(rl);

    return result;

}
static int32_t __CFRunLoopRun(CFRunLoopRef rl, CFRunLoopModeRef rlm, CFTimeInterval seconds, Boolean stopAfterHandle, CFRunLoopModeRef previousMode) {

    uint64_t startTSR = 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();

    

#if USE_DISPATCH_SOURCE_FOR_TIMERS

    mach_port_name_t modeQueuePort = MACH_PORT_NULL;

    if (rlm->_queue) {

        modeQueuePort = _dispatch_runloop_root_queue_get_port_4CF(rlm->_queue);

        if (!modeQueuePort) {

            CRASH("Unable to get port for run loop mode queue (%d)", -1);

        }

    }

#endif

    

    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 = 0ULL;

    } else if (seconds <= TIMER_INTERVAL_LIMIT) {

    dispatch_queue_t queue = pthread_main_np() ? __CFDispatchQueueGetGenericMatchingMain() : __CFDispatchQueueGetGenericBackground();

    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 ns_at = (uint64_t)((__CFTSRToTimeInterval(startTSR) + seconds) * 1000000000ULL);

        dispatch_source_set_timer(timeout_timer, dispatch_time(1, ns_at), DISPATCH_TIME_FOREVER, 1000ULL);

        dispatch_resume(timeout_timer);

    } else { // infinite timeout

        seconds = 9999999999.0;

        timeout_context->termTSR = UINT64_MAX;

    }



    Boolean didDispatchPortLastTime = true;

    int32_t retVal = 0;

    do {

#if DEPLOYMENT_TARGET_MACOSX || DEPLOYMENT_TARGET_EMBEDDED || DEPLOYMENT_TARGET_EMBEDDED_MINI

        voucher_mach_msg_state_t voucherState = VOUCHER_MACH_MSG_STATE_UNCHANGED;

        voucher_t voucherCopy = NULL;

#endif

        uint8_t msg_buffer[3 * 1024];

#if DEPLOYMENT_TARGET_MACOSX || DEPLOYMENT_TARGET_EMBEDDED || DEPLOYMENT_TARGET_EMBEDDED_MINI

        mach_msg_header_t *msg = NULL;

        mach_port_t livePort = MACH_PORT_NULL;

#elif DEPLOYMENT_TARGET_WINDOWS

        HANDLE livePort = NULL;

        Boolean windowsMessageReceived = false;

#endif

    __CFPortSet waitSet = rlm->_portSet;



        __CFRunLoopUnsetIgnoreWakeUps(rl);



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

            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;



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



        CFAbsoluteTime sleepStart = poll ? 0.0 : CFAbsoluteTimeGetCurrent();



#if DEPLOYMENT_TARGET_MACOSX || DEPLOYMENT_TARGET_EMBEDDED || DEPLOYMENT_TARGET_EMBEDDED_MINI

#if USE_DISPATCH_SOURCE_FOR_TIMERS

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

    if (!poll && (rlm->_observerMask & kCFRunLoopAfterWaiting)) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopAfterWaiting);



        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

        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

        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) {

#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

        

    __CFRunLoopDoBlocks(rl, rlm);

        



    if (sourceHandledThisLoop && stopAfterHandle) {

        retVal = kCFRunLoopRunHandledSource;

        } else if (timeout_context->termTSR < 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;

    }

        

#if DEPLOYMENT_TARGET_MACOSX || DEPLOYMENT_TARGET_EMBEDDED || DEPLOYMENT_TARGET_EMBEDDED_MINI

        voucher_mach_msg_revert(voucherState);

        os_release(voucherCopy);

#endif



    } while (0 == retVal);



    if (timeout_timer) {

        dispatch_source_cancel(timeout_timer);

        dispatch_release(timeout_timer);

    } else {

        free(timeout_context);

    }



    return retVal;

}

Runloop的应用

ARC

AutoreleasePool

App启动后,苹果在主线程 RunLoop 里注册了两个 Observer,其回调都是 _wrapRunLoopWithAutoreleasePoolHandler()。

第一个 Observer 监视的事件是 Entry(即将进入Loop),其回调内会调用 _objc_autoreleasePoolPush() 创建自动释放池。其 order 是-2147483647,优先级最高,保证创建释放池发生在其他所有回调之前。

第二个 Observer 监视了两个事件: BeforeWaiting(准备进入休眠) 时调用_objc_autoreleasePoolPop() 和 _objc_autoreleasePoolPush() 释放旧的池并创建新池;Exit(即将退出Loop) 时调用 _objc_autoreleasePoolPop() 来释放自动释放池。这个 Observer 的 order 是 2147483647,优先级最低,保证其释放池子发生在其他所有回调之后。

在主线程执行的代码,通常是写在诸如事件回调、Timer回调内的。这些回调会被 RunLoop 创建好的 AutoreleasePool 环绕着,所以不会出现内存泄漏,开发者也不必显示创建 Pool 了。

UI&Interaction

事件响应

苹果注册了一个 Source1 (基于 mach port 的) 用来接收系统事件,其回调函数为 __IOHIDEventSystemClientQueueCallback()。

当一个硬件事件(触摸/锁屏/摇晃等)发生后,首先由 IOKit.framework 生成一个 IOHIDEvent 事件并由 SpringBoard 接收。这个过程的详细情况可以参考这里。SpringBoard 只接收按键(锁屏/静音等),触摸,加速,接近传感器等几种 Event,随后用 mach port 转发给需要的App进程。随后苹果注册的那个 Source1 就会触发回调,并调用 _UIApplicationHandleEventQueue() 进行应用内部的分发。

_UIApplicationHandleEventQueue() 会把 IOHIDEvent 处理并包装成 UIEvent 进行处理或分发,其中包括识别 UIGesture/处理屏幕旋转/发送给 UIWindow 等。通常事件比如 UIButton 点击、touchesBegin/Move/End/Cancel 事件都是在这个回调中完成的。

手势识别

当上面的 _UIApplicationHandleEventQueue() 识别了一个手势时,其首先会调用 Cancel 将当前的 touchesBegin/Move/End 系列回调打断。随后系统将对应的 UIGestureRecognizer 标记为待处理。

苹果注册了一个 Observer 监测 BeforeWaiting (Loop即将进入休眠) 事件,这个Observer的回调函数是 _UIGestureRecognizerUpdateObserver(),其内部会获取所有刚被标记为待处理的 GestureRecognizer,并执行GestureRecognizer的回调。

当有 UIGestureRecognizer 的变化(创建/销毁/状态改变)时,这个回调都会进行相应处理。

界面更新

当在操作 UI 时,比如改变了 Frame、更新了 UIView/CALayer 的层次时,或者手动调用了 UIView/CALayer 的 setNeedsLayout/setNeedsDisplay方法后,这个 UIView/CALayer 就被标记为待处理,并被提交到一个全局的容器去。

苹果注册了一个 Observer 监听 BeforeWaiting(即将进入休眠) 和 Exit (即将退出Loop) 事件,回调去执行一个很长的函数:

_ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv()。这个函数里会遍历所有待处理的 UIView/CAlayer 以执行实际的绘制和调整,并更新 UI 界面。

这个函数内部的调用栈大概是这样的:

_ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv()

    QuartzCore:CA::Transaction::observer_callback:

        CA::Transaction::commit();

            CA::Context::commit_transaction();

                CA::Layer::layout_and_display_if_needed();

                    CA::Layer::layout_if_needed();

                        [CALayer layoutSublayers];

                            [UIView layoutSubviews];

                    CA::Layer::display_if_needed();

                        [CALayer display];

                            [UIView drawRect];

Scheduling

Timer

NSTimer 其实就是 CFRunLoopTimerRef,他们之间是 toll-free bridged 的。一个 NSTimer 注册到 RunLoop 后,RunLoop 会为其重复的时间点注册好事件。例如 10:00, 10:10, 10:20 这几个时间点。RunLoop为了节省资源,并不会在非常准确的时间点回调这个Timer。Timer 有个属性叫做 Tolerance (宽容度),标示了当时间点到后,容许有多少最大误差。

如果某个时间点被错过了,例如执行了一个很长的任务,则那个时间点的回调也会跳过去,不会延后执行。就比如等公交,如果 10:10 时我忙着玩手机错过了那个点的公交,那我只能等 10:20 这一趟了。

CADisplayLink 是一个和屏幕刷新率一致的定时器(但实际实现原理更复杂,和 NSTimer 并不一样,其内部实际是操作了一个 Source)。如果在两次屏幕刷新之间执行了一个长任务,那其中就会有一帧被跳过去(和 NSTimer 相似),造成界面卡顿的感觉。在快速滑动TableView时,即使一帧的卡顿也会让用户有所察觉。Facebook 开源的 AsyncDisplayLink 就是为了解决界面卡顿的问题,其内部也用到了 RunLoop,这个稍后我会再单独写一页博客来分析。

PerformSelecter

当调用 NSObject 的 performSelecter:afterDelay: 后,实际上其内部会创建一个 Timer 并添加到当前线程的 RunLoop 中。所以如果当前线程没有 RunLoop,则这个方法会失效。

当调用 performSelector:onThread: 时,实际上其会创建一个 Timer 加到对应的线程去,同样的,如果对应线程没有 RunLoop 该方法也会失效。

与GCD

实际上 RunLoop 底层也会用到 GCD 的东西,比如 RunLoop 是用 dispatch_source_t 实现的 Timer(评论中有人提醒,NSTimer 是用了 XNU 内核的 mk_timer,我也仔细调试了一下,发现 NSTimer 确实是由 mk_timer 驱动,而非 GCD 驱动的)。但同时 GCD 提供的某些接口也用到了 RunLoop, 例如 dispatch_async()。

当调用 dispatch_async(dispatch_get_main_queue(), block) 时,libDispatch 会向主线程的 RunLoop 发送消息,RunLoop会被唤醒,并从消息中取得这个 block,并在回调 CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE() 里执行这个 block。但这个逻辑仅限于 dispatch 到主线程,dispatch 到其他线程仍然是由 libDispatch 处理的。

引用

blog.ibireme.com/2015/05/18/…

developer.apple.com/documentati…