底層原理探究(二)RunLoop

weixin_34377065發表於2018-06-05

轉自: 老司機出品——原始碼解析之RunLoop詳解
入門使用: RunLoop入門 看我就夠了
孫源的Runloop視訊筆記

以下2篇還沒有看
擴充:
深入理解RunLoop
iOS底層原理總結 - RunLoop

開始
NSRunLoop是基於CoreFoundation框架中的CFRunLoop進行的一層簡單的封裝,所以我們這裡著重介紹CFRunLoop
1.runLoop的組成

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;
    uint32_t _winthread;
    CFMutableSetRef _commonModes;
    CFMutableSetRef _commonModeItems;
    CFRunLoopModeRef _currentMode;
    CFMutableSetRef _modes;
    struct _block_item *_blocks_head;
    struct _block_item *_blocks_tail;
    CFTypeRef _counterpart;
};

CFRunLoop是這麼一個結構體。
_lock 結構體用來保證執行緒安全的鎖 ,
_wakeUpPort 用來喚醒runLoop的埠
_pthread 執行緒物件 ,
_modes 一個模式集合
以及一些其他輔助的屬性。

1.1 _pthread
runLoop與執行緒是一一對應的。也就是一個runLoop對應著一個執行緒,一個執行緒對應著一個runLoop。
我們從runLoop的建構函式和獲取函式即可看出:

static CFRunLoopRef __CFRunLoopCreate(pthread_t t) {
    CFRunLoopRef loop = NULL;
    CFRunLoopModeRef rlm;
    uint32_t size = sizeof(struct __CFRunLoop) - sizeof(CFRuntimeBase);
    loop = (CFRunLoopRef)_CFRuntimeCreateInstance(kCFAllocatorSystemDefault, __kCFRunLoopTypeID, size, NULL);
    if (NULL == loop) {
        return NULL;
    }
    (void)__CFRunLoopPushPerRunData(loop);
    __CFRunLoopLockInit(&loop->_lock);
    loop->_wakeUpPort = __CFPortAllocate();
    if (CFPORT_NULL == loop->_wakeUpPort) HALT;
    __CFRunLoopSetIgnoreWakeUps(loop);
    loop->_commonModes = CFSetCreateMutable(kCFAllocatorSystemDefault, 0, &kCFTypeSetCallBacks);
    CFSetAddValue(loop->_commonModes, kCFRunLoopDefaultMode);
    loop->_commonModeItems = NULL;
    loop->_currentMode = NULL;
    loop->_modes = CFSetCreateMutable(kCFAllocatorSystemDefault, 0, &kCFTypeSetCallBacks);
    loop->_blocks_head = NULL;
    loop->_blocks_tail = NULL;
    loop->_counterpart = NULL;
    loop->_pthread = t;
#if DEPLOYMENT_TARGET_WINDOWS
    loop->_winthread = GetCurrentThreadId();
#else
    loop->_winthread = 0;
#endif
    rlm = __CFRunLoopFindMode(loop, kCFRunLoopDefaultMode, true);
    if (NULL != rlm) __CFRunLoopModeUnlock(rlm);
    return loop;
}

可以看出構造一個runLoop物件僅需要一個pthread_t執行緒即可。即一個runLoop對應一個執行緒。

