Runloop 是和线程紧密相关的一个基础组件,是不少线程有关功能的幕后功臣。尽管在日常使用中几乎不太会直接用到,理解 Runloop 有利于咱们更加深刻地理解 iOS 的多线程模型。bash
Runloop 是什么?Runloop 仍是比较顾名思义的一个东西,说白了就是一种循环,只不过它这种循环比较高级。通常的 while 循环会致使 CPU 进入忙等待状态,而 Runloop 则是一种“闲”等待,这部分能够类比 Linux 下的 epoll。当没有事件时,Runloop 会进入休眠状态,有事件发生时, Runloop 会去找对应的 Handler 处理事件。Runloop 可让线程在须要作事的时候忙起来,不须要的话就让线程休眠。 盗一张苹果官方文档的图,也是几乎每一个讲 Runloop 的文章都会引用的图,大致说明了 Runloop 的工做模式: 盗一张苹果官方文档的图,也是几乎每一个讲 Runloop 的文章都会引用的图,大致说明了 Runloop 的工做模式:多线程
Runloop 和线程是绑定在一块儿的。每一个线程(包括主线程)都有一个对应的 Runloop 对象。咱们并不能本身建立 Runloop 对象,可是能够获取到系统提供的 Runloop 对象。函数
主线程的 Runloop 会在应用启动的时候完成启动,其余线程的 Runloop 默认并不会启动,须要咱们手动启动。oop
这两个都是 Runloop 事件的来源,其中 Input Source 又能够分为三类spa
Timer Source 顾名思义就是指定时器事件了。线程
在 CoreFoundation 里面关于 RunLoop 有5个类: CFRunLoopRef CFRunLoopModeRef CFRunLoopSourceRef CFRunLoopTimerRef CFRunLoopObserverRef指针
其中 CFRunLoopModeRef 类并无对外暴露,只是经过 CFRunLoopRef 的接口进行了封装。他们的关系以下:code
CFRunLoopTimerRef 是基于时间的触发器,它和 NSTimer 是toll-free bridged 的,能够混用。其包含一个时间长度和一个回调(函数指针)。当其加入到 RunLoop 时,RunLoop会注册对应的时间点,当时间点到时,RunLoop会被唤醒以执行那个回调。orm
CFRunLoopSourceRef 是事件产生的地方。Source有两个版本:Source0 和 Source1。 • Source0 只包含了一个回调(函数指针),它并不能主动触发事件。使用时,你须要先调用 CFRunLoopSourceSignal(source),将这个 Source 标记为待处理,而后手动调用 CFRunLoopWakeUp(runloop) 来唤醒 RunLoop,让其处理这个事件。 • Source1 包含了一个 mach_port 和一个回调(函数指针),被用于经过内核和其余线程相互发送消息。这种 Source 能主动唤醒 RunLoop 的线程,其原理在下面会讲到。cdn
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 会直接退出,不进入循环。
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 了。
当在操做 UI 时,好比改变了 Frame、更新了 UIView/CALayer 的层次时,或者手动调用了 UIView/CALayer 的 setNeedsLayout/setNeedsDisplay方法后,这个 UIView/CALayer 就被标记为待处理,并被提交到一个全局的容器去。
苹果注册了一个 Observer 监听 BeforeWaiting(即将进入休眠) 和 Exit (即将退出Loop) 事件,回调去执行一个很长的函数: _ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv()。这个函数里会遍历全部待处理的 UIView/CAlayer 以执行实际的绘制和调整,并更新 UI 界面。
NSTimer 其实就是 CFRunLoopTimerRef,他们之间是 toll-free bridged 的。一个 NSTimer 注册到 RunLoop 后,RunLoop 会为其重复的时间点注册好事件。例如 10:00, 10:10, 10:20 这几个时间点。RunLoop为了节省资源,并不会在很是准确的时间点回调这个Timer。Timer 有个属性叫作 Tolerance (宽容度),标示了当时间点到后,允许有多少最大偏差。
若是某个时间点被错过了,例如执行了一个很长的任务,则那个时间点的回调也会跳过去,不会延后执行。就好比等公交,若是 10:10 时我忙着玩手机错过了那个点的公交,那我只能等 10:20 这一趟了。
CADisplayLink 是一个和屏幕刷新率一致的定时器(但实际实现原理更复杂,和 NSTimer 并不同,其内部实际是操做了一个 Source)。若是在两次屏幕刷新之间执行了一个长任务,那其中就会有一帧被跳过去(和 NSTimer 类似),形成界面卡顿的感受。在快速滑动TableView时,即便一帧的卡顿也会让用户有所察觉。Facebook 开源的 AsyncDisplayLink 就是为了解决界面卡顿的问题,其内部也用到了 RunLoop,这个稍后我会再单独写一页博客来分析。
当调用 NSObject 的 performSelecter:afterDelay: 后,实际上其内部会建立一个 Timer 并添加到当前线程的 RunLoop 中。因此若是当前线程没有 RunLoop,则这个方法会失效。
当调用 performSelector:onThread: 时,实际上其会建立一个 Timer 加到对应的线程去,一样的,若是对应线程没有 RunLoop 该方法也会失效。