Runloop简单理解

Runloop 是和线程紧密相关的一个基础组件,是不少线程有关功能的幕后功臣。尽管在日常使用中几乎不太会直接用到,理解 Runloop 有利于咱们更加深刻地理解 iOS 的多线程模型。bash

Runloop 基本概念

Runloop 是什么?Runloop 仍是比较顾名思义的一个东西,说白了就是一种循环,只不过它这种循环比较高级。通常的 while 循环会致使 CPU 进入忙等待状态,而 Runloop 则是一种“闲”等待,这部分能够类比 Linux 下的 epoll。当没有事件时,Runloop 会进入休眠状态,有事件发生时, Runloop 会去找对应的 Handler 处理事件。Runloop 可让线程在须要作事的时候忙起来,不须要的话就让线程休眠。 盗一张苹果官方文档的图,也是几乎每一个讲 Runloop 的文章都会引用的图,大致说明了 Runloop 的工做模式: 盗一张苹果官方文档的图,也是几乎每一个讲 Runloop 的文章都会引用的图,大致说明了 Runloop 的工做模式:多线程

图中展示了 Runloop 在线程中的做用:从 input source 和 timer source 接受事件,而后在线程中处理事件。

Runloop 与线程

Runloop 和线程是绑定在一块儿的。每一个线程(包括主线程)都有一个对应的 Runloop 对象。咱们并不能本身建立 Runloop 对象,可是能够获取到系统提供的 Runloop 对象。函数

主线程的 Runloop 会在应用启动的时候完成启动,其余线程的 Runloop 默认并不会启动,须要咱们手动启动。oop

Input Source 和 Timer Source

这两个都是 Runloop 事件的来源,其中 Input Source 又能够分为三类spa

  • Port-Based Sources,系统底层的 Port 事件,例如 CFSocketRef ,在应用层基本用不到
  • Custom Input Sources,用户手动建立的 Source
  • Cocoa Perform Selector Sources, Cocoa 提供的 performSelector 系列方法,也是一种事件源

Timer Source 顾名思义就是指定时器事件了。线程

RunLoop 对外的接口

在 CoreFoundation 里面关于 RunLoop 有5个类: CFRunLoopRef CFRunLoopModeRef CFRunLoopSourceRef CFRunLoopTimerRef CFRunLoopObserverRef指针

其中 CFRunLoopModeRef 类并无对外暴露,只是经过 CFRunLoopRef 的接口进行了封装。他们的关系以下:code

一个 RunLoop 包含若干个 Mode,每一个 Mode 又包含若干个 Source/Timer/Observer。每次调用 RunLoop 的主函数时,只能指定其中一个 Mode,这个Mode被称做 CurrentMode。若是须要切换 Mode,只能退出 Loop,再从新指定一个 Mode 进入。这样作主要是为了分隔开不一样组的 Source/Timer/Observer,让其互不影响。

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 会直接退出,不进入循环。

AutoreleasePool

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,这个稍后我会再单独写一页博客来分析。

performSelecter:afterDelay:

当调用 NSObject 的 performSelecter:afterDelay: 后,实际上其内部会建立一个 Timer 并添加到当前线程的 RunLoop 中。因此若是当前线程没有 RunLoop,则这个方法会失效。

当调用 performSelector:onThread: 时,实际上其会建立一个 Timer 加到对应的线程去,一样的,若是对应线程没有 RunLoop 该方法也会失效。

相关文章
相关标签/搜索