CF_EXPORT CFRunLoopRef _CFRunLoopGet0(pthread_t t) {
    if (pthread_equal(t, kNilPthreadT)) {
        //如果傳入執行緒為空指標則預設取主執行緒對應的runLoop
        t = pthread_main_thread_np();
    }
    __CFSpinLock(&loopsLock);
    if (!__CFRunLoops) {
        //__CFRunLoops就是一個全域性字典,以下程式碼為如果全域性字典不存在則建立全域性字典,並將主執行緒對應的mainLoop存入字典中
        __CFSpinUnlock(&loopsLock);
        CFMutableDictionaryRef dict = CFDictionaryCreateMutable(kCFAllocatorSystemDefault, 0, NULL, &kCFTypeDictionaryValueCallBacks);
        CFRunLoopRef mainLoop = __CFRunLoopCreate(pthread_main_thread_np());
        CFDictionarySetValue(dict, pthreadPointer(pthread_main_thread_np()), mainLoop);
        if (!OSAtomicCompareAndSwapPtrBarrier(NULL, dict, (void * volatile *)&__CFRunLoops)) {
            CFRelease(dict);
        }
        CFRelease(mainLoop);
        __CFSpinLock(&loopsLock);
    }
    CFRunLoopRef loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
    //從全域性字典中,取出對應執行緒的runLoop
    __CFSpinUnlock(&loopsLock);
    if (!loop) {
        //若對應執行緒的runLoop為空,則建立對應相乘的runLoop並儲存在全域性字典中
        CFRunLoopRef newLoop = __CFRunLoopCreate(t);
        __CFSpinLock(&loopsLock);
        loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
        if (!loop) {
            CFDictionarySetValue(__CFRunLoops, pthreadPointer(t), newLoop);
            loop = newLoop;
        }
        // don't release run loops inside the loopsLock, because CFRunLoopDeallocate may end up taking it
        __CFSpinUnlock(&loopsLock);
        CFRelease(newLoop);
    } if (pthread_equal(t, pthread_self())) {
        _CFSetTSD(__CFTSDKeyRunLoop, (void *)loop, NULL);
        if (0 == _CFGetTSD(__CFTSDKeyRunLoopCntr)) {
            _CFSetTSD(__CFTSDKeyRunLoopCntr, (void *)(PTHREAD_DESTRUCTOR_ITERATIONS-1), (void (*)(void *))__CFFinalizeRunLoop);    
        }
    }
    return loop;
}

這是runLoop的獲取函式,我們看到系統從一個全域性字典中取出runLoop,key就是一個執行緒,這足以說明runLoop與執行緒是一一對應的關係。

值得一提的是,一個執行緒最開始是沒有對應的runLoop的,是在呼叫獲取函式的時候才對應了一個runLoop的。
因為本身這個對應關係是由runLoop類管理的,而不是執行緒。

當然上述兩個為私有api,CFRunLoop真正對外暴露的只有兩個介面:

CF_EXPORT CFRunLoopRef CFRunLoopGetCurrent(void);
CF_EXPORT CFRunLoopRef CFRunLoopGetMain(void);

兩個方法的實現很簡單,只要把對應的執行緒傳入獲取函式即可:

CFRunLoopRef CFRunLoopGetMain(void) {
    CHECK_FOR_FORK();
    static CFRunLoopRef __main = NULL; // no retain needed
    if (!__main) __main = _CFRunLoopGet0(pthread_main_thread_np()); // no CAS needed
    return __main;
}

CFRunLoopRef CFRunLoopGetCurrent(void) {
    CHECK_FOR_FORK();
    CFRunLoopRef rl = (CFRunLoopRef)_CFGetTSD(__CFTSDKeyRunLoop);
    if (rl) return rl;
    return _CFRunLoopGet0(pthread_self());
}

1.2 _modes
我們看到,一個runLoop中同時還維護著一個集合,_modes。那麼這個modes是做什麼的呢?應該說,_modes才是runLoop的核心。

首先我們看一下這個_modes裡面到底都裝了些什麼?
答案是__CFRunLoopMode物件。那麼他又是什麼呢?

struct __CFRunLoopMode {
    CFRuntimeBase _base;
    pthread_mutex_t _lock; /* must have the run loop locked before locking this */
    CFStringRef _name;
    Boolean _stopped;
    char _padding[3];
    CFMutableSetRef _sources0;
    CFMutableSetRef _sources1;
    CFMutableArrayRef _observers;
    CFMutableArrayRef _timers;
    CFMutableDictionaryRef _portToV1SourceMap;
    __CFPortSet _portSet;
    CFIndex _observerMask;
#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 */
};

