重拾RunLoop原理

博客鏈接重拾RunLoop原理

更新于2019.07.26

雖然自己很早前就看過RunLoop的源碼暖途,當時看得時候蠢护,有點地方還是比較生澀的伞访。所有抽了個時間裳瘪,重新整理了一下之前RunLoop的筆記百炬。CoreFoundation源代碼關于RunLoop的源碼主要集中在CFRunLoop.c文件中穿香。

RunLoop的獲取

蘋果并不允許我們直接創(chuàng)建RunLoop国觉,RunLoop的創(chuàng)建在第一次獲取的時候嚼松,使用[NSRunLoop mainRunLoop]CFRunLoopGetMain()可以獲取主線程的RunLoop猾普;通過[NSRunLoop currentRunLoop]CFRunLoopGetCurrent()獲取當前線程的RunLoop袜炕。

它們之間的關系是Foundation中的RunLoop是對Core Foundation中的包裝〕跫遥可以通過執(zhí)行NSLog(@"%@, %p", [NSRunLoop mainRunLoop], CFRunLoopGetMain());得出偎窘,這里就不貼實驗結果了。

接著看一下RunLoop在CFRunLoop.c中的定義:

// 主線程的RunLoop
CFRunLoopRef CFRunLoopGetMain(void) {
    CHECK_FOR_FORK(); // 判斷是否需要fork進程
    static CFRunLoopRef __main = NULL; // no retain needed
    if (!__main) __main = _CFRunLoopGet0(pthread_main_thread_np()); // no CAS needed
    return __main;
}

// 當前線程的RunLoop
CFRunLoopRef CFRunLoopGetCurrent(void) {
    CHECK_FOR_FORK();
    // 先從TSD中查找有沒有相關的runloop信息溜在,有則返回陌知。
    // 我們可以理解為runloop不光存在與全局字典中,也存在中TSD中炕泳。
    CFRunLoopRef rl = (CFRunLoopRef)_CFGetTSD(__CFTSDKeyRunLoop);
    if (rl) return rl;
    return _CFRunLoopGet0(pthread_self());
}

CHECK_FOR_FORK用來判斷是否需要fork進程纵诞,這里我們可以暫時不管。

在獲取主線程RunLoop的時候培遵,它使用了static CFRunLoopRef __main進行保存浙芙,當第二次調用CFRunLoopGetMain()__main是有值的籽腕,就不會再重新創(chuàng)建嗡呼,否則就使用_CFRunLoopGet0進行創(chuàng)建,傳入的是pthread_main_thread_np()即主線程皇耗。

在獲取當前線程的RunLoop的時候南窗,首頁會通過_CFGetTSD獲取RunLoop,如果沒有再通過_CFRunLoopGet0郎楼,傳入的是當前的線程万伤。

這里介紹一下Thread-specific dataThread-specific data是線程私有數據就是上面的TSD呜袁,顧名思義就是存一些特定的數據的敌买,RunLoop會保存在線程的私有數據里。

// __CFTSDTable
typedef struct __CFTSDTable {
    uint32_t destructorCount;
    uintptr_t data[CF_TSD_MAX_SLOTS];
    tsdDestructor destructors[CF_TSD_MAX_SLOTS];
} __CFTSDTable;

// _CFGetTSD
CF_EXPORT void *_CFGetTSD(uint32_t slot) {
    __CFTSDTable *table = __CFTSDGetTable();
    if (!table) { return NULL; }
    uintptr_t *slots = (uintptr_t *)(table->data);
    return (void *)slots[slot];
}

// _CFSetTSD
CF_EXPORT void *_CFSetTSD(uint32_t slot, void *newVal, tsdDestructor destructor) {
    __CFTSDTable *table = __CFTSDGetTable();
    if (!table) { return NULL; }

    void *oldVal = (void *)table->data[slot];
    table->data[slot] = (uintptr_t)newVal;
    table->destructors[slot] = destructor;
    
    return oldVal;
}

__CFTSDTabledata數組用來保存私有數據阶界,destructors數組用來保存析構函數虹钮,destructorCount用來記錄析構函數的個數聋庵。

_CFGetTSD的作用就是獲取__CFTSDTabledata數據,并返回slot對應的值芙粱。

_CFSetTSD的作用就是給__CFTSDTable里設置data[slot]destructors[slot]位置的值祭玉。

RunLoop與線程之間的關系

要想知道RunLoop與線程之間的關系,就需要看一下_CFRunLoopGet0函數春畔。

