RunLoop 学习总结
开始
很久之前就看了一次 YY 的文章,没看懂。后来又看了 sunny 的视频和叶孤城直播的视频,找了很多资料,对 RunLoop 也越来越清晰。然后又看了两三次 YY 的文章,虽然还没完全看懂,不得不说写得非常好,帮助很大。还有官方文档,也学到了很多东西。还有就是 GitHub 上的一些 demo,文中一些代码别人写过了,我就直接拿过来了。文中一些结论也是取自大神的文章或者视频。非常感谢这些前辈大神们的分享吧!!
什么是RunLoop
RunLoop 其实就是一种处理事件、消息的机制被面向对象化,它就是一个对象。它的本质就是一个 do-while 循环。
1 | do { |
NSRunLoop 和 CFRunLoop
NSRunLoop 是对 CFRunLoop 的封装,使之更面向对象化。我们一般在 NSRunLoop 层面上操作。CFRunLoop 基于 C 语言,从属于 CoreFoundation(开源)。
官方文档里对 CFRunLoop 的解释很好,它建立任务来监测输入源(事件源,事件/消息),并在准备好处理输入源的时候进行调度管理。输入源包括用户输入的设备、网络连接、周期性或者延迟的事件,还有异步回调等。
RunLoop的结构组成
一个 RunLoop 可以监测三个对象,Sources、Timers、Observers,如图。

图来自YY的文章,这个图很棒
其实是一个 RunLoop 对象可以包含一个或者多个 Mode,这个 Mode 可以说是一种模式,一种配置。而 Mode 又包含着输入源(事件源)Source、观察者 Observer、计时器 Timer 和其他一些信息。
CFRunLoopMode
Mode 决定了 RunLoop 在什么时候处理什么事情,在某个 Mode 某个时候能做什么、不能做什么。
切换 Mode 需要先退出 RunLoop 再重新进入。
Mode 有几种:
- DefaultMode,
CFRunLoop的是kCFRunLoopDefaultMode,NSRunLoop的是NSDefaultRunLoopMode,这是默认的 mode,即 APP 平常的状态 - UITrackingRunLoopMode,追踪 scrollView 滑动的状态,当有 scrollView 滑动的时候 RunLoop 会切换到该 mode 下,滑动结束再切换到其他 mode
- UIInitializationRunLoopMode,这个是私有的,在 APP 启动时到显示第一个界面期间会进入这个 mode
- NSRunLoopCommonModes,这个是 Mode 集合,自定义或者若干个集合,默认是包含了 defaultMode 和 trackingMode
- GSEventReceiveRunLoopMode: 接受系统事件的内部 Mode,通常用不到。
这里先说一个例子,例如我们创建的 NSTimer 对象在运行时碰到有 scrollView 的控件滑动的时候,默认是不会继续计时的。
1 | [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(tiktok) userInfo:nil repeats:YES]; |

那是因为用 scheduleTime… 这个方法,timer 默认被添加到了 RunLoop 的 defaultMode,而滑动 tableView 的时候 RunLoop 切换到了 trackingMode,所以停止了。
解决方法就是把 timer 添加到当前 RunLoop 的 UITrackingRunLoopMode 或者 NSRunLoopCommonModes。
1 | //只要添加一句 |

Mode的结构
CFRunLoopMode 是一个 struct,最主要是 Source、Timer 和 Observer,这些被称为 mode item。还有一些其他信息,例如 name,就是指上面的 defaultMode 或者 trackingMode 的名字,还有一些线程锁标志信息等。
1 | struct __CFRunLoopMode { |
CFRunLoopSource
输入源(input source)是指用户的操作事件(例如点击屏幕)或者网络端口接收到的信息等。而一个 CFRunLoopSource 对象就是跑在 RunLoop 上输入源的抽象概念,Source 可以说是用户操作的事件、消息和 RunLoop 的中间层。
有两种 Source:
- Source0,被 APP 所管理,处理内部事件,如 UIEvent、CFSocket
- Source1,被 RunLoop 和内核所管理,由 Mach port 驱动,如 CFMachPort、CFMessagePort
这里已经涉及到很底层的东西了,查了下 Mach port,很底层,太少资料,大概了解到一点是用来通信的通道、端口,这个还有待研究。
CFRunLoopObserver
这个就是观察者,用来接收各种回调(callbacks)。一个 Observer 一次只能被一个 RunLoop 注册。当 RunLoop 的状态切换的时候,Observer 可以观察到。
这个是 RunLoop 的状态:
1 | typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) { |
然后我们来测试一下,创建一个 Observer 来观察 RunLoop 的状态。
1 | //创建一个Observer,观察RunLoop的所有状态 |
然后查看打印。

后面的数字即刚才 RunLoop 的状态,对比 struct 可以看出,APP 一运行就先进入 RunLoop,然后 2/4 切换,即处理 Source 和 Timer,然后是 32,即将进入睡眠状态,再 64,即将从睡眠状态中醒来。然后循环两三次到最后 32,即将进入睡眠状态,这就是一个 APP 打开的时候还没给任何动作。
接下来我滑动一下 tableView,来看一下打印。

先醒过来,处理 Source 和 Timer,然后有个 128(即对应上面的 2^7),即将退出 RunLoop,这是意料之中,因为我滑动 tableView 把 mode 切换到了 trackingMode,而切换 Mode 的时候需要先退出 mode 再重新进入,等松开手后最终状态又变成 32,睡眠状态。
这也很符合刚开始的 do-while 循环,一有事件/消息处理就处理,不然就进入睡眠。
RunLoop和线程的关系
线程和 RunLoop 之间是一一对应的,其关系是保存在一个全局的 Dictionary 里。线程刚创建时并没有 RunLoop,如果你不主动获取,那它一直都不会有。RunLoop 的创建是发生在第一次获取时,RunLoop 的销毁是发生在线程结束时。你只能在一个线程的内部获取其 RunLoop(主线程除外)。(直接取自 YY 的结论)
RunLoop和Autorelease Pool
UIKit 通过 RunLoopObserver 在 RunLoop 两次 Sleep 间对 Autorelease Pool 进行 Pop 和 Push,将这次 Loop 中产生的 Autorelease 对象释放。
应用
tableView滑动后再加载图片
当一个 tableView 或者 collectionView(这两个都是继承自 scrollView)里面放了很多图片并且要加载的时候,有一个优化措施就是在滑动的时候不加载,在滑动完之后再开始加载。
1 | UIImage *downloadedImage = ...; |
(以上代码取自sunny的视频)
让 setImage 方法只在 NSDefaultRunLoopMode 里面运行,在滑动的时候进入 trackingMode 就不执行。
还有更具体的实现可以看下这个 GitHub demo。
虽然可以这样实现,但是我觉得实用性不强,我看了很多较常用的 APP 都没有这样实现,且当学习吧。
常驻线程
1 | [[NSRunLoop currentRunLoop] addPort:[NSPort port] forMode:NSDefaultRunLoopMode]; |
或者
1 | [[NSRunLoop currentRunLoop] runUntilDate:[NSDate distantFuture]]; |
监测滑动卡顿
前面说到 RunLoop 会一直切换状态,我们要监测的是在处理 Source 的时候的时间,如果时间太长,则说明有可能卡顿了。
只需要另外再开启一个线程,实时计算这两个状态区域之间的耗时是否到达某个阈值,便能揪出这些性能杀手。为了让计算更精确,需要让子线程更及时地获知主线程 NSRunLoop 状态变化,所以 dispatch_semaphore_t 是个不错的选择。另外卡顿需要覆盖到多次连续小卡顿和单次长时间卡顿两种情景,所以判定条件也需要做适当优化。将上面两个方法添加计算的逻辑如下:
1 | static void runLoopObserverCallBack(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info) |
以上代码取自 https://github.com/suifengqjn/PerformanceMonitor
最后
写得有点糟,只是一个小总结,感觉还是有好多不懂,还是要深入学习。