概念

  • RunLoop 是一个对象,在它的生命周期中有一个线程与之对应,它是这个线程的thread specific data(线程特有数据);它是线程不安全的,所有的操作都必须在这个线程中进行;
  • 开发者很少直接使用 RunLoop,大部分情况下系统已经通过 RunLoop 来响应例如网络、输入输出事件、点击事件、UI事件、定时器事件等;
  • RunLoop 内部实际上是一个 dowhile 循环,不同于我们自己写的循环,RunLoop 循环的目的是保证系统能在闲时不占用过多的资源,而在需要响应事件时及时做出响应;

RunLoop 流程

在了解 RunLoop 流程之前,需要先了解 RunLoop 的一些属性或元素

RunLoop 相关的属性/元素

在 CoreFoundation 里面关于 RunLoop 有5个类:

  • CFRunLoop
  • CFRunLoopMode
  • CFRunLoopSource
  • CFRunLoopTimer
  • CFRunLoopObserver

下面是部分源码:

/* CFRunLoop.h */

typedef struct __CFRunLoop * CFRunLoopRef;

typedef struct __CFRunLoopSource * CFRunLoopSourceRef;

typedef struct __CFRunLoopObserver * CFRunLoopObserverRef;

typedef struct CF_BRIDGED_MUTABLE_TYPE(NSTimer) __CFRunLoopTimer * CFRunLoopTimerRef;
/* CFRunLoop.c */

struct __CFRunLoopMode {
    ...
    CFMutableSetRef _sources0;
    CFMutableSetRef _sources1;
    CFMutableArrayRef _observers;
    CFMutableArrayRef _timers;
    ...
}

struct __CFRunLoop {
    ...
    CFMutableSetRef _commonModes;
    CFMutableSetRef _commonModeItems;
    CFRunLoopModeRef _currentMode;
    CFMutableSetRef _modes;
    ...
}

通过这些声明,可以很清楚的明确它们的关系,但 CFRunLoopMode 并未开放出来

  • CFRunLoop 包含若干 CFRunLoopMode
  • CFRunLoopMode 包含若干 CFRunLoopSourceCFRunLoopObserverCFRunLoopTimer

我们先从 CFRunLoopMode 包含的 source/observer/timer 说起

CFRunLoopSource

CFRunLoopSource 用定义了一个 _context 共同体,个人认为这个属性是用来区分 source0source1 的:

struct __CFRunLoopSource {
    ...
    union {
    	CFRunLoopSourceContext version0;  /* immutable, except invalidation */
    	CFRunLoopSourceContext1 version1; /* immutable, except invalidation */
    } _context;
}

通过 ibireme(后简称”I大”)的文章「RunLoop 对外的接口」一节我们得知 source0source1 的区别:

CFRunLoopSourceRef 是事件产生的地方。Source有两个版本:Source0 和 Source1。

  • Source0 只包含了一个回调(函数指针),它并不能主动触发事件。使用时,你需要先调用 CFRunLoopSourceSignal(source),将这个 Source 记为待处理,然后手动调用 CFRunLoopWakeUp(runloop) 来唤醒 RunLoop,让其处理这个事件。

  • Source1 包含了一个 mach_port 和一个回调(函数指针),被用于通过内核和其他线程相互发送消息。这种 Source 能主动唤醒 RunLoop 的线程,其原理在下面会讲到。

source1 的信息可以在 CFRunLoopAddSource 这个函数实现中发现一些:

/* CFRunLoop.c */

...
typedef mach_port_t __CFPort;
...

void CFRunLoopAddSource(CFRunLoopRef rl, CFRunLoopSourceRef rls, CFStringRef modeName) { /* DOES CALLOUT */
    ...
    CFRunLoopModeRef rlm = __CFRunLoopFindMode(rl, modeName, true);
	if (NULL != rlm && NULL == rlm->_sources0) {
	    rlm->_sources0 = CFSetCreateMutable(kCFAllocatorSystemDefault, 0, &kCFTypeSetCallBacks);
	    rlm->_sources1 = CFSetCreateMutable(kCFAllocatorSystemDefault, 0, &kCFTypeSetCallBacks);
	    rlm->_portToV1SourceMap = CFDictionaryCreateMutable(kCFAllocatorSystemDefault, 0, NULL, NULL);
	}
    ...
    
    if (NULL != rlm && !CFSetContainsValue(rlm->_sources0, rls) && !CFSetContainsValue(rlm->_sources1, rls)) {
        ...
        
        __CFPort src_port = rls->_context.version1.getPort(rls->_context.version1.info);
        ...
    }
}