CF_EXPORT CFRunLoopRef _CFRunLoopGet0(pthread_t t) {
    // 當前線程為0脱货,則取主線程
    if (pthread_equal(t, kNilPthreadT)) {
        t = pthread_main_thread_np();
    }
    __CFLock(&loopsLock);
    // __CFRunLoops是一個全局的靜態(tài)字典。
    // 如果該字典為空拐迁,就進行以下操作:
    // 1.創(chuàng)建一個臨時字典蹭劈;
    // 2.創(chuàng)建主線程的RunLoop,并將它存到臨時字典里
    // 3.OSAtomicCompareAndSwapPtrBarrier用來將這個臨時字典復制到全局字典里线召;
    // 并且使用了鎖機制確保上述操作的安全性。
    if (!__CFRunLoops) {
        __CFUnlock(&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);
        __CFLock(&loopsLock);
    }
    // 當前線程RunLoop的獲取多矮,獲取不到就使用__CFRunLoopCreate創(chuàng)建一個RunLoop缓淹,并保存在全局字典里
    CFRunLoopRef loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
    __CFUnlock(&loopsLock);
    if (!loop) {
        CFRunLoopRef newLoop = __CFRunLoopCreate(t);
        __CFLock(&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
        __CFUnlock(&loopsLock);
        CFRelease(newLoop);
    }
    if (pthread_equal(t, pthread_self())) {
        // t為當前線程的話,將loop保存在線程私有數據中
        _CFSetTSD(__CFTSDKeyRunLoop, (void *)loop, NULL);
        // __CFFinalizeRunLoop是RunLoop的析構函數塔逃,
        // PTHREAD_DESTRUCTOR_ITERATIONS 表示是線程退出時銷毀線程私有數據的最大次數
        // 這也是RunLoop的釋放時機--線程退出的時候
        if (0 == _CFGetTSD(__CFTSDKeyRunLoopCntr)) {
            // 注冊一個回調讯壶,當線程銷毀時,順便也銷毀其對應的RunLoop
            _CFSetTSD(__CFTSDKeyRunLoopCntr, (void *)(PTHREAD_DESTRUCTOR_ITERATIONS-1), (void (*)(void *))__CFFinalizeRunLoop);
        }
    }
    return loop;
}

通過源代碼我們可以知道:

  1. RunLoop和線程之間是一一對應的湾盗,它們之間的關系保存在一個全局字典以及線程私有數據中伏蚊;
  2. 全局字典以線程為Key,RunLoop對象為Value的形式保存RunLoop和線程之間的映射關系格粪;
  3. 在線程創(chuàng)建的時候躏吊,是沒有對應的RunLoop,它的創(chuàng)建是在第一次獲取的時候帐萎,它的銷毀則發(fā)生在線程銷毀的時候比伏。

之前在看源碼的時候有兩個地方不是很理解:

1.為什么上面的loop要再取一次

后來在《程序員的自我修養(yǎng)》第29頁中得到啟發(fā)。里面關于單例有這樣一段代碼:

volatile T* pInst = 0;
T* GetInstance()
{
    if(pInst == NULL)
    {
        lock();
        if(pInst == NULL)
            pInst = new T;
        unlock();
    }
    return pInst;
}

書上只說明雙重if在這里可以讓lock的調用開銷降到最低疆导。為什么有這個效果赁项,這里做一下說明。

在不考慮CPU亂序的情況下澈段,假設有兩個線程A悠菜、B同時訪問GetInstance(),A和B同時執(zhí)行第一個判斷語句败富,結果一樣悔醋,都進入了代碼塊。lock()的設定就是只允許一個線程進入囤耳,假設A先進入篙顺,B在等待偶芍。A進入后首先判斷pInstNULL,那么new一個對象德玫,然后解鎖返回對象匪蟀。喚醒B,這是B進入發(fā)現第二個判斷通過不了(因為pInst已經有值了)宰僧,這樣的話B就直接解鎖返回對象材彪。假設只有最外層的判斷的話,那么B也會創(chuàng)建一個對象琴儿。

我想這里應該也是類似的作用吧段化。

2.RunLoop銷毀的時機

上面的源代碼只說明了這個會在RunLoop的析構函數是__CFFinalizeRunLoop,但是具體的釋放時機會在后面說明造成。

RunLoop的創(chuàng)建

_CFRunLoopGet0函數的實現中可以知道显熏,RunLoop的創(chuàng)建是通過調用使用__CFRunLoopCreate返回一個CFRunLoopRef的實例,這個函數大致分為兩步:

  1. 使用_CFRuntimeCreateInstance創(chuàng)建一個CFRunLoopRef實例晒屎,其實現為CFRuntime.c文件喘蟆;
  2. CFRunLoopRef進行初始化配置,包括調用__CFRunLoopFindMode(loop, kCFRunLoopDefaultMode, true);鼓鲁。

另外在__CFRunLoopFindMode里講到了RunLoop的定時器蕴轨,用宏進行了判斷

#if DEPLOYMENT_TARGET_MACOSX
#define USE_DISPATCH_SOURCE_FOR_TIMERS 1
#define USE_MK_TIMER_TOO 1
#else
#define USE_DISPATCH_SOURCE_FOR_TIMERS 0
#define USE_MK_TIMER_TOO 1
#endif

MACOSX下,RunLoop會使用GCD TimerMK_TIMER來做定時器骇吭,在非MACOSX下橙弱,使用MK_TIMER作為定時器。

RunLoop的釋放

我們知道RunLoop的釋放是發(fā)生在線程銷毀的時候燥狰。

__CFTSDGetTable()函數的實現中有這樣的一句代碼:

pthread_key_init_np(CF_TSD_KEY, __CFTSDFinalize);

通過CF_TSD_KEY棘脐,指定了對應的析構函數__CFTSDFinalize是一個析構函數。

__CFTSDFinalize的實現如下:

static void __CFTSDFinalize(void *arg) {
    __CFTSDSetSpecific(arg);

    if (!arg || arg == CF_TSD_BAD_PTR) {
        return;
    }
    
    __CFTSDTable *table = (__CFTSDTable *)arg;
    table->destructorCount++;
        
    for (int32_t i = 0; i < CF_TSD_MAX_SLOTS; i++) {
        if (table->data[i] && table->destructors[i]) {
            uintptr_t old = table->data[i];
            table->data[i] = (uintptr_t)NULL;
            table->destructors[i]((void *)(old));
        }
    }
    
    if (table->destructorCount == PTHREAD_DESTRUCTOR_ITERATIONS - 1) {    // On PTHREAD_DESTRUCTOR_ITERATIONS-1 call, destroy our data
        free(table);
        
        __CFTSDSetSpecific(CF_TSD_BAD_PTR);
        return;
    }
}

我們可以看到碾局,table會循環(huán)遍歷datadestructors的數據荆残,并且把old變量作為destructors里函數的參數。table->destructors[i]((void *)(old));相當于就是在調用一個析構函數净当。通過前面的代碼内斯,我們知道RunLoop的析構函數是會存到destructors中去的。所以當線程退出的時候像啼,會調用到RunLoop的析構函數__CFFinalizeRunLoop釋放RunLoop俘闯。

接著看一下__CFFinalizeRunLoop函數

// Called for each thread as it exits
CF_PRIVATE void __CFFinalizeRunLoop(uintptr_t data) {
    CFRunLoopRef rl = NULL;
    if (data <= 1) {
        __CFLock(&loopsLock);
        if (__CFRunLoops) {
            rl = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(pthread_self()));
            if (rl) CFRetain(rl);
            // 移除全局字典中RunLoop與線程之間的映射關系
            CFDictionaryRemoveValue(__CFRunLoops, pthreadPointer(pthread_self()));
        }
        __CFUnlock(&loopsLock);
    } else {
        // 遞歸移除
        _CFSetTSD(__CFTSDKeyRunLoopCntr, (void *)(data - 1), (void (*)(void *))__CFFinalizeRunLoop);
    }
    if (rl && CFRunLoopGetMain() != rl) { // protect against cooperative threads
        if (NULL != rl->_counterpart) {
            CFRelease(rl->_counterpart);
            rl->_counterpart = NULL;
        }
        // purge all sources before deallocation
        CFArrayRef array = CFRunLoopCopyAllModes(rl);
        for (CFIndex idx = CFArrayGetCount(array); idx--;) {
            CFStringRef modeName = (CFStringRef)CFArrayGetValueAtIndex(array, idx);
             // 移除RunLoop中的mode
            __CFRunLoopRemoveAllSources(rl, modeName);
        }
        // 移除RunLoop中的common mode
        __CFRunLoopRemoveAllSources(rl, kCFRunLoopCommonModes);
        CFRelease(array);
    }
    if (rl) CFRelease(rl);
}

