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下运行 |