CFRunLoopObserver

CFRunLoopObserver 是观察者,它包含了一个状态位和回调函数:

/* CFRunLoop.c */
struct __CFRunLoopObserver {
    ...
    CFOptionFlags _activities;		/* immutable */
    ...
    CFRunLoopObserverCallBack _callout;	/* immutable */
    ...
}
/* CFRunLoop.h */
/* Run Loop Observer Activities */
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
};

CFRunLoopTimer

以下引用自 I大 的文章:

CFRunLoopTimerRef 是基于时间的触发器,它和 NSTimer 是toll-free bridged 的,可以混用。其包含一个时间长度和一个回调(函数指针)。当其加入到 RunLoop 时,RunLoop会注册对应的时间点,当时间点到时,RunLoop会被唤醒以执行那个回调。

其结构大致如下:

/* CFRunLoop.c */
struct __CFRunLoopTimer {
    ...
    CFTimeInterval _interval;		/* immutable */
    ...
    CFRunLoopTimerCallBack _callout;	/* immutable */
    ...
}

CFRunLoopMode

以上 source/observer/timer 是 CFRunLoopMode 的重要组成部分:

从源码很容易看出,Runloop总是运行在某种特定的CFRunLoopModeRef下(每次运行__CFRunLoopRun()函数时必须指定Mode)。而通过CFRunloopRef对应结构体的定义可以很容易知道每种Runloop都可以包含若干个Mode,每个Mode又包含Source/Timer/Observer。每次调用Runloop的主函数__CFRunLoopRun()时必须指定一种Mode,这个Mode称为** _currentMode**,当切换Mode时必须退出当前Mode,然后重新进入Runloop以保证不同Mode的Source/Timer/Observer互不影响。

以上文章节选自另一位先行者 KenshinCui(下文简称“K大”)的文章,以下是节选的部分源码,以验证 K大 的结论:

/* CFRunLoop.c */
static int32_t __CFRunLoopRun(CFRunLoopRef rl, CFRunLoopModeRef rlm, CFTimeInterval seconds, Boolean stopAfterHandle, CFRunLoopModeRef previousMode) {
    ...
    if (__CFRunLoopIsStopped(rl)) {
        __CFRunLoopUnsetStopped(rl);
        return kCFRunLoopRunStopped;
    } else if (rlm->_stopped) {
	    rlm->_stopped = false;
	    return kCFRunLoopRunStopped;
    }
    ...
}

通过 CFRunLoopFindMode 函数获取 mode:

/* CFRunLoop.c */

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

SInt32 CFRunLoopRunSpecific(CFRunLoopRef rl, CFStringRef modeName, CFTimeInterval seconds, Boolean returnAfterSourceHandled) {     /* DOES CALLOUT */
    ...
    CFRunLoopModeRef currentMode = __CFRunLoopFindMode(rl, modeName, false);
    ...
    int32_t result = kCFRunLoopRunFinished;

	if (currentMode->_observerMask & kCFRunLoopEntry ) __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopEntry);
	    result = __CFRunLoopRun(rl, currentMode, seconds, returnAfterSourceHandled, previousMode);
	...
}

static CFRunLoopModeRef __CFRunLoopFindMode(CFRunLoopRef rl, CFStringRef modeName, Boolean create) {
    ...
    rlm = (CFRunLoopModeRef)_CFRuntimeCreateInstance(kCFAllocatorSystemDefault, __kCFRunLoopModeTypeID, sizeof(struct __CFRunLoopMode) - sizeof(CFRuntimeBase), NULL);
    if (NULL == rlm) {
	    return NULL;
    }
    ...
    rlm->_name = CFStringCreateCopy(kCFAllocatorSystemDefault, modeName);
    rlm->_stopped = false;
    ...
    
    return rlm;
}

开发者可以通过以下两个方法来管理 mode:

CF_EXPORT void CFRunLoopAddCommonMode(CFRunLoopRef rl, CFStringRef mode);
CF_EXPORT SInt32 CFRunLoopRunInMode(CFStringRef mode, CFTimeInterval seconds, Boolean returnAfterSourceHandled);

它们在内部通过以下方法进行 mode 的修改:

void CFRunLoopAddSource(CFRunLoopRef rl, CFRunLoopSourceRef rls, CFStringRef modeName);
void CFRunLoopRemoveSource(CFRunLoopRef rl, CFRunLoopSourceRef rls, CFStringRef modeName);

void CFRunLoopAddObserver(CFRunLoopRef rl, CFRunLoopObserverRef rlo, CFStringRef modeName);
void CFRunLoopRemoveObserver(CFRunLoopRef rl, CFRunLoopObserverRef rlo, CFStringRef modeName);

void CFRunLoopAddTimer(CFRunLoopRef rl, CFRunLoopTimerRef rlt, CFStringRef modeName);
void CFRunLoopRemoveTimer(CFRunLoopRef rl, CFRunLoopTimerRef rlt, CFStringRef modeName);

执行三组函数时,RunLoop 都会将 _commonModeItems 里的 Source/Observer/Timer 同步到具有 “Common” 标记的所有Mode里,我们可以在以上代码的实现中发现以下代码片段:

    ...
    if (modeName == kCFRunLoopCommonModes) {
	    CFSetRef set = rl->_commonModes ? CFSetCreateCopy(kCFAllocatorSystemDefault, rl->_commonModes) : NULL;
	    if (NULL == rl->_commonModeItems) {
	        rl->_commonModeItems = CFSetCreateMutable(kCFAllocatorSystemDefault, 0, &kCFTypeSetCallBacks);
	    }
	    CFSetAddValue(rl->_commonModeItems, rlt);
	    ...
    }
    ...

RunLoop 的内部逻辑

苹果在 RunLoop 文档The Run Loop Sequence of Events这一项中提到了两个概念:non-port-based input sourcesport-based input source,根据上面我们分析的 source0 和 source1 的区别,很容易得知 source0 是前者,而 source1 属于后者;流程图可以参照 I大 文章中 「RunLoop 的内部逻辑」展示的图片,如下图所示:

RunLoop内部逻辑

这一章 I大 和K大 的文章说的都很具体,我们可以结合起来一起看。详细的代码可以在 CFRunLoop.c 文件中的 CFRunLoopRunSpecific 函数找到,这里就不再展开说明了;

RunLoop 与线程的关系

还有一点需要说明的是,苹果不允许用户直接创建 RunLoop,而是提供了两个函数来实现:

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

它们函数都调用了 _CFRunLoopGet0,在这个函数中我们可以看出 RunLoop 与线程的关系,下面是部分源码:

/* CFRunLoop.c */
// should only be called by Foundation
// t==0 is a synonym for "main thread" that always works
CF_EXPORT CFRunLoopRef _CFRunLoopGet0(pthread_t t) {
    if (pthread_equal(t, kNilPthreadT)) {
	    t = pthread_main_thread_np();
    }
    __CFLock(&loopsLock);
    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);
    }
    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())) {
        _CFSetTSD(__CFTSDKeyRunLoop, (void *)loop, NULL);
        if (0 == _CFGetTSD(__CFTSDKeyRunLoopCntr)) {
            _CFSetTSD(__CFTSDKeyRunLoopCntr, (void *)(PTHREAD_DESTRUCTOR_ITERATIONS-1), (void (*)(void *))__CFFinalizeRunLoop);
        }
    }
    return loop;
}

一些说明:

  • 首次调用这个函数时,因为 __CFRunLoopsNULL,会创建一个字典 dictmainLoop,并将 pthreadPointer(pthread_main_thread_np()) 作为 keymainLoop 作为值保存在 dict 里;此时通过 OSAtomicCompareAndSwapPtrBarrier 函数,在 __CFRunLoops 等于 NULL 时,将 dict 赋值给 __CFRunLoops,并保证其原子性,否则不赋值;
  • 根据传递进来的线程 t 获取 runLoop 对象,如果对象不存在就创建一个新的;