RunLoop相關的類與作用

CFRunLoop.c中關于RunLoop的類一共有五個,它們分別是CFRunLoopRef忽冻、CFRunLoopModeRef真朗、CFRunLoopSourceRefCFRunLoopObserverRef僧诚、CFRunLoopTimerRef遮婶。各個類之間的關系:

Runloop中類之間的關系

CFRunLoopRef

CFRunLoopRef對應__CFRunLoop結構體蝗碎,它的定義如下:

struct __CFRunLoop {
    // 省略其他成員變量
    ...
    // common mode的集合
    CFMutableSetRef _commonModes;
    // 每個common mode都有的item(source,timer and observer)集合
    CFMutableSetRef _commonModeItems;
    // 當前runloop的mode
    CFRunLoopModeRef _currentMode;
    // 所有的mode的集合
    CFMutableSetRef _modes;
};

一個RunLoop可以包含幾個Mode,但是必須指定一個Mode來運行旗扑,它取決于_currentMode的值蹦骑。關于_currentMode的賦值在CFRunLoopRunSpecific函數中。

CFRunLoopModeRef

接著看CFRunLoopModeRef臀防,CFRunLoopModeRef對應著__CFRunLoopMode結構體眠菇,其定義如下:

struct __CFRunLoopMode {
    CFStringRef _name;
    // source0的集合
    CFMutableSetRef _sources0;
    // source1的集合
    CFMutableSetRef _sources1;
    // observer的數組
    CFMutableArrayRef _observers;
    // timer的數組
    CFMutableArrayRef _timers;
    // 省略其他屬性
    ...
};