這裡挑出幾個重點
有用來標誌runLoopMode的標誌_name
有兩個事件源的集合_sources0、_sources1
有一組觀察者_obeserver
有一組被加入到runLoop中的_timers
還有Mode本身維護著的一個用於計時 _timerSource,_timerPort
這兩個一個是GCD時鐘、一個是核心時鐘。

下面runLoop的實現中結合程式碼講為什麼runLoopMode長這樣。
2. RunLoop程式碼實現
接下來程式碼有點長,先看一下大概流程,然後對著流程去看一下程式碼。

3867175-87b91dd0ab0d8d16.png
圖.png

RunLoop核心程式碼
300行程式碼=。= 有點長

/* rl, rlm are locked on entrance and exit */
static int32_t __CFRunLoopRun(CFRunLoopRef rl, CFRunLoopModeRef rlm, CFTimeInterval seconds, Boolean stopAfterHandle, CFRunLoopModeRef previousMode) {
    uint64_t startTSR = mach_absolute_time();//獲取當前核心時間
    
    if (__CFRunLoopIsStopped(rl)) {
        //如果當前runLoop或者runLoopMode為停止狀態的話直接返回
        __CFRunLoopUnsetStopped(rl);
        return kCFRunLoopRunStopped;
    } else if (rlm->_stopped) {
        rlm->_stopped = false;
        return kCFRunLoopRunStopped;
    }
    //判斷是否是第一次在主執行緒中啟動RunLoop,如果是且當前RunLoop為主執行緒的RunLoop,那麼就給分發一個佇列排程埠
    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
    //初始化一個GCD計時器,用於管理當前模式的超時
    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 = 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 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 {
        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);
        // 第二步,通知觀察者準備開始處理Timer源事件
        if (rlm->_observerMask & kCFRunLoopBeforeTimers) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeTimers);
        
        // 第三步,通知觀察者準備開始處理Source源事件
        if (rlm->_observerMask & kCFRunLoopBeforeSources) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeSources);
        
        //執行提交到runLoop中的block
        __CFRunLoopDoBlocks(rl, rlm);
        
        // 第四步,執行source0中的源事件
        Boolean sourceHandledThisLoop = __CFRunLoopDoSources0(rl, rlm, stopAfterHandle);
        
        //如果當前source0源事件處理完成後執行提交到runLoop中的block
        if (sourceHandledThisLoop) {
            __CFRunLoopDoBlocks(rl, rlm);
        }
        
        //標誌是否等待埠喚醒
        Boolean poll = sourceHandledThisLoop || (0ULL == timeout_context->termTSR);
        
        // 第五步,檢測埠,如果埠有事件則跳轉至handle_msg(首次執行不會進入判斷,因為didDispatchPortLastTime為true)
        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)) {
                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);
        
        // 標誌當前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);
        
        // 第七步,進入迴圈開始不斷的讀取埠資訊,如果埠有喚醒資訊則喚醒當前runLoop
#if DEPLOYMENT_TARGET_MACOSX || DEPLOYMENT_TARGET_EMBEDDED || DEPLOYMENT_TARGET_EMBEDDED_MINI
#if USE_DISPATCH_SOURCE_FOR_TIMERS
        do {
            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), &livePort, poll ? 0 : TIMEOUT_INFINITY);
            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);
            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);