这里有一个cf_trace 函数,在文件头部被声明为 #define cf_trace(...) do{}while(0),这在 C/C++ 中比较常见,一般用来消除 goto 语句;

在宏定义中声明,还能够让宏定义作为 if 语句条件的情况下保证不出错;

网上搜了搜并没有发现 tc_trace 的有关说明,因此这个函数在这里的具体作用尚不清楚,如有知晓的老哥老姐欢迎补充;

  • 下一步,判断传递进来的线程 t 是否是当前线程,如果是,就调用 CFSetTSD 设置 __CFRunLoopsloop 的对应关系;TSDthread specific data,意思是线程特有数据;
  • 最后一步,通过 __CFFinalizeRunLoop 这个函数将其释放;至于为什么通过 CFTSDKeyRunLoopCntr 能够设置清理 loop 的回调,另一位先行者 ZenonHuang 在他的 blog 里做了详细说明;

顺带说一句,通过 CFPlatform.c__CFTSDFinalize 的实现,以及 CFInternal.h 中的宏定义:

{

__CFTSDKeyRunLoop = 10,

// autorelease pool stuff must be higher than run loop constants __CFTSDKeyAutoreleaseData2 = 61,

__CFTSDKeyAutoreleaseData1 = 62,

}

可以明确推断出 RunLoopaotuRelease 的释放顺序关系:前者在后者释放之前释放;

RunLoop 实践

何时使用 RunLoop

苹果在文档中指出,开发者使用 RunLoop 的唯一场景是在需要创建辅助线程时;而在主线程,RunLoop 会由系统自动配置并生效;

如果启用了辅助线程,使用 RunLoop 也并非必须:如果只这个线程中执行一些预定的耗时操作,而不涉及到跨线程的交互时,是不需要用到 RunLoop 的:

If you use a thread to perform some long-running and predetermined task, you can probably avoid starting the run loop.

那么何种场景需要采用 RunLoop 呢?苹果给出了几个场景:

  • 需要通过使用端口或自定义输入源与其它线程进行交互;
  • 在线程里使用定时器;
  • 在 Cocoa 应用程序中使用任意 performSelector... 方法;
  • 保持线程定期的执行某项任务;

总结一下就是涉及跨线程交互、涉及时间相关(定期做某事)都需要用到 RunLoop

例子

假如某个操作比较耗时,且需要多次调用,我们可以新开一个线程来处理,为了保持这个线程一直存在,我们需要在线程中加入一个 RunLoop 来保证这个线程一直存在,大致的代码如下:

/* ViewController.swift */

    /// 懒加载自定义线程对象
    lazy var myThread: Thread = {
        let th = Thread.init(target: self, selector: #selector(runTask), object: nil)
        return th
    }()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        // 模拟第一次执行
        DispatchQueue.main.asyncAfter(deadline: .now()+1.0) {
            self.perform(#selector(self.otherTask), on: self.myThread, with: nil, waitUntilDone: false)
        }
        // 模拟第二次执行
        DispatchQueue.main.asyncAfter(deadline: .now()+5.0) {
            self.perform(#selector(self.aTask), on: self.myThread, with: nil, waitUntilDone: false)
        }
    }
    
    @objc func runTask() -> Void {
        print("run task")
        // 增加一个 runloop 
        let loop = RunLoop.current
        loop.add(NSMachPort.init(), forMode: .default)
        loop.run()
    }
    
    @objc func otherTask() -> Void {
        print("otherTask")
        
        if self.myThread.isEqual(Thread.current) {
            print("123")
            // 如果需要退出线程,执行这一句
            // Thread.exit()
        }
    }
    
    @objc func aTask() ->Void {
        print("aTask")
        if self.myThread == Thread.current {
            print("222")
        }
    }

写在最后

通过阅读前人的文章,再加上自己的验证,能够让自己加深对知识的印象,只是需要花费比较多的时间;目前来看自己在理论方面掌握的相对强一点,而在实际运用方面略有不足;在学习的过程中我了解到有很多第三方框架在实现的过程中都用到了 RunLoop 的知识,后面我会抽时间去阅读一些运用到多线程的框架的源码,比如 AFNetworkingSDWebImage 甚至是它们的 Swift 版。

参考链接