__CFRunLoopMode中包含的就是RunLoop要處理的一些事情(source0/source1/observer/timer)。前面提到RunLoop必須在執(zhí)行的Mode下運行袱衷,如果RunLoop需要切換Mode捎废,只能退出Loop,再重新指定一個Mode進入致燥。這樣的好處是:不同組的source0/source1/observer/timer可以相互隔離登疗,互不影響,從而提高執(zhí)行效率嫌蚤。

RunLoop的Mode

RunLoop有五種運行模式谜叹,其中常見的1、2和5這三種

  1. kCFRunLoopDefaultMode:App的默認Mode搬葬,通常主線程是在這個Mode下運行;
  2. UITrackingRunLoopMode:界面跟蹤Mode艳悔,用于滾動視圖追蹤觸摸滑動急凰,保證界面滑動時不受其他 Mode影響;
  3. UIInitializationRunLoopMode:在剛啟動App時第進入的第一個Mode猜年,啟動完成后就不再使用抡锈,會切換到kCFRunLoopDefaultMode;
  4. GSEventReceiveRunLoopMode:接受系統(tǒng)事件的內部Mode乔外;
  5. kCFRunLoopCommonModes:這是一個占位用的Mode床三,并不是一種真正的Mode;

CommonModes

kCFRunLoopCommonModes是蘋果提供的一種“CommonModes”杨幼。它其實是一個標識符撇簿,并不是一個具體的Mode。kCFRunLoopDefaultModeUITrackingRunLoopMode差购,并且都被標記為“CommonModes”四瘫。

一個Mode可以將自己標記為“Common”屬性(通過將其ModeName添加到RunLoop的commonModes中)。每當RunLoop的內容發(fā)生變化時欲逃,RunLoop都會自動將_commonModeItems里的source0/source1/observer/timer同步到具有“Common”標記的所有Mode里找蜜,即能在所有具有“Common”標記的所有Mode里運行。

CFRunLoopAddSource函數為例稳析,只關注“CommonModes”的部分:

void CFRunLoopAddSource(CFRunLoopRef rl, CFRunLoopSourceRef rls, CFStringRef modeName) {
    // 該Mode是CommonMode
    if (modeName == kCFRunLoopCommonModes) {
        // _commonModes存在則獲取一份數據拷貝
        CFSetRef set = rl->_commonModes ? CFSetCreateCopy(kCFAllocatorSystemDefault, rl->_commonModes) : NULL;
        if (NULL == rl->_commonModeItems) {
            // _commonModeItems不存在創(chuàng)建一個新的集合
            rl->_commonModeItems = CFSetCreateMutable(kCFAllocatorSystemDefault, 0, &kCFTypeSetCallBacks);
        }
        // 將source添加到_commonModeItems
        CFSetAddValue(rl->_commonModeItems, rls);
        if (NULL != set) {
            CFTypeRef context[2] = {rl, rls};
            // 調用__CFRunLoopAddItemToCommonModes函數向_commonModes中所有的Mode添加這個source
            CFSetApplyFunction(set, (__CFRunLoopAddItemToCommonModes), (void *)context);
            CFRelease(set);
        }
    }
}

上面的source0/source1/observer/timer被統(tǒng)稱為mode item洗做,一個item可以被同時加入多個Mode弓叛。如果Mode里沒有任何source0/source1/observer/timer,RunLoop便會立刻退出诚纸。

這也解決了一個問題--為什么列表滑動的時候撰筷,NSTimer不執(zhí)行回調?該如何解決咬清?

默認NSTimer是運行在RunLoop的kCFRunLoopDefaultMode下闭专,在列表滑動的時候,RunLoop會切換UITrackingRunLoopMode旧烧,因為RunLoop只能運行在一種模式下影钉,所以NSTimer不會執(zhí)行回調。
使用現成的API將NSTimer就有添加到CommonModes就可以掘剪,kCFRunLoopDefaultModeUITrackingRunLoopMode都已經被標為”Common”屬性的平委。這樣Timer就同時加入了這兩個Mode中。

CFRunLoopSourceRef

CFRunLoopSourceRef對應著__CFRunLoopSource結構體夺谁,其定義如下:

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

其中有兩個字段version0version1分別對應Source0Source1廉赔。

Source0

Source0的定義如下:

typedef struct {
    CFIndex version;
    void *  info;
    const void *(*retain)(const void *info);
    void    (*release)(const void *info);
    CFStringRef (*copyDescription)(const void *info);
    Boolean (*equal)(const void *info1, const void *info2);
    CFHashCode  (*hash)(const void *info);
    // 當source被添加到RunLoop中后,會調用這個指針
    void    (*schedule)(void *info, CFRunLoopRef rl, CFRunLoopMode mode);
    // 當調CFRunLoopSourceInvalidate函數移除該source的時候匾鸥,會調用這個指針
    void    (*cancel)(void *info, CFRunLoopRef rl, CFRunLoopMode mode);
    // RunLoop處理Source0的時候蜡塌,會調用這個指針
    void    (*perform)(void *info);
} CFRunLoopSourceContext;

大神的博客中提到:Source0并不能主動觸發(fā)事件。使用時勿负,你需要先調用CFRunLoopSourceSignal馏艾,將這個Source標記為待處理,然后手動調用CFRunLoopWakeUp來喚醒RunLoop奴愉,讓其處理這個事件琅摩。

優(yōu)秀的博客總是會被很多人閱讀和模仿,這是可以理解的锭硼。但是確實沒看到有人對這幾句結論進行驗證一下房资,當然我一開始也是看過記住,但是并沒有做進一步的理解檀头。

下面給出我自己的推導過程:

RunLoop通過__CFRunLoopDoSources0函數處理Source0轰异。在它的實現有一段很關鍵的代碼:

if (__CFRunLoopSourceIsSignaled(rls)) {
    __CFRunLoopSourceUnsetSignaled(rls);
    if (__CFIsValid(rls)) {
        __CFRunLoopSourceUnlock(rls);
        __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__(rls->_context.version0.perform, rls->_context.version0.info);
        CHECK_FOR_FORK();
        sourceHandled = true;
    } else {
        __CFRunLoopSourceUnlock(rls);
    }
}

將其簡化一下:

if (__CFRunLoopSourceIsSignaled(rls)) {
    __CFRunLoopSourceUnsetSignaled(rls);
    __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__(rls->_context.version0.perform, rls->_context.version0.info);
}

// __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__便是處理Source0的函數
// perform指針也是Source0中定義的,
static void __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__(void (*perform)(void *), void *info) {
    if (perform) {
        perform(info);
    }
    asm __volatile__(""); // thwart tail-call optimization
}

先判斷該Source0是否被標記鳖擒,如果是溉浙,取消該Source0的標記,并處理蒋荚。既然這樣肯定存在對應的一個標記函數__CFRunLoopSourceSetSignaled

// CFRunLoopSourceSignal函數是對外公開的戳稽。
void CFRunLoopSourceSignal(CFRunLoopSourceRef rls) {
    CHECK_FOR_FORK();
    __CFRunLoopSourceLock(rls);
    if (__CFIsValid(rls)) {
        __CFRunLoopSourceSetSignaled(rls);
    }
    __CFRunLoopSourceUnlock(rls);
}

關于CFRunLoopSourceSignal函數的使用,CFRunLoop.c并沒有相關使用代碼。但是在CFSocket.c文件中能找到些許痕跡惊奇。

相關代碼如下:

// 
if (shared->_source) {
    CFRunLoopSourceSignal(shared->_source);
    _CFRunLoopSourceWakeUpRunLoops(shared->_source);
}

// CFRunLoopSourceContext代表Source0
sock->_shared->_source = 
CFRunLoopSourceCreate(allocator, order, (CFRunLoopSourceContext *)&context);

if (sock->_shared->_source) {
    CFRunLoopSourceSignal(sock->_shared->_source);
    _CFRunLoopSourceWakeUpRunLoops(sock->_shared->_source);
}

// _CFRunLoopSourceWakeUpRunLoops是CFRunLoop.c中的內部方法
// 其核心就是調用CFRunLoopWakeUp函數
CF_PRIVATE void _CFRunLoopSourceWakeUpRunLoops(CFRunLoopSourceRef rls) {
    CFBagRef loops = NULL;
    __CFRunLoopSourceLock(rls);
    if (__CFIsValid(rls) && NULL != rls->_runLoops) {
        loops = CFBagCreateCopy(kCFAllocatorSystemDefault, rls->_runLoops);
    }
    __CFRunLoopSourceUnlock(rls);
    if (loops) {
    CFBagApplyFunction(loops, __CFRunLoopSourceWakeUpLoop, NULL);
        CFRelease(loops);
    }
}

static void __CFRunLoopSourceWakeUpLoop(const void *value, void *context) {
    // 主動喚醒RunLoop
    CFRunLoopWakeUp((CFRunLoopRef)value);
}

通過上面給出的相關代碼互躬,我想可以解釋Source0是如何被觸發(fā)的了。

使用Source0的情況:

  • 觸摸事件處理颂郎;


    RunLoop處理觸摸事件
  • 調用performSelector:onThread:withObject:waitUntilDone:方法吼渡;

Source1

Source1的定義如下:

typedef struct {
    CFIndex version;
    void *  info;
    const void *(*retain)(const void *info);
    void    (*release)(const void *info);
    CFStringRef (*copyDescription)(const void *info);
    Boolean (*equal)(const void *info1, const void *info2);
    CFHashCode  (*hash)(const void *info);
#if (TARGET_OS_MAC && !(TARGET_OS_EMBEDDED || TARGET_OS_IPHONE)) || (TARGET_OS_EMBEDDED || TARGET_OS_IPHONE)
    mach_port_t (*getPort)(void *info);
    void *  (*perform)(void *msg, CFIndex size, CFAllocatorRef allocator, void *info);
#else
    void *  (*getPort)(void *info);
    void    (*perform)(void *info);
#endif
} CFRunLoopSourceContext1;

Source1中有一個mach_port_t,mach_port是用于內核向線程發(fā)送消息的乓序。 注意:Source1在處理的時候會分發(fā)一些操作給Source0去處理寺酪。

使用Source1的情況:

  • 基于端口的線程間通信(A線程通過端口發(fā)送消息到B線程,這個消息是Source1的替劈;
  • 系統(tǒng)事件的捕捉寄雀,以點擊屏幕觸發(fā)事件為例,我們點擊屏幕到系統(tǒng)捕捉到這個點擊事件是Source1陨献,接著分發(fā)到Source0去處理這個點擊事件盒犹。

CFRunLoopObserverRef

CFRunLoopObserverRef對應著__CFRunLoopObserver結構體,實現如下:

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

每個Observer都包含了一個回調(函數指針CFRunLoopObserverCallBack _callout)眨业,當RunLoop的狀態(tài)發(fā)生變化時急膀,觀察者就能通過回調接受到這個變化。

RunLoop有以下幾種狀態(tài):

typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
    kCFRunLoopEntry = (1UL << 0), // 即將進入loop
    kCFRunLoopBeforeTimers = (1UL << 1), // 即將處理Timer
    kCFRunLoopBeforeSources = (1UL << 2), // 即將處理Source
    kCFRunLoopBeforeWaiting = (1UL << 5), // 即將進入休眠
    kCFRunLoopAfterWaiting = (1UL << 6), // 結束休眠或被喚醒
    kCFRunLoopExit = (1UL << 7), // 退出loop
    kCFRunLoopAllActivities = 0x0FFFFFFFU
};

使用Observer的情況:

  • 用于監(jiān)聽RunLoop的狀態(tài)龄捡;
  • UI刷新(Before Waiting)卓嫂;
  • AutoreleasePool釋放;

CFRunLoopTimerRef

CFRunLoopTimerRef對應著__CFRunLoopTimer結構體聘殖,實現如下:

struct __CFRunLoopTimer {
    CFRuntimeBase _base;
    uint16_t _bits;
    pthread_mutex_t _lock;
    CFRunLoopRef _runLoop;
    CFMutableSetRef _rlModes;
    CFAbsoluteTime _nextFireDate;
    CFTimeInterval _interval;       /* immutable */
    CFTimeInterval _tolerance;          /* mutable */
    uint64_t _fireTSR;          /* TSR units */
    CFIndex _order;         /* immutable */
    CFRunLoopTimerCallBack _callout;    /* immutable */
    CFRunLoopTimerContext _context; /* immutable, except invalidation */
};

使用Timer的情況:

  • NSTimer命黔,NSTimer基于RunLoop,其內部使用的就是CFRunLoopTimerRef就斤;
  • performSelector:withObject:afterDelay:或類似帶有afterDelay的方法。

RunLoop運行

RunLoop通過CFRunLoopRunCFRunLoopRunInMode這兩個函數運行蘑辑。

CFRunLoopRun

void CFRunLoopRun(void) {
    int32_t result;
    do {
        result = CFRunLoopRunSpecific(CFRunLoopGetCurrent(), kCFRunLoopDefaultMode, 1.0e10, false);
        CHECK_FOR_FORK();
    } while (kCFRunLoopRunStopped != result && kCFRunLoopRunFinished != result);
}

函數默認在kCFRunLoopDefaultMode下運行RunLoop洋机,并且一直運行在一個do-while的循環(huán)里。
另外函數不會主動調用CFRunLoopStop函數(kCFRunLoopRunStopped)或者將所有事件源移除(kCFRunLoopRunFinished)洋魂。

CFRunLoopRunInMode

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

無論是CFRunLoopRun還是CFRunLoopRunInMode都是調用了CFRunLoopRunSpecific绷旗。

CFRunLoopRunSpecific

這里對CFRunLoopRunSpecific函數的實現做了精簡處理:

SInt32 CFRunLoopRunSpecific(CFRunLoopRef rl, CFStringRef modeName, CFTimeInterval seconds, Boolean returnAfterSourceHandled) {
    // 第1步:通知Observers,進入loop
    __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopEntry);
    // 具體要做的事情
    result = __CFRunLoopRun(rl, currentMode, seconds, returnAfterSourceHandled, previousMode);
    // 第10步:通知Observers副砍,退出loop
    __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopExit);
    
    return result;
}