#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);
        
        // 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);
        
        //標誌當前runLoop為喚醒狀態
        __CFRunLoopSetIgnoreWakeUps(rl);
        
        // user callouts now OK again
        __CFRunLoopUnsetSleeping(rl);
        
        // 第八步,通知觀察者執行緒被喚醒了
        if (!poll && (rlm->_observerMask & kCFRunLoopAfterWaiting)) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopAfterWaiting);
        
        //執行埠的事件
    handle_msg:;
        
        //設定此時runLoop忽略埠喚醒(保證執行緒安全)
        __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
        //處理有GCD提交到主執行緒喚醒的事件
        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 {
            //處理source1喚醒的事件
            CFRUNLOOP_WAKEUP_FOR_SOURCE();
            // 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;
                // 處理Source1(基於埠的源)
                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 || 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;
        }
    } while (0 == retVal);
    
    // 第十步,釋放定時器
    if (timeout_timer) {
        dispatch_source_cancel(timeout_timer);
        dispatch_release(timeout_timer);
    } else {
        free(timeout_context);
    }
    return retVal;
}

這300行的流程其實就是上面歸納的10步:

首先進入runLoop對應的Mode並開始迴圈,然後在休眠之前做了三件事:DoBlocks、DoSource0、檢測source1埠是否有訊息,如果有則跳過稍後的休眠。
然後runLoop就進入了休眠狀態,直到有埠事件喚醒runLoop,被喚醒後則處理響應的埠事件然後再次開始迴圈。直到runLoop超時或者runLoop被停止後在結束runLoop。

1.source0,source1
首先這個源事件分為兩種,一種是不基於埠的source0,一直是基於埠的source1。

source0 只包含了一個回撥(函式指標),它並不能主動觸發事件。使用時,你需要先呼叫 CFRunLoopSourceSignal(source),將這個 Source 標記為待處理,然後手動呼叫 CFRunLoopWakeUp(runloop) 來喚醒 RunLoop,讓其處理這個事件。

source0 呢主要處理App內部事件、App自己負責管理(觸發),如UIEvent、CFSocket,
說明了button的點選是屬於sourse0的。

source1 包含了一個 mach_port 和一個回撥(函式指標),被用於通過核心和其他執行緒相互傳送訊息。這種 Source 能主動喚醒 RunLoop 的執行緒,其原理在下面會講到。

source1 呢主要有Runloop和核心管理,Mach port驅動,如CFMahPort、CFMessagePort

2.NSTimer事件是藉助runLoop實現的。

在初始化Timer的時候要將Timer提交到runLoop中,並且要指定mode,才可以工作。今天我們可以深入講一下。

這個事件是怎麼執行的?並且為什麼有的時候會延遲?為什麼子執行緒中建立的Timer並不執行?

首先,在進入迴圈開始以後,就要處理source0事件,處理後檢測一下source1埠是否有訊息,如果一個Timer的時間間隔剛好到了則此處有可能會得到一個訊息,則runLoop直接跳轉至埠啟用處從而去處理Timer事件。

第二,為什麼會延遲?我們知道,兩次埠事件是在兩個runLoop迴圈中分別執行的。比如Timer的時間間隔為1秒,在第一次Timer回撥結束後,在很短時間內立即進入runLoop的下一次迴圈,這次並不是Timer回撥並且是一個計算量非常大的任務,計算時間超過了1秒,那麼runLoop的第二個迴圈就要執行很久,無法進入下一個迴圈等待有可能即將到來的Timer第二次回撥的訊號,所以Timer第二次回撥就會推遲了。

第三,為什麼在子執行緒中建立的Timer並且提交到當前runLoop中並不會執行?這還是要從runLoop的獲取函式中看,當呼叫currentRunLoop的時候會取當前執行緒對應的runLoop,而首次是取不到的,則會建立一個新的runLoop。但是!這個runLoop並沒有run。就是沒有開啟=。=

3.同一時間內,runLoop只能執行同一種mode。那commonMode是怎麼實現的?
從runLoop的結構我們可以知道,一個runLoop會包含多種runLoopMode,runLoop是不停的在這些mode之間進行切換去完成對應Mode中的相關任務。

3867175-d9eebce47c0ee903.png
runLoop中多個mode.png

首先為什麼說runLoop只能在各種Mode之間切換,同一時間只能存在一個呢?
因為上面那個方法必須要傳一個runLoopMode,然後這個方法貫穿始終,都在用。

