RunLoop
RunLoop 运行循环,在程序运行过程中循环做一些事情。
应用范畴
- 1、定时器(Timer)、PerformSelector
 - 2、GCD
 - 3、事件响应、手势识别、界面刷新
 - 4、网络请求
 - 5、AutoreleasePool
 
概念介绍
在我们命令行项目的main函数里面
1  | int main(int argc, const char * argv[]) {  | 
执行完NSLog(@"Hello, World!");这个代码以后,程序立即退出,但是在我们的正常项目main函数里面
1  | int main(int argc, char * argv[]) {  | 
 如果用一个伪代码来简单的解释一下上面代码的意思,就是
 1
2
3
4
5
6
7
8
9
10
11int main(int argc, char * argv[]) {
  @autoreleasepool {
    int retVal = 0;
    do {
      //休眠等待消息
      int message = sleep_and_wait();
      //处理消息
      retVal = process_message(message)
    } while (0 == retVal);
  }
}
 程序不会马上退出,而是保持运行状态,RunLoop的基本作用:
- 1、保持程序持续的运行
 - 2、处理app中的各种事件(比如触摸事件,定时器事件)
 - 3、节省CPU资源,提高程序性能,该做事的时候做事,改休息的时候休息
 
RunLoop对象
iOS中有2套API来访问和使用RunLoop
- 1、Fundataion:NSRunLoop
 - 2、Core Fundataion:CFRunLoop
 
NSRunLoop是基于CFRunLoop的一层OC包装,CFRunLoop是开源的,地址:https://opensource.apple.com/tarballs/CF/
RunLoop与线程
- 1、每一条线程都有唯一的一个与之对应的RunLoop对象
 - 2、RunLoop保存在一个全局的Dictionary里,线程作为Key,RunLoop作为Value
 - 3、线程刚创建时,并没有RunLoop对象,RunLoop会在第一次获取她时创建
 - 4、RunLoop会在线程结束的时候销毁
 - 5、主线程的RunLoop已经自动获取(创建),子线程默认没有开启RunLoop
 
获取RunLoop对象
- 1、Fundation
- 1、获取当前线程的RunLoop对象
[NSRunLoop currentRunLoop] - 2、获取主线程的RunLoop对象
[NSRunLoop mainRunLoop] 
 - 1、获取当前线程的RunLoop对象
 - 2、Core Foundation
- 1、获取当前线程的RunLoop对象
CFRunLoopGetCurrent() - 2、获取主线程的RunLoop对象
CFRunLoopGetMain() 
 - 1、获取当前线程的RunLoop对象
 
RunLoop相关类
Core Foundation中关于RunLoop一共有5个类
- 1、CFRunLoopRef
 - 2、CFRunLoopModeRef
 - 3、CFRunLoopSourceRef
 - 4、CFRunLoopTimerRef
 - 5、CFRunLoopObserverRef
 
我们下载RunLoop,然后搜索CFRunLoop的组成
1  | struct __CFRunLoop {  | 
我们打印一下NSLog(@"%@",[NSRunLoop currentRunLoop]);
其中主要的有下面几个:
- 1、
_pthread记录当前线程 - 2、
_commonModes - 3、
_commonModeItems - 4、
_currentMode,当前mode类型 - 5、
_modes存放CFRunLoop里面的所有mode 
我们在RunLoop源码中搜索CFRunLoopMode来查看一下CFRunLoopMode都存放了哪些东西:
- 1、Source0:
- 处理触摸事件,
 - performSelector:onThread:
 
 - 2、Source1:
- 基于Port的线程间通信,
 - 系统事件的捕捉
 
 - 3、Timer :
- NSTimer,
 - performSelector:withObject:afterDelay:
 
 - 4、Observers:
- 用于监听RunLoop的状态
 - UI刷新
 - Autorelease pool(BeforeWaiting)
 
 
我们来简单的证明一下Source0,我们随便写一个touchesBegan触摸事件,然后在里面打一个断点,bt指令就是打印线程执行的所有方法
我们可以在线程执行方法中可以发现,在调用RunLoop相关方法的时候,第一个是调用的__CFRunLoopDoSources0
RunLoop里面会有多个Mode,但是只有一个_currentMode
CFRunLoopModeRef
- 1、CFRunLoopModeRef代表着RunLoop的运行模式
 - 2、一个RunLoop包含若干个
Mode,每个Mode又包含若干个Source0/Source1/Timer/Observer - 3、RunLoop启动的时候只能选择其中一个Mode作为currentMode
 - 4、如果要切换Mode,只能退出当前Loop,再重新选择一个Mode进入,不同组的
Source0/Source1/Timer/Observer互不影响 - 5、如果Mode里面没有任何
Source0/Source1/Timer/Observer,RunLoop会立刻退出 
CFRunLoopModeRef常见的Mode
- 1、KCFRunLoopDefaultMode(NSDefaultRunLoopMode):App的默认Mode,通常主线程是在这个Mode下运行的
 - 2、UITrackingRunLoopMode:界面跟踪 Mode,用于 ScrollView 追踪触摸滑动,保证界面滑动时不受其他 Mode 影响
 
获取当前Mode
1  | CFRunLoopMode mode = CFRunLoopCopyCurrentMode(CFRunLoopGetCurrent());  | 
CFRunLoopObserverRef
RunLoop的几种状态
1  | /* Run Loop Observer Activities */  | 
我们可以添加一个Observer监听RunLoop的所有状态,代码如下
1  | // 创建Observer  | 
我们运行上面代码,然后查看打印结果:
在没有任何事件处理的情况下,最终RunLoop的活动状态为kCFRunLoopBeforeWaiting即将进入休眠。
既然我们可以监听到了RunLoop的Mode变化情况,那么我们就可以打印一下KCFRunLoopDefaultMode和UITrackingRunLoopMode的切换情况了。
我们在view上随便拉一个UITextView,然后滚动UITextView
监听代码
1  | //创建observer  | 
打印结果为:
- 1、在刚开始滚动
UITextView的时候,先退出kCFRunLoopDefaultMode,所以默认应该就是kCFRunLoopDefaultMode - 2、在滚动中,进入
UITrackingRunLoopMode - 3、在滚动结束,先退出
UITrackingRunLoopMode,然后在进入kCFRunLoopDefaultMode 
RunLoop的运行逻辑:
每次运行RunLoop,线程的RunLoop会自动处理之前未处理的消息,并通知相关的观察者。具体顺序
- 1、通知观察者(observers)RunLoop即将启动
 - 2、通知观察者(observers)任何即将要开始的定时器
 - 3、通知观察者(observers)即将处理source0事件
 - 4、处理source0
 - 5、如果有source1,跳到第9步
 - 6、通知观察者(observers)线程即将进入休眠
 - 7、将线程置于休眠知道任一下面的事件发生
- 1、source0事件触发
 - 2、定时器启动
 - 3、外部手动唤醒
 
 - 8、通知观察者(observers)线程即将唤醒
 - 9、处理唤醒时收到的时间,之后跳回2
- 1、如果用户定义的定时器启动,处理定时器事件
 - 2、如果source0启动,传递相应的消息
 
 - 10、通知观察者RunLoop结束
 
RunLoop休眠原理
在RunLoop即将休眠的时候,通过mach_msg()方法来让软件和硬件交互
- 1、即将休眠的时候,程序调用
mach_msg()传递给CPU,告诉CPU停止运行 - 2、即将启动RunLoop的时候,程序调用
mach_msg()传递给CPU,告诉CPU开始工作 
RunLoop简单应用
滚动视图上面NSTimer不失效
我们写一个简单的定时器,然后视图上面创建一个TextView,然后滚动TextView
1  | static int count = 0;  | 
我们观察可以发现在打印的第二秒和第三秒之间其实相差了14s,因为一个线程只会有一个RunLoop,默认情况下是kCFRunLoopDefaultMode,在滚动UITextView的时候,RunLoop切换到了UITrackingRunLoopMode,这个时候定时器就会停止,在滚动UITextView结束的时候,RunLoop切换到了kCFRunLoopDefaultMode,定时器继续开始启动了。
解决这个问题的方法就是把这个NSTimer添加到两种RunLoop中
- 1、
1
2[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
[[NSRunLoop currentRunLoop] addTimer:timer forMode:UITrackingRunLoopMode]; - 2、还有一个NSRunLoopCommonModes,我们用
NSRunLoopCommonModes标记的时候,就可以实现上面效果NSRunLoopCommonModes并不是一个真的模式,它只是一个标记,timer能在_commonModes数组中存放的模式下工作1
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
 
线程保活(常驻线程)
开始之前先介绍几个概念
- 1、线程刚创建时,并没有RunLoop对象,RunLoop会在第一次获取她时创建
- 1、获取线程:[NSRunLoop currentRunLoop]
 - 2、获取线程:CFRunLoopGetCurrent()
 
 - 2、启动RunLoop的三种方法
- 1、
- (void)run;,
这种方法runloop会一直运行下去,在此期间会处理来自输入源的数据,并且会在NSDefaultRunLoopMode模式下重复调用runMode:beforeDate:方法; - 2、
- (void)runUntilDate:(NSDate *)limitDate;
可以设置超时时间,在超时时间到达之前,runloop会一直运行,在此期间runloop会处理来自输入源的数据,并且也会在NSDefaultRunLoopMode模式下重复调用runMode:beforeDate:方法; - 3、
- (void)runMode:(NSString *)mode beforeDate:(NSDate *)limitDate;
runloop会运行一次,超时时间到达或者第一个input source被处理,则runloop就会退出 
 - 1、
 - 3、退出RunLoop的方式
- 1、启动方式的退出方法,如果runloop没有input sources或者附加的timer,runloop就会退出。
 - 2、启动方式runUntilDate,可以通过设置超时时间来退出runloop。
 - 3、启动方式runMode:beforeDate,通过这种方式启动,runloop会运行一次,当超时时间到达或者第一个输入源被处理,runloop就会退出。
 
 
如果我们想控制runloop的退出时机,而不是在处理完一个输入源事件之后就退出,那么就要重复调用runMode:beforeDate:,
具体可以参考苹果文档给出的方案,如下:
1  | NSRunLoop *myLoop = [NSRunLoop currentRunLoop];  | 
1  | //关闭runloop的地方  | 
面试题
1、讲讲RunLoop项目中有用到吗?
- 1、定时器切换的时候,为了保证定时器的准确性,需要添加runLoop
 - 2、在聊天界面,我们需要持续的把聊天信息存到数据库中,这个时候需要开启一个保活线程,在这个线程中处理
 
2、RunLoop内部实现逻辑?
- 1、通知观察者(observers)RunLoop即将启动
 - 2、通知观察者(observers)任何即将要开始的定时器
 - 3、通知观察者(observers)即将处理source0事件
 - 4、处理source0
 - 5、如果有source1,跳到第9步
 - 6、通知观察者(observers)线程即将进入休眠
 - 7、将线程置于休眠知道任一下面的事件发生
 - 1、source0事件触发
 - 2、定时器启动
 - 3、外部手动唤醒
 - 8、通知观察者(observers)线程即将唤醒
 - 9、处理唤醒时收到的时间,之后跳回2
 - 1、如果用户定义的定时器启动,处理定时器事件
 - 2、如果source0启动,传递相应的消息
 - 10、通知观察者RunLoop结束
 
3、RunLoop和线程的关系?
- 1、每一条线程都有唯一的一个与之对应的RunLoop对象
 - 2、RunLoop保存在一个全局的Dictionary里,线程作为Key,RunLoop作为Value
 - 3、线程刚创建时,并没有RunLoop对象,RunLoop会在第一次获取她时创建
 - 4、RunLoop会在线程结束的时候销毁
 - 5、主线程的RunLoop已经自动获取(创建),子线程默认没有开启RunLoop
 
4、RunLoop有几种状态
kCFRunLoopEntry = (1UL << 0),   //   即将进入RunLoop
kCFRunLoopBeforeTimers = (1UL << 1), // 即将处理Timer
kCFRunLoopBeforeSources = (1UL << 2), // 即将处理Source
kCFRunLoopBeforeWaiting = (1UL << 5), //即将进入休眠
kCFRunLoopAfterWaiting = (1UL << 6),// 刚从休眠中唤醒
kCFRunLoopExit = (1UL << 7),// 即将退出RunLoop
5、RunLoop的mode的作用
系统注册了5中mode
1  | kCFRunLoopDefaultMode //App的默认Mode,通常主线程是在这个Mode下运行  | 
但是我们只能使用两种mode
1  | kCFRunLoopDefaultMode //App的默认Mode,通常主线程是在这个Mode下运行  |