__CFRunLoopRun

__CFRunLoopRun可以說是RunLoop運行的核心方法衔肢。由于代碼過長,這里對代碼進行了精簡:

static int32_t __CFRunLoopRun(CFRunLoopRef rl, CFRunLoopModeRef rlm, CFTimeInterval seconds, Boolean stopAfterHandle, CFRunLoopModeRef previousMode) {
    int32_t retVal = 0;
    do {
        // 第2步:通知Observers豁翎,即將處理Timers
        __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeTimers);
        // 第3步:通知Observers角骤,即將處理Source
        __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeSources);
        // 處理Blocks
        __CFRunLoopDoBlocks(rl, rlm);
        
        // 第4步:處理Source0
        Boolean sourceHandledThisLoop = __CFRunLoopDoSources0(rl, rlm, stopAfterHandle);
        if (sourceHandledThisLoop) {
            // 處理Blocks
            __CFRunLoopDoBlocks(rl, rlm);
        }
        
        Boolean poll = sourceHandledThisLoop || (0ULL == timeout_context->termTSR);
        
        // 第5步:判斷有無Source1,有Source1,跳轉到handle_msg
        if (__CFRunLoopServiceMachPort(dispatchPort, &msg, sizeof(msg_buffer), &livePort, 0, &voucherState, NULL)) {
            // 
            goto handle_msg;
        }
        
        didDispatchPortLastTime = false;
        
        // 第6步:通知Observers邦尊,即將進入休眠
        __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeWaiting);
        
        // RunLoop休眠
        __CFRunLoopSetSleeping(rl);
        
        CFAbsoluteTime sleepStart = poll ? 0.0 : CFAbsoluteTimeGetCurrent();
        
        // 第7步:等待別的消息來喚醒背桐,如果沒有被喚醒那就不會執(zhí)行下面的代碼
        // 這些消息可能是:
        // 一個基于port的Source的事件。
        // 一個Timer到時間了
        // RunLoop自身的超時時間到了
        // 被其他什么調用者手動喚醒
        __CFRunLoopServiceMachPort(waitSet,
                                   &msg,
                                   sizeof(msg_buffer),
                                   &livePort, poll ? 0 : TIMEOUT_INFINITY,
                                   &voucherState,
                                   &voucherCopy);
        
        rl->_sleepTime += (poll ? 0.0 : (CFAbsoluteTimeGetCurrent() - sleepStart));
        
        // 取消RunLoop的休眠
        __CFRunLoopUnsetSleeping(rl);
        
        // 第8步:通知Observers蝉揍,結束休眠
        __CFRunLoopDoObservers(rl, rlm, kCFRunLoopAfterWaiting);
    
    // 判斷RunLoop被喚醒的方式链峭,并處理對應的事件
    handle_msg:;
        // 判斷RunLoop被喚醒的方式
        // MACH_PORT_NULL == livePort和livePort == rl->_wakeUpPort兩種情況什么都不做,省略
        // 被timer喚醒
        else if (rlm->_timerPort != MACH_PORT_NULL && livePort == rlm->_timerPort) {
            // 處理timer
            __CFRunLoopDoTimers(rl, rlm, mach_absolute_time());
        }
        // 被GCD喚醒
        else if (livePort == dispatchPort) {
            // 處理GCD
            __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(msg);
        }
        // 被Source1喚醒
        else {
            // 處理Source1
            __CFRunLoopDoSource1(rl, rlm, rls, msg, msg->msgh_size, &reply) || sourceHandledThisLoop;
        }
        
        // 處理Blocks
        __CFRunLoopDoBlocks(rl, rlm);
        
        // 第9步:決定RunLoop的返回值
        if (sourceHandledThisLoop && stopAfterHandle) {
            // 處理完事件就返回
            retVal = kCFRunLoopRunHandledSource;
        } else if (timeout_context->termTSR < mach_absolute_time()) {
            // 超時
            retVal = kCFRunLoopRunTimedOut;
        } else if (__CFRunLoopIsStopped(rl)) {
            // RunLoop終止
            __CFRunLoopUnsetStopped(rl);
            retVal = kCFRunLoopRunStopped;
        } else if (rlm->_stopped) {
            // mode終止
            rlm->_stopped = false;
            retVal = kCFRunLoopRunStopped;
        } else if (__CFRunLoopModeIsEmpty(rl, rlm, previousMode)) {
            retVal = kCFRunLoopRunFinished;
        }    
    } while (0 == retVal);
    
    return retVal;
}

RunLoop運行流程圖:
RunLoop_run

上述過程中有兩個地方要注意:

1.RunLoop處理GCD事件