我們看到,上面的方法中首先就要傳入一個指定的mode才能執行對應mode中的事件。那麼所謂的CommonMode是如何實現的呢?

我們看到runLoop中執行任務有調到CFRunLoopDoBlocks這麼一個函式,那麼這個函式是什麼樣的呢?

static Boolean __CFRunLoopDoBlocks(CFRunLoopRef rl, CFRunLoopModeRef rlm) { // Call with rl and rlm locked
    if (!rl->_blocks_head) return false;
    if (!rlm || !rlm->_name) return false;
    ...省略一些非重點...
    while (item) {
        struct _block_item *curr = item;
        item = item->_next;
    Boolean doit = false;
    if (CFStringGetTypeID() == CFGetTypeID(curr->_mode)) {
        doit = CFEqual(curr->_mode, curMode) || (CFEqual(curr->_mode, kCFRunLoopCommonModes) && CFSetContainsValue(commonModes, curMode));
        }else {
        doit = CFSetContainsValue((CFSetRef)curr->_mode, curMode) || (CFSetContainsValue((CFSetRef)curr->_mode, kCFRunLoopCommonModes) && CFSetContainsValue(commonModes, curMode));
        }
    if (!doit) prev = curr;
    if (doit) {
        if (prev) prev->_next = item;
        if (curr == head) head = item;
        if (curr == tail) tail = prev;
        void (^block)(void) = curr->_block;
            CFRelease(curr->_mode);
            free(curr);
        if (doit) {
            __CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__(block);
            did = true;
        }
        ...省略一些非重點...
        return did;
    }

我們看到doit這個bool變數完全決定了當前block是否執行。預設他是No的,而他被置為true的條件就是CFEqual(curr->_mode, curMode) || (CFEqual(curr->_mode, kCFRunLoopCommonModes) && CFSetContainsValue(commonModes, curMode))。就是當前mode與制定mode相等或者當前mode為commonMode(此處為一個字串)且commonMode(此處為一個集合,若有不懂,請看runLoop結構)這個集合中包含指定mode。

這是因為這個判斷的存在才允許commondMode可以在任意Mode下執行。
當然這是提交到runLoop裡的程式碼塊才會走到__CFRunLoopDoBlocks這個方法。

相同的,我們通過上述程式碼也可以知道,runLoop通過埠喚醒的事件需要通過__CFRunLoopDoSource1和__CFRunLoopDoTimers兩個方法來呼叫。__CFRunLoopDoSource1方法沒什麼說的,直接呼叫源事件runLoopSourceRef即可。重點我們看一下Timer的實現,核心程式碼如下:

static Boolean __CFRunLoopDoTimers(CFRunLoopRef rl, CFRunLoopModeRef rlm, uint64_t limitTSR) { /* DOES CALLOUT */
    Boolean timerHandled = false;
    CFMutableArrayRef timers = NULL;
    //遍歷runLoopMode維護的Timers陣列,取其中有效的timer並加入新臨時陣列
    for (CFIndex idx = 0, cnt = rlm->_timers ? CFArrayGetCount(rlm->_timers) : 0; idx < cnt; idx++) {
        CFRunLoopTimerRef rlt = (CFRunLoopTimerRef)CFArrayGetValueAtIndex(rlm->_timers, idx);
        if (__CFIsValid(rlt) && !__CFRunLoopTimerIsFiring(rlt)) {
            if (rlt->_fireTSR <= limitTSR) {
                if (!timers) timers = CFArrayCreateMutable(kCFAllocatorSystemDefault, 0, &kCFTypeArrayCallBacks);
                CFArrayAppendValue(timers, rlt);
            }
        }
    }
    //遍歷臨時陣列,每個有效Timer呼叫__CFRunLoopDoTimer
    for (CFIndex idx = 0, cnt = timers ? CFArrayGetCount(timers) : 0; idx < cnt; idx++) {
        CFRunLoopTimerRef rlt = (CFRunLoopTimerRef)CFArrayGetValueAtIndex(timers, idx);
        Boolean did = __CFRunLoopDoTimer(rl, rlm, rlt);
        timerHandled = timerHandled || did;
    }
    if (timers) CFRelease(timers);
    return timerHandled;
}

我們可以看到,此處Timer是否會回撥完全取決於對應Mode的_Timers陣列。那麼當我們將Timer加入到commonModes中的時候一定是同時將Timer加入到了commonModes所包含的其他Mode中了,我們看下程式碼:

void CFRunLoopAddTimer(CFRunLoopRef rl, CFRunLoopTimerRef rlt, CFStringRef modeName) {
    CHECK_FOR_FORK();
    if (__CFRunLoopIsDeallocating(rl)) return;
    if (!__CFIsValid(rlt) || (NULL != rlt->_runLoop && rlt->_runLoop != rl)) return;
    __CFRunLoopLock(rl);
    if (modeName == kCFRunLoopCommonModes) {//commonModes分支
        //取到commonModes所代表的Mode的集合
        CFSetRef set = rl->_commonModes ? CFSetCreateCopy(kCFAllocatorSystemDefault, rl->_commonModes) : NULL;
        if (NULL == rl->_commonModeItems) {
            //將commonModeItems中加入當前定時器
            rl->_commonModeItems = CFSetCreateMutable(kCFAllocatorSystemDefault, 0, &kCFTypeSetCallBacks);
        }
        CFSetAddValue(rl->_commonModeItems, rlt);
        if (NULL != set) {
            CFTypeRef context[2] = {rl, rlt};
            /* add new item to all common-modes */
            //最主要還是還是這句,這句的作用是集合中的所有物件均呼叫__CFRunLoopAddItemToCommonModes這個方法。
            CFSetApplyFunction(set, (__CFRunLoopAddItemToCommonModes), (void *)context);
            CFRelease(set);
        }
    } else {//非commonModes的分支
        CFRunLoopModeRef rlm = __CFRunLoopFindMode(rl, modeName, true);
        if (NULL != rlm) {
            if (NULL == rlm->_timers) {
                CFArrayCallBacks cb = kCFTypeArrayCallBacks;
                cb.equal = NULL;
                rlm->_timers = CFArrayCreateMutable(kCFAllocatorSystemDefault, 0, &cb);
            }
        }
        if (NULL != rlm && !CFSetContainsValue(rlt->_rlModes, rlm->_name)) {
            __CFRunLoopTimerLock(rlt);
            if (NULL == rlt->_runLoop) {
                rlt->_runLoop = rl;
            } else if (rl != rlt->_runLoop) {
                __CFRunLoopTimerUnlock(rlt);
                __CFRunLoopModeUnlock(rlm);
                __CFRunLoopUnlock(rl);
                return;
            }
            CFSetAddValue(rlt->_rlModes, rlm->_name);
            __CFRunLoopTimerUnlock(rlt);
            __CFRunLoopTimerFireTSRLock();
            __CFRepositionTimerInMode(rlm, rlt, false);
            __CFRunLoopTimerFireTSRUnlock();
            if (!_CFExecutableLinkedOnOrAfter(CFSystemVersionLion)) {
                // Normally we don't do this on behalf of clients, but for
                // backwards compatibility due to the change in timer handling...
                if (rl != CFRunLoopGetCurrent()) CFRunLoopWakeUp(rl);
            }
        }
        if (NULL != rlm) {
            __CFRunLoopModeUnlock(rlm);
        }
    }
    __CFRunLoopUnlock(rl);
}

static void __CFRunLoopAddItemToCommonModes(const void *value, void *ctx) {
    CFStringRef modeName = (CFStringRef)value;
    CFRunLoopRef rl = (CFRunLoopRef)(((CFTypeRef *)ctx)[0]);
    CFTypeRef item = (CFTypeRef)(((CFTypeRef *)ctx)[1]);
    if (CFGetTypeID(item) == __kCFRunLoopSourceTypeID) {
        CFRunLoopAddSource(rl, (CFRunLoopSourceRef)item, modeName);
    } else if (CFGetTypeID(item) == __kCFRunLoopObserverTypeID) {
        CFRunLoopAddObserver(rl, (CFRunLoopObserverRef)item, modeName);
    } else if (CFGetTypeID(item) == __kCFRunLoopTimerTypeID) {
        CFRunLoopAddTimer(rl, (CFRunLoopTimerRef)item, modeName);
    }
}

我們可以看到,當加入到commonModes中時,實際上系統是找出commonModes代表的所有Mode,如defaultMode和trackingMode,讓後分別將其加入了這些mode中。
同樣的方法還有CFRunLoopAddSource/CFRunLoopAddObserver都是同樣的道理。

所以說當scrollView或其子類進行滾動的時候,UIKIT會自動將當前runLoopMode切換為UITrackingRunLoopMode,所以你加在defaultMode中的計時器當然不會走了。

4.runLoop是如何休眠有如何被喚醒的?

從第7步開始,我們看到runLoop進入了休眠狀態。然而所謂的休眠狀態指示將當前runLoop標記為休眠之後,進入了一個while死迴圈。然後在迴圈內就不斷的去讀取埠訊息。如果說從埠中讀取到一個喚醒資訊的話,break掉while迴圈從而進入喚醒狀態。

5.可以喚醒runLoop的都有哪些事件?
從原始碼中我們可以看出,所謂的runLoop進入休眠狀態不過是一個while迴圈,如下:

do {
    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), &livePort, poll ? 0 : TIMEOUT_INFINITY);
    
    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);

