老司機出品——源碼解析之RunLoop詳解

RunLoop詳解

不得不說,人的惰性是真可怕啊。
從上周六就到寫runLoop的建議開始,星期三告訴自己從星期四開始著手寫這篇博客。然而現在戳個時間戳,現在是4.30星期日。寫完發出去又不知道是什么時候啦,哈哈哈??

懶癌

這一期講什么呢?這一期講runLoop喲。一直以來,runLoop這個玄而又玄的東西似乎被當做了公司面試挑人的終極話題,原因不難想,日常開發用到runLoop的地方少之又少,沒有時間的積累這方面的知識應該還是相對較于匱乏的,所以runLoop的了解側面也能發應開發者的開發經驗,當然就被當做甄選人才的最后殺器。你可能一臉憤怒的說平時有用不到,我不會也不影響開發啊!的確,用不到,但這只是一個過濾器而已。但是蛋疼的是,在于國內的環境下,runLoop的相關資料又是少之又少,開發者又難以有一個深入的了解

出于以上原因,老司機今天就以老司機個人的角度,盡可能將老司機所了解到的runLoop知識。

在今天的文章中你可能會看到以下內容:

  • runLoop相關知識

runLoop是什么

直譯以下,跑圈。翻譯以下,事件循環吧。
為什么要有這個事件循環呢?我們知道,任何程序如果執行到程序的最后一句之后都會結束運行。然而對于我們要的手機應用程序而言,他顯然不可以執行一個事件后就結束運行,他應該具有持續接受事件的能力從而不斷地處理事件。所以最基本的思路就是用于個while循環讓程序不能走到最后一句結束,而是在循環體內不斷的接受事件。所以我們需要runLoop。不過值得注意的是,runLoop并不是iOS獨有的概念,因為準去的來說runLoop應該是一個模式,在其他平臺同樣存在這種模式,不過叫不叫runLoop我就不知道了。

循環

runLoop是如何實現的

首先要明確的一點事,在平時我們使用的是Foundation框架的NSRunLoop類去做一些實現,而其實NSRunLoop是基于CoreFoundation框架中的CFRunLoop進行的一層簡單的封裝。所以我們這里著重介紹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,有用來喚醒runLoop的端口_wakeUpPort(這里后面會說到,不用執著),有線程對象_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,CF真正對外暴露的只有兩個接口:

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時鐘一個是內核時鐘。

至于runLoopMode為什么長這樣,老司機會在下面runLoopRun的實現中結合代碼講到。


2.runLoop代碼實現

恩,接下來代碼有點長,先給你們看一下大概流程,然后對著流程去看一下代碼。

圖是我盜的

前方高能預警,代碼很多!

runLoop核心代碼

/* 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行代碼=。=

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

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

不過好在代碼很全,在這里我們能出到很多問題。

1.source0,source1

首先這個源事件分為兩種,一種是不基于端口的source0,一直是基于端口的source1。

Source0 只包含了一個回調(函數指針),它并不能主動觸發事件。使用時,你需要先調用 CFRunLoopSourceSignal(source),將這個 Source 標記為待處理,然后手動調用 CFRunLoopWakeUp(runloop) 來喚醒 RunLoop,讓其處理這個事件。

Source1 包含了一個 mach_port 和一個回調(函數指針),被用于通過內核和其他線程相互發送消息。這種 Source 能主動喚醒 RunLoop 的線程,其原理在下面會講到。

————引自深入理解RunLoop


source0呢主要處理App內部事件、App自己負責管理(觸發),如UIEvent、CFSocket

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

————引自孫源runLoop線下分享會視頻

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

這點老司機早在CoreAnimation系列中第三篇介紹三個Timer的時候老司機就有提到過,在初始化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中的相關任務。

runLoop中多個mode

首先為什么說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循環從而進入喚醒狀態

關于runLoop的幾種mode老司機之前也有講過,在CoreAnimation中的第三篇中有講到,這里就只羅列一下。

  • NSDefaultRunLoopMode
  • NSConnectionReplyMode
  • NSModalPanelRunLoopMode
  • UITrackingRunLoopMode
  • NSRunLoopCommonModes

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都能做些什么呢?

以下內容整理自深入理解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 該方法也會失效。

基本也就差不多了。


老司機寫這篇博客呢,也是應盆友的要求寫的,基本上也是針對前人博客的總結以及自己對源碼的解讀畢竟有了源碼以后runLoop也就沒有那么神秘了。只是希望大家明白runLoop并不是什么多么可怕的東西,只要我們一點一點去看,他也是人寫的代碼啊=。=不過老司機懶到連偽代碼都沒給你們寫直接上的源碼。原諒一個懶癌晚期的人吧

還是要感謝兩位大神郭耀源孫源兩位大神之前的博客和視頻講解讓我很受用。大神名里都帶源字,我要不要改成老源=。=


參考資料:

另外你如果想看孫源的視頻,老司機已經給了下載鏈接,然后從最開始到1小時10分鐘的時候都是干貨,后面35分鐘偏討論,時間緊的童靴可以跳過,但是后面也有很好的思路分享,聽聽也是不錯的。

如果你想看runLoop源碼,因為源碼里面有4000多行,老司機讀源碼的時候講有用的方法的行數都記了下來,你可以對應的找一下:

方法名 行數
__CFRunLoopFindMode #754
__CFRunLoopCreate #1321
_CFRunLoopGet0 #1358
__CFRunLoopAddItemToCommonModes #1536
__CFRunLoopDoObservers #1668
__CFRunLoopDoSources0 #1764
__CFRunLoopDoSource1 #1829
__CFRepositionTimerInMode #1999
__CFRunLoopDoTimer #2024
__CFRunLoopDoTimers #2152
__CFRunLoopServiceMachPort #2196
__CFRunLoopRun #2308
CFRunLoopRunSpecific #2601
CFRunLoopRun #2628
CFRunLoopRunInMode #2636
CFRunLoopWakeUp #2645
CFRunLoopAddSource #2791
CFRunLoopAddObserver #2978
CFRunLoopAddTimer #3081

打完收功!


打完收功

老司機寫這篇博客真的事廢了很多心血的,好就給贊關注吧么么噠??

無恥的廣告時間:

DWCoreTextLabel支持cocoaPods了~
pod search DWCoreTextLabel

DWCoreTextLabel

插入圖片、繪制圖片、添加事件統統一句話實現~

一句話實現

盡可能保持系統Label屬性讓你可以無縫過渡使用~

無縫過渡

恩,說了這么多,老司機放一下地址:DWCoreTextLabel,寶寶們給個star吧愛你喲

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,732評論 6 539
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,214評論 3 426
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 177,781評論 0 382
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,588評論 1 316
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,315評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,699評論 1 327
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,698評論 3 446
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,882評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,441評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,189評論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,388評論 1 372
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,933評論 5 363
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,613評論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,023評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,310評論 1 293
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,112評論 3 398
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,334評論 2 377

推薦閱讀更多精彩內容