一,什么是RunLoop?
要说RunLoop就必须说道线程.一般情况下,一个线程被开启,它去执行自己的任务,当任务完成后,线程就会退出.如果我们需要某个线程在没有任务的时候,不退出,我们就引入了RunLoop.所以RunLoop得存在,就是为了让一个线程在无任务的时候,不退出.
一般情况下,当一个线程在执行完线程中的任务后,线程会自动退出.如果我们需要你个线程处理一个周期性任务,不想让他退出,那么我们就需要一个机制,让线程在执行完任务,或者暂时无任务的状态下不退出.在IOS和MacOS中,这个东西就是RunLoop.RunLoop可以让线程在有任务的时候执行,没有任务的时候处于休眠状态,从而节省资源.
RunLoop其实就是一个对象,这个对象管理了其所需要处理的事件和消息.
二,RunLoop是和线程的关系.
RunLoop,翻译过来就是跑圈的意思.苹果向我们提供了两个获取RunLoop的函数,MainRunLoop和currentRunLoop.
当然一个是主线程对应的RunLoop,一个是当前线程的RunLoop.我们可以在这里找到CFRunLoopRef的源码.其中有一段能看出RunLoop的到底是干什么的:
/// 全局的Dictionary,key 是 pthread_t, value 是 CFRunLoopRef
static CFMutableDictionaryRef loopsDic;
/// 访问 loopsDic 时的锁
static CFSpinLock_t loopsLock;
/// 获取一个 pthread 对应的 RunLoop。
CFRunLoopRef _CFRunLoopGet(pthread_t thread) {
OSSpinLockLock(&loopsLock);
if (!loopsDic) {
// 第一次进入时,初始化全局Dic,并先为主线程创建一个 RunLoop。
loopsDic = CFDictionaryCreateMutable();
CFRunLoopRef mainLoop = _CFRunLoopCreate();
CFDictionarySetValue(loopsDic, pthread_main_thread_np(), mainLoop);
}
/// 直接从 Dictionary 里获取。
CFRunLoopRef loop = CFDictionaryGetValue(loopsDic, thread));
if (!loop) {
/// 取不到时,创建一个
loop = _CFRunLoopCreate();
CFDictionarySetValue(loopsDic, thread, loop);
/// 注册一个回调,当线程销毁时,顺便也销毁其对应的 RunLoop。
_CFSetTSD(..., thread, loop, __CFFinalizeRunLoop);
}
OSSpinLockUnLock(&loopsLock);
return loop;
}
CFRunLoopRef CFRunLoopGetMain() {
return _CFRunLoopGet(pthread_main_thread_np());
}
CFRunLoopRef CFRunLoopGetCurrent() {
return _CFRunLoopGet(pthread_self());
}
从上面的代码我们可以看出RunLoop和线程被放在了一个全局的字典中,并且以键值对的形式存放.所以线程和RunLoop肯定是一一对应的.(RunLoop可以嵌套,但是主RunLoop只有一个).当一个线程被创建出来后,RunLoop是不存在的,当你第一次调用CurrentRunLoop时,才会创建对应的RunLoop.主线程的RunLoop在最开始就生成了.RunLoop的销毁发生在线程结束时.除了主线程的RunLoop外,子线程的RunLoop你只能在子线程的内部通过currentRunLoop来获取.
三,RunLoop的组成
RunLoop必定对应一个线程.但是要想开启一个RunLoop.必须给RunLoop一个Mode.其中Mode中又包含了Source,Timer,Observer.Source,Timer,Observer都是集合.一个Mode必须含有Source,Timer,Observer.的其中之一.
一个 RunLoop 包含若干个 Mode,每个 Mode 又包含若干个 Source/Timer/Observer。每次调用 RunLoop 的主函数时,只能指定其中一个 Mode,这个Mode被称作 CurrentMode。如果需要切换 Mode,只能退出 Loop,再重新指定一个 Mode 进入。这样做主要是为了分隔开不同组的 Source/Timer/Observer,让其互不影响。
CFRunLoopSourceRef 是事件产生的地方。Source有两个版本:Source0 和 Source1。
• Source0 只包含了一个回调(函数指针),它并不能主动触发事件。使用时,你需要先调用 CFRunLoopSourceSignal(source),将这个 Source 标记为待处理,然后手动调用 CFRunLoopWakeUp(runloop) 来唤醒 RunLoop,让其处理这个事件。
• Source1 包含了一个 mach_port 和一个回调(函数指针),被用于通过内核和其他线程相互发送消息。这种 Source 能主动唤醒 RunLoop 的线程,其原理在下面会讲到。
CFRunLoopTimerRef 是基于时间的触发器,它和 NSTimer 是toll-free bridged 的,可以混用。其包含一个时间长度和一个回调(函数指针)。当其加入到 RunLoop 时,RunLoop会注册对应的时间点,当时间点到时,RunLoop会被唤醒以执行那个回调。
CFRunLoopObserverRef 是观察者,每个 Observer 都包含了一个回调(函数指针),当 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
};
上面的 Source/Timer/Observer 被统称为 mode item,一个 item 可以被同时加入多个 mode。但一个 item 被重复加入同一个 mode 时是不会有效果的。如果一个 mode 中一个 item 都没有,则 RunLoop 会直接退出,不进入循环。
四,RunLoop和NSTimer,performSelector以及CADisplayLink的关系.
(1),NSTimer
NSTimer的执行必须依赖RunLoop.因为NSTimer对象必须被放在一个RunLoop中,定时器才会运行.其实NSTimer就是CFRunLoopTimerRef.在定时器被加入到RunLoop中后,RunLoop会在定时器指定的时间点注册好事件.例如,5s,10s,15s这几个时间点.RunLoop为了节省资源,可能并不会在准确的时间点调用之前注册好的事件.NSTimer类中有一个属性tolerance.这个是定时器的允许误差.如果当期现场堵塞,一直到允许误差结束后,还没有调用定时器事件,那么这次事件就会被跳过.所以往RunLoop中添加定时器,其实就是往RunLoop中添加了一个CFRunLoopTimerRef对象.这里需要注意的是[timer fire]是source0事件,不是CFRunLoopTimerRef.你可以打断点后,在Xcode的栈中看到
(2)performSelector
当调用 NSObject 的 performSelecter:afterDelay: 后,实际上其内部会创建一个 Timer 并添加到当前线程的 RunLoop 中。所以如果当前线程没有 RunLoop,则这个方法会失效。
当调用 performSelector:onThread: 时,实际上其会创建一个 Timer 加到对应的线程去,同样的,如果对应线程没有 RunLoop 该方法也会失效。
(3)CADisplayLink
CADisplayLink 是一个和屏幕刷新率一致的定时器(但实际实现原理更复杂,和 NSTimer 并不一样,其内部实际是操作了一个 Source)。如果在两次屏幕刷新之间执行了一个长任务,那其中就会有一帧被跳过去(和 NSTimer 相似),造成界面卡顿的感觉。在快速滑动TableView时,即使一帧的卡顿也会让用户有所察觉。Facebook 开源的 AsyncDisplayLink 就是为了解决界面卡顿的问题,其内部也用到了 RunLoop.