相應的我們還得看一個函式,__CFRunLoopServiceMachPort:

static Boolean __CFRunLoopServiceMachPort(mach_port_name_t port, mach_msg_header_t**buffer, size_t buffer_size, mach_port_t *livePort, mach_msg_timeout_t timeout) {
    Boolean originalBuffer = true;
    kern_return_t ret = KERN_SUCCESS;
    for (;;) { /* In that sleep of death what nightmares may come ... */
        mach_msg_header_t *msg = (mach_msg_header_t *)*buffer;
        msg->msgh_bits = 0;
        msg->msgh_local_port = port;
        msg->msgh_remote_port = MACH_PORT_NULL;
        msg->msgh_size = buffer_size;
        msg->msgh_id = 0;
        if (TIMEOUT_INFINITY == timeout) { CFRUNLOOP_SLEEP(); } else { CFRUNLOOP_POLL(); }
        ret = mach_msg(msg, MACH_RCV_MSG|MACH_RCV_LARGE|((TIMEOUT_INFINITY != timeout) ? MACH_RCV_TIMEOUT : 0)|MACH_RCV_TRAILER_TYPE(MACH_MSG_TRAILER_FORMAT_0)|MACH_RCV_TRAILER_ELEMENTS(MACH_RCV_TRAILER_AV), 0, msg->msgh_size, port, timeout, MACH_PORT_NULL);
        CFRUNLOOP_WAKEUP(ret);
        if (MACH_MSG_SUCCESS == ret) {
            *livePort = msg ? msg->msgh_local_port : MACH_PORT_NULL;
            return true;
        }
        if (MACH_RCV_TIMED_OUT == ret) {
            if (!originalBuffer) free(msg);
            *buffer = NULL;
            *livePort = MACH_PORT_NULL;
            return false;
        } if (MACH_RCV_TOO_LARGE != ret) break;
        buffer_size = round_msg(msg->msgh_size + MAX_TRAILER_SIZE);
        if (originalBuffer) *buffer = NULL;
        originalBuffer = false;
        *buffer = realloc(*buffer, buffer_size);
    }
    HALT;
    return false;
}

