重拾RunLoop原理

博客链接重拾RunLoop原理

更新于2019.07.26

虽然自己很早前就看过RunLoop的源码,当时看得时候,有点地方还是比较生涩的。所有抽了个时间,重新整理了一下之前RunLoop的笔记。CoreFoundation源代码关于RunLoop的源码主要集中在CFRunLoop.c文件中。

RunLoop的获取

苹果并不允许我们直接创建RunLoop,RunLoop的创建在第一次获取的时候,使用[NSRunLoop mainRunLoop]CFRunLoopGetMain()可以获取主线程的RunLoop;通过[NSRunLoop currentRunLoop]CFRunLoopGetCurrent()获取当前线程的RunLoop。

它们之间的关系是Foundation中的RunLoop是对Core Foundation中的包装。可以通过执行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是有值的,就不会再重新创建,否则就使用_CFRunLoopGet0进行创建,传入的是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是一个全局的静态字典。
    // 如果该字典为空,就进行以下操作:
    // 1.创建一个临时字典;
    // 2.创建主线程的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创建一个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. 在线程创建的时候,是没有对应的RunLoop,它的创建是在第一次获取的时候,它的销毁则发生在线程销毁的时候。

之前在看源码的时候有两个地方不是很理解:

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

后来在《程序员的自我修养》第29页中得到启发。里面关于单例有这样一段代码:

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同时执行第一个判断语句,结果一样,都进入了代码块。lock()的设定就是只允许一个线程进入,假设A先进入,B在等待。A进入后首先判断pInstNULL,那么new一个对象,然后解锁返回对象。唤醒B,这是B进入发现第二个判断通过不了(因为pInst已经有值了),这样的话B就直接解锁返回对象。假设只有最外层的判断的话,那么B也会创建一个对象。

我想这里应该也是类似的作用吧。

2.RunLoop销毁的时机

上面的源代码只说明了这个会在RunLoop的析构函数是__CFFinalizeRunLoop,但是具体的释放时机会在后面说明。

RunLoop的创建

_CFRunLoopGet0函数的实现中可以知道,RunLoop的创建是通过调用使用__CFRunLoopCreate返回一个CFRunLoopRef的实例,这个函数大致分为两步:

  1. 使用_CFRuntimeCreateInstance创建一个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的释放是发生在线程销毁的时候。

__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会循环遍历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的类一共有五个,它们分别是CFRunLoopRefCFRunLoopModeRefCFRunLoopSourceRefCFRunLoopObserverRefCFRunLoopTimerRef。各个类之间的关系:

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

接着看CFRunLoopModeRefCFRunLoopModeRef对应着__CFRunLoopMode结构体,其定义如下:

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

__CFRunLoopMode中包含的就是RunLoop要处理的一些事情(source0/source1/observer/timer)。前面提到RunLoop必须在执行的Mode下运行,如果RunLoop需要切换Mode,只能退出Loop,再重新指定一个Mode进入。这样的好处是:不同组的source0/source1/observer/timer可以相互隔离,互不影响,从而提高执行效率

RunLoop的Mode

RunLoop有五种运行模式,其中常见的1、2和5这三种

  1. kCFRunLoopDefaultMode:App的默认Mode,通常主线程是在这个Mode下运行;
  2. UITrackingRunLoopMode:界面跟踪Mode,用于滚动视图追踪触摸滑动,保证界面滑动时不受其他 Mode影响;
  3. UIInitializationRunLoopMode:在刚启动App时第进入的第一个Mode,启动完成后就不再使用,会切换到kCFRunLoopDefaultMode;
  4. GSEventReceiveRunLoopMode:接受系统事件的内部Mode;
  5. kCFRunLoopCommonModes:这是一个占位用的Mode,并不是一种真正的Mode;

CommonModes

kCFRunLoopCommonModes是苹果提供的一种“CommonModes”。它其实是一个标识符,并不是一个具体的Mode。kCFRunLoopDefaultModeUITrackingRunLoopMode,并且都被标记为“CommonModes”。

一个Mode可以将自己标记为“Common”属性(通过将其ModeName添加到RunLoop的commonModes中)。每当RunLoop的内容发生变化时,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不存在创建一个新的集合
            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被统称为mode item,一个item可以被同时加入多个Mode。如果Mode里没有任何source0/source1/observer/timer,RunLoop便会立刻退出。

这也解决了一个问题--为什么列表滑动的时候,NSTimer不执行回调?该如何解决?

默认NSTimer是运行在RunLoop的kCFRunLoopDefaultMode下,在列表滑动的时候,RunLoop会切换UITrackingRunLoopMode,因为RunLoop只能运行在一种模式下,所以NSTimer不会执行回调。
使用现成的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并不能主动触发事件。使用时,你需要先调用CFRunLoopSourceSignal,将这个Source标记为待处理,然后手动调用CFRunLoopWakeUp来唤醒RunLoop,让其处理这个事件。

优秀的博客总是会被很多人阅读和模仿,这是可以理解的。但是确实没看到有人对这几句结论进行验证一下,当然我一开始也是看过记住,但是并没有做进一步的理解。

下面给出我自己的推导过程:

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是如何被触发的了。

使用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是用于内核向线程发送消息的。 注意:Source1在处理的时候会分发一些操作给Source0去处理

使用Source1的情况:

  • 基于端口的线程间通信(A线程通过端口发送消息到B线程,这个消息是Source1的;
  • 系统事件的捕捉,以点击屏幕触发事件为例,我们点击屏幕到系统捕捉到这个点击事件是Source1,接着分发到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的状态发生变化时,观察者就能通过回调接受到这个变化。

RunLoop有以下几种状态:

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的情况:

  • 用于监听RunLoop的状态;
  • 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的循环里。
另外函数不会主动调用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步:等待别的消息来唤醒,如果没有被唤醒那就不会执行下面的代码
        // 这些消息可能是:
        // 一个基于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各自有这自己的执行流程,不会出现依赖,但是有一种情况比较特殊。先看以下代码:

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()函数。操作系统中有内核层面的API和应用层面的API。内核层面的API是不会轻易暴露出来,mach_msg()可以理解为是应用层面的API,告诉内核休眠该线程休眠。一旦接受到系统事件,也会转化成内核API,告诉内核需要唤醒该线程,那么又可以执行应用层API了。所以RunLoop的休眠可以看成是用户状态到内核状态的切换,而唤醒RunLoop就是内核状态到用户状态的切换。

总结

由于总结的东西相对来说比较多,会以面试题的形式单独写一篇RunLoop面试题分析来总结。

参考

《程序员的自我修养》
CoreFoundation源代码
深入理解RunLoop
苹果文档--RunLoop

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 157,298评论 4 360
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,701评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 107,078评论 0 237
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,687评论 0 202
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,018评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,410评论 1 211
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,729评论 2 310
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,412评论 0 194
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,124评论 1 239
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,379评论 2 242
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,903评论 1 257
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,268评论 2 251
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,894评论 3 233
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,014评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,770评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,435评论 2 269
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,312评论 2 260

推荐阅读更多精彩内容