在大多數情況又沾,RunLoop和GCD各自有這自己的執(zhí)行流程弊仪,不會出現依賴,但是有一種情況比較特殊杖刷。先看以下代碼:

dispatch_async(dispatch_get_global_queue(0, 0), ^{
    dispatch_async(dispatch_get_main_queue(), ^{
        NSLog(@"1111111");
    });
});

打印函數調用棧:


RunLoop處理GCD事件

使用GCD異步操作的時候励饵,我們在一個子線程處理完一些事情后,要返回主線程處理事情的時候挺勿,這時候需要依賴于RunLoop曲横。內部會調用__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__函數。

2.RunLoop休眠

當RunLoop一旦休眠意味著CPU不會分配任何資源不瓶,那線程也就沒有事情干了禾嫉,也進入休眠。RunLoop休眠內部是調用了mach_msg()函數蚊丐。操作系統(tǒng)中有內核層面的API和應用層面的API熙参。內核層面的API是不會輕易暴露出來,mach_msg()可以理解為是應用層面的API麦备,告訴內核休眠該線程休眠孽椰。一旦接受到系統(tǒng)事件,也會轉化成內核API凛篙,告訴內核需要喚醒該線程黍匾,那么又可以執(zhí)行應用層API了。所以RunLoop的休眠可以看成是用戶狀態(tài)到內核狀態(tài)的切換呛梆,而喚醒RunLoop就是內核狀態(tài)到用戶狀態(tài)的切換锐涯。

總結

由于總結的東西相對來說比較多,會以面試題的形式單獨寫一篇RunLoop面試題分析來總結填物。

參考

《程序員的自我修養(yǎng)》
CoreFoundation源代碼
深入理解RunLoop
蘋果文檔--RunLoop

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末纹腌,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子滞磺,更是在濱河造成了極大的恐慌升薯,老刑警劉巖,帶你破解...
    沈念sama閱讀 219,539評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件击困,死亡現場離奇詭異涎劈,居然都是意外死亡,警方通過查閱死者的電腦和手機,發(fā)現死者居然都...
    沈念sama閱讀 93,594評論 3 396
  • 文/潘曉璐 我一進店門责语,熙熙樓的掌柜王于貴愁眉苦臉地迎上來炮障,“玉大人,你說我怎么就攤上這事坤候⌒灿” “怎么了?”我有些...
    開封第一講書人閱讀 165,871評論 0 356
  • 文/不壞的土叔 我叫張陵白筹,是天一觀的道長智末。 經常有香客問我,道長徒河,這世上最難降的妖魔是什么系馆? 我笑而不...
    開封第一講書人閱讀 58,963評論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮顽照,結果婚禮上由蘑,老公的妹妹穿的比我還像新娘。我一直安慰自己代兵,他們只是感情好尼酿,可當我...
    茶點故事閱讀 67,984評論 6 393
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著植影,像睡著了一般裳擎。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上思币,一...
    開封第一講書人閱讀 51,763評論 1 307
  • 那天鹿响,我揣著相機與錄音,去河邊找鬼谷饿。 笑死惶我,一個胖子當著我的面吹牛,可吹牛的內容都是我干的博投。 我是一名探鬼主播指孤,決...
    沈念sama閱讀 40,468評論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼贬堵!你這毒婦竟也來了?” 一聲冷哼從身側響起结洼,我...
    開封第一講書人閱讀 39,357評論 0 276
  • 序言:老撾萬榮一對情侶失蹤黎做,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后松忍,有當地人在樹林里發(fā)現了一具尸體蒸殿,經...
    沈念sama閱讀 45,850評論 1 317
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 38,002評論 3 338
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現自己被綠了宏所。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片酥艳。...
    茶點故事閱讀 40,144評論 1 351
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖爬骤,靈堂內的尸體忽然破棺而出充石,到底是詐尸還是另有隱情,我是刑警寧澤霞玄,帶...
    沈念sama閱讀 35,823評論 5 346
  • 正文 年R本政府宣布骤铃,位于F島的核電站,受9級特大地震影響坷剧,放射性物質發(fā)生泄漏惰爬。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,483評論 3 331
  • 文/蒙蒙 一惫企、第九天 我趴在偏房一處隱蔽的房頂上張望撕瞧。 院中可真熱鬧,春花似錦狞尔、人聲如沸丛版。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,026評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽硼婿。三九已至,卻和暖如春禽车,著一層夾襖步出監(jiān)牢的瞬間寇漫,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,150評論 1 272
  • 我被黑心中介騙來泰國打工殉摔, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留州胳,地道東北人。 一個月前我還...
    沈念sama閱讀 48,415評論 3 373
  • 正文 我出身青樓逸月,卻偏偏與公主長得像栓撞,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子碗硬,可洞房花燭夜當晚...
    茶點故事閱讀 45,092評論 2 355

推薦閱讀更多精彩內容