我們先看後面這個函式,在這裡僅兩種情況會對livePort進行賦值
一種是成功獲取到訊息後,會根據情況賦值為msg->msgh_local_port或者MACH_PORT_NULL,
另一種獲取訊息超時的情況會賦值為MACH_PORT_NULL。首先請先記住這兩個結論。

然後我們把目光聚焦到while迴圈中,在呼叫__CFRunLoopServiceMachPort後如果livePort變成了modeQueuePort(livePort初值為MACH_PORT_NULL),則代表為當前佇列的檢測埠,那麼在_dispatch_runloop_root_queue_perform_4CF的條件下再次進入二級迴圈,知道Timer被啟用了才跳出二級迴圈繼續迴圈一級迴圈。

那麼如果livePort不為modeQueuePort時我們的runLoop被喚醒。這代表__CFRunLoopServiceMachPort給出的livePort只有兩種可能:
一種情況為MACH_PORT_NULL,另一種為真正獲取的訊息的埠。

所以我們可以看到後面runLoop處理埠時間的方法如下的判斷:

if (MACH_PORT_NULL == livePort) {//什麼都不做,有肯能是超時之類的或者是資訊過大
    CFRUNLOOP_WAKEUP_FOR_NOTHING();
    // handle nothing
} else if (livePort == rl->_wakeUpPort) {//只有外界呼叫CFRunLoopWakeUp才會進入此分支,這是外部主動喚醒runLoop的介面
    CFRUNLOOP_WAKEUP_FOR_WAKEUP();
    // do nothing on Mac OS
}
#if USE_DISPATCH_SOURCE_FOR_TIMERS
else if (modeQueuePort != MACH_PORT_NULL && livePort == modeQueuePort) {
    //這裡不是從runLoop休眠後喚醒到這裡的,而是在runLoop10步中的第五步跳轉過來的,是處理計時器事件
    CFRUNLOOP_WAKEUP_FOR_TIMER();
    ...省略處理計時器事件的程式碼...
}
#endif
else if (livePort == dispatchPort) {//這裡是處理GCD提交到mainQueue的block的埠事件
    CFRUNLOOP_WAKEUP_FOR_DISPATCH();
    ...省略處理GCD的程式碼...
} else {//之前所有情況都不是,那麼喚醒runLoop的就只可能是source1的源事件了。
    CFRUNLOOP_WAKEUP_FOR_SOURCE();
    ...省略處理source1源事件的程式碼...
    }
}

