NSRunLoop 是什么?
在 Cocoa 中,每个线程( NSThread
)对象中内部都有一个 RunLoop( NSRunLoop
) 对象用来循环处理输入事件.
NSRunloop 并不真的是一个 loop,在 Apple 的文档中也提到了需要自己写 while 或者 for 语句来实现,类似下面:
1 | while(running){ |
何为 RunLoop 事件源
从字面翻译来看,RunLoop 就是一个运行循环,的确它就是一个处理输入时间的运行循环,为什么需要这样处理,难道没有事件发生的时候让线程空转浪费资源?很明显在有事件发生的时候唤醒线程,没有事件发生的时候让其 sleep 更好。
下面我还是拿这张百看不厌的图来说事:
处理的事件包括两类
- 来自 Timer sources 的同步事件
- 来自 Input sources 的异步事件
- Time Source. Timer sources deliver synchronous events, occurring at a scheduled time or repeating interval. 苹果文档中有句话需要注意,
Timer sources deliver events to their handler routines but do not cause the RunLoop to exit.*
创建 NSTimer 添加到 RunLoop 中的时候,这里需要注意的是, NSTimer
默认是处于 NSDefaultRunloopMode
,这也就可以解释为什么如果你在你的控制器中添加了一个 timer 定时刷新你的界面,而你在拖动视图的时候 timer 不回 fire,因为这个时候你的 runloop 是 NSEventTrackingRunloopMode
,在这个 mode 下 timer 不会 fire。
- input source input source 主要是一些异步的事件,比如来自其它线程或者其它 app 的消息。
input source 传递异步事件到其对应的处理函数,并且使 runUntilDate(与线程相关联的 RunLoop 对象调用)返回
为了能够处理 input source, RunLoop 产生 notifications。通过注册成 RunLoop observers 可以接受到这些通知(通过 Core Foundation 来注册 observers)。
RunLoopMode 有哪些?
RunLoop 在处理输入事件时会产生通知,可以通过 Core Foundation 向线程中添加 RunLoop observers 来监听特定事件,以在监听的事件发生时做附加的处理工作。
每个 RunLoop 可运行在不同的模式下,一个 RunLoop mode 是一个集合,其中包含其监听的若干输入事件源,定时器,以及在事件发生时需要通知的 RunLoop observers。运行在一种 mode 下的 RunLoop 只会处理其 RunLoop mode 中包含的输入源事件、定时器事件、以及通知 RunLoop mode 中包含的 observers。
Cocoa 中的预定义模式有:
- Default 模式
- 定义 NSDefaultRunLoopMode (Cocoa) kCFRunLoopDefaultMode (Core Foundation)
- 描述:默认模式中几乎包含了所有输入源(NSConnection 除外),一般情况下应使用此模式,这是最常用的 RunLoop mode。
- Connection 模式
- 定义:
NSConnectionReplyMode
(Cocoa) - 描述:处理
NSConnection
对象相关事件,系统内部使用,这个 mode 表明NSConnection
对象等待 reply,用户基本不会使用。
- 定义:
- Modal 模式
- 定义:
NSModalPanelRunLoopMode
(Cocoa) - 描述:处理 modal panels 事件,需要等待处理的 input source 为 modal panel 时设置,比如 NSSavePanel 和 NSOpenPanel。
- 定义:
- Event tracking 模式
- 定义: UITrackingRunLoopMode (iOS)
NSEventTrackingRunLoopMode
(cocoa) - 描述:使用该模式来处理用户界面相关的事件,例如在拖动 loop 或其他 user interface tracking loops 时处于此种模式下,在此模式下会限制输入事件的处理。例如,当手指按住 UITableView 拖动时就会处于此模式。
- 定义: UITrackingRunLoopMode (iOS)
- Common 模式
- 定义: NSRunLoopCommonModes (Cocoa) kCFRunLoopCommonModes (Core Foundation)
- 描述:这是一个伪模式,其为一组 RunLoop mode 的集合,将输入源加入此模式意味着在 Common Modes 中包含的所有模式下都可以处理。在 Cocoa 应用程序中,默认情况下 Common Modes 包含 default modes,modal modes,event Tracking modes, 可使用 CFRunLoopAddCommonMode 方法向 Common Modes 中添加自定义 modes。
注意这个并不是一个特定的 mode,而是一个 mode 的集合,而 runloop 必须运行在一个特定的 mode 下 。
获取当前线程的 runloop mode
1 | NSString *runLoopMode = [[NSRunLoop currentRunLoop] currentMode]; |
NSTimer、NSURLConnection 与 UITrackingRunLoopMode
NSTimer 与 NSURLConnection 默认运行在 default mode 下,这样当用户在拖动 UITableView 处于 UITrackingRunLoopMode 模式时,NSTimer 不能 fire,NSURLConnection 的数据也无法处理。
NSTimer 的例子: 在一个 UITableViewController 中启动一个 0.2s 的循环定时器,在定时器到期时更新一个计数器,并显示在 label 上。
1 | - (void)viewDidLoad { |
在正常情况下,可看到每隔 0.2s,label 上显示的数字 +1,但当你拖动或按住 tableView 时,label 上的数字不再更新,当你手指离开时,label 上的数字继续更新。当你拖动 UItableView
时,当前线程 RunLoop 处于 UIEventTrackingRunLoopMode
模式,在这种模式下,不处理定时器事件,即定时器无法 fire,label 上的数字也就无法更新。 解决方法,一种方法是在另外的线程中处理定时器事件,可把 Timer 加入到 NSOperation
中在另一个线程中调度;还有一种方法时修改 Timer 运行的 RunLoop 模式,将其加入到 UITrackingRunLoopMode
模式或 NSRunLoopCommonModes
模式中。 即
1 | [[NSRunLoop currentRunLoop] addTimer:timer forMode:UITrackingRunLoopMode]; |
或
1 | [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes]; |
另外一种是放到 NSThread
中
1 | - (void)viewDidLoad{ |
NSURLConnection
也是如此,见 SDWebImage 中的描述,以及 SDWebImageDownloader.m 代码中的实现。修改 NSURLConnection 的运行模式可使用 scheduleInRunLoop:forMode: 方法。
1 | NSURL *url = [NSURL URLWithString:@"https://www.baidu.com"]; |
参考资料:
- Threading Programming Guide – RunLoops
- NSRunLoop Class Reference
- NSURLConnection Class Reference
- NSTimer Class Reference
- CFRunLoop wiki
- SDWebImage
- TestButtonDown
- 关于 NSRunloop 的学习和理解
备注:欢迎转载,但请一定注明出处! http://blog.wangruofeng007.com