runLoop的喚醒過程,及喚醒過後的時間處理就是上面的流程,
大家可以看看每個分支後的註釋。同時runLoopRun的核心程式碼也就解讀完畢了。

剩下的幾個run方法事實上都是對這個核心方法的封裝了

CFRunLoopRunSpecific
CFRunLoopRun
CFRunLoopRunInMode

至此,整個runLoop中的核心流程分析了一遍~

runLoop都能做什麼 說了這麼多,那麼runLoop都能做些什麼呢?

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 了。

CAAnimation
我們知道CAAniamtion為我們提供的是補間動畫,開發者只要給出始末狀態後中間狀態有系統自動生成。那麼動畫是怎麼出現的呢,是開發者給出始末狀態後,系統計算出每一箇中間態的各項引數,然後啟一個定時器不斷去回撥並改變屬性。

事件響應
蘋果註冊了一個 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 的變化(建立/銷燬/狀態改變)時,這個回撥都會進行相應處理。

定時器
PerformSelecter
當呼叫 NSObject 的 performSelecter:afterDelay: 後,實際上其內部會建立一個 Timer 並新增到當前執行緒的 RunLoop 中。所以如果當前執行緒沒有 RunLoop,則這個方法會失效。

當呼叫 performSelector:onThread: 時,實際上其會建立一個 Timer 加到對應的執行緒去,同樣的,如果對應執行緒沒有 RunLoop 該方法也會失效。

完。

相關文章