简述
- 运行循环、跑圈 内部是一个do-while循环 在这个循环内部不断处理各种比如(source timer observer) 有事情就跑一圈看看有没有东西要处理 提高程序性能
作用
- 可以实现 RunLoop 实现自动释放池、延迟回调、触摸事件、屏幕刷新等功能的。
RunLoop 对外的接口
- 在 CoreFoundation 里面关于 RunLoop 有5个类:
CFRunLoopRef
CFRunLoopModeRef
CFRunLoopSourceRef
CFRunLoopTimerRef
CFRunLoopObserverRef
其中CFRunLoopModeRef类并没有对外暴露,只是通过CFRunLoopRef的接口进行了封装。他们的关系如下:
-
一个 RunLoop 包含若干个 Mode,每个 Mode 又包含若干个 Source/Timer/Observer。每次调用 RunLoop 的主函数时,只能指定其中一个 Mode,这个Mode被称作 CurrentMode。如果需要切换 Mode,只能退出 Loop,再重新指定一个 Mode 进入。这样做主要是为了分隔开不同组的 Source/Timer/Observer,让其互不影响。
-
上面的 Source/Timer/Observer 被统称为 mode item,一个 item 可以被同时加入多个 mode。但一个 item 被重复加入同一个 mode 时是不会有效果的。如果一个 mode 中一个 item 都没有,则 RunLoop 会直接退出,不进入循环。
CFRunLoopSourceRef 是事件产生的地方。Source有两个版本:Source0 和 Source1。
• Source0 只包含了一个回调(函数指针),它并不能主动触发事件。使用时,你需要先调用 CFRunLoopSourceSignal(source),将这个 Source 标记为待处理,然后手动调用 CFRunLoopWakeUp(runloop) 来唤醒 RunLoop,让其处理这个事件。
• Source1 包含了一个 mach_port 和一个回调(函数指针),被用于通过内核和其他线程相互发送消息。这种 Source 能主动唤醒 RunLoop 的线程,其原理在下面会讲到。
系统默认注册了5个Mode
CFRunLoopTimerRef是基于时间的触发器,它和 NSTimer 是toll-free bridged 的,可以混用。其包含一个时间长度和一个回调(函数指针)。当其加入到 RunLoop 时,RunLoop会注册对应的时间点,当时间点到时,RunLoop会被唤醒以执行那个回调。
CFRunLoopObserverRef是观察者,每个 Observer 都包含了一个回调(函数指针),当 RunLoop 的状态发生变化时,观察者就能通过回调接受到这个变化。可以观测的时间点有以下几个:
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
kCFRunLoopEntry = (1UL << 0), 1
kCFRunLoopBeforeTimers = (1UL << 1), 2
kCFRunLoopBeforeSources = (1UL << 2), 4
kCFRunLoopBeforeWaiting = (1UL << 5), 32
kCFRunLoopAfterWaiting = (1UL << 6), 64
kCFRunLoopExit = (1UL << 7), 12867
kCFRunLoopAllActivities = 0x0FFFFFFFU
};
RunLoop的Mode
Mode是一种状态
-
苹果公开提供的 Mode 有两个:kCFRunLoopDefaultMode (NSDefaultRunLoopMode) 和 UITrackingRunLoopMode,你可以用这两个 Mode Name 来操作其对应的 Mode。
应用场景举例:主线程的 RunLoop 里有两个预置的 Mode:kCFRunLoopDefaultMode 和 UITrackingRunLoopMode。这两个 Mode 都已经被标记为”Common”属性。DefaultMode 是 App 平时所处的状态,TrackingRunLoopMode 是追踪 ScrollView 滑动时的状态。当你创建一个 Timer 并加到 DefaultMode 时,Timer 会得到重复回调,但此时滑动一个TableView时,RunLoop 会将 mode 切换为 TrackingRunLoopMode,这时 Timer 就不会被回调,并且也不会影响到滑动操作。
-
同时苹果还提供了一个操作 Common 标记的字符串:kCFRunLoopCommonModes (NSRunLoopCommonModes),你可以用这个字符串来操作 Common Items,或标记一个 Mode 为 “Common”。使用时注意区分这个字符串和其他 mode name。
应用场景举例:有时你需要一个 Timer,在两个 Mode 中都能得到回调,一种办法就是将这个 Timer 分别加入这两个 Mode。还有一种方式,就是将 Timer 加入到顶层的 RunLoop 的 “commonModeItems” 中。”commonModeItems” 被 RunLoop 自动更新到所有具有”Common”属性的 Mode 里去。
RunLoop 的内部逻辑
- 实际上就是一个do-while循环 当你调用RunLoop并且让他开始Run的时候线程就会一直停留在这个循环里;直到超时或被手动停止,该函数才会返回。
下面是例子
autoreleasepool
int main(int argc, char * argv[]) {
@autoreleasepool {
return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
}
}
熟悉吗?是的。每个程序被创建我们都能在main文件中看到这一行,在这里UIApplicationMain表示主线程已经创建了一个RunLoop, 线程结束销毁。注意哦RunLoop在子线程中不是自动创建的。在NSRunLoop进入休眠状态前会把该进程中的东西释放(autoreleasepool),第二次唤醒的时候再把数据添加进池子。
Timer
#pragma mark 定时器
-(void)timer{
//调用了scheduledTimer返回的定时器,已经自动被添加到当前的runLoop中,而且是NSDefaultRunLoopMode 但是仍然可以通过addtimer的方式修改他的runloop
NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:2.0 target:self selector:@selector(run) userInfo:nil repeats:YES];
//选择在一个模式(mode)下运行,要想切换就必须先退出当前模式,然后再去重开另外一个模式。一个模式下只能执行当面模式下的状态
//添加定时器只在NSDefaultRunLoopMode(空闲)下可以使用 进入其他模式后timer就不能工作了
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
//添加定时器只在UITrackingRunLoopMode(滑动)下可以使用 进入其他模式后timer就不能工作了
[[NSRunLoop currentRunLoop] addTimer:timer forMode:UITrackingRunLoopMode];
//定时器会表跑在标记为NSRunLoopCommonModes的模式下 标记有UITrackingRunLoopMode(scrollerview滚动)和NSDefaultRunLoopMode(空闲状态下)
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
}
Source
Observers
#pragma mark Observer
-(void)observers{
CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(CFAllocatorGetDefault(), kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
NSLog(@"---------监听状态改变----%zd",activity);
});
//添加观察者:监听runloop的状态
CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopDefaultMode);
//释放observ
CFRelease(observer);
//Core foundati内Create Copy Retain 都会让计数器+1 都要释放
}
PerformSelecter
- 让某个对象在执行performSelector的时候一定要处于某种情况下 这句话在@selector的方法执行完的时候出于消亡状态。
- 当调用 NSObject 的 performSelecter:afterDelay: 后,实际上其内部会创建一个 Timer 并添加到当前线程的 RunLoop 中。所以如果当前线程没有 RunLoop,则这个方法会失效。
[object performSelector:@selector(method) withObject:nil afterDelay:3.0 inModes:@[NSDefaultRunLoopMode]];
- 当调用 performSelector:onThread: 时,实际上其会创建一个 Timer 加到对应的线程去,同样的,如果对应线程没有 RunLoop 该方法也会失效。
[self performSelector:@selector(method) onThread:Somethread withObject:nil waitUntilDone:NO];
GCD
- 当调用 dispatch_async(dispatch_get_main_queue(), block) 时,libDispatch 会向主线程的 RunLoop 发送消息,RunLoop会被唤醒,并从消息中取得这个 block,并在回调CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE() 里执行这个 block。但这个逻辑仅限于 dispatch 到主线程,dispatch 到其他线程仍然是由 libDispatch 处理的。
常驻线程
- 概念:让一个子线程不进入消亡状态,让他一直处于runloop状态 等待其他线程发来消息 处理时间。
- 做法:在该线程中添加一个mode并且run起来该NSRunLoop
- (void)viewDidLoad {
[super viewDidLoad];
//创建线程
self.thread = [[WXZThread alloc] initWithTarget:self selector:@selector(run) object:nil];
//开启线程
[self.thread start];
}
- (void)run
{
NSLog(@"----------run----%@", [NSThread currentThread]);
//保证线程永远都在运行状态,并且开始在NSDefaultRunLoopMode下等待处理[NSPort port]
[[NSRunLoop currentRunLoop] addPort:[NSPort port] forMode:NSDefaultRunLoopMode];
//开始启动循环处理self.thread线程内的事件
[[NSRunLoop currentRunLoop] run];
//后面的都不是self.thread线程的事情 所以没有被打印 因为不会走到这一步 上面的一直在跑圈监听self.thread的线程
NSLog(@"----------我没有被打印---------");
}
//当外界给这个正在RunLoo的线程一个外力时,也能够发送消息给test。因为self.thread线程没有消亡(RunLoop即使休眠了也会因为self.thread里有mode而不会消亡)
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
//selector也是一个source
[self performSelector:@selector(test) onThread:self.thread withObject:nil waitUntilDone:NO];
}
- (void)test
{
NSLog(@"----------test----%@", [NSThread currentThread]);
}
在什么情况下可以使用runloop
- 常驻线程 概念:让一个子线程不进入消亡状态,让他一直处于runloop状态 等待其他线程发来消息 处理时间。做法:在该线程中添加一个mode并且run起来该NSRunLoop
- 在子线程中开启定时器NSTimer
- 在子线程中进行一些长期监控 在什么情况下使用:长时间的耗时操作 比如socket
- 让某些事情在特定的mode下实行
- 可以添加observers监听runloop的状态 比如在点击事件前先做一些事情
一些面试题
runloop 和线程的关系:
- 主线程的run loop默认是启动的。
iOS的应用程序里面,程序启动后会有一个如下的main()函数
int main(int argc, char * argv[]) {
@autoreleasepool {
return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
}}
重点是UIApplicationMain()函数,这个方法会为main thread设置一个NSRunLoop对象,这就解释了:为什么我们的应用可以在无人操作的时候休息,需要让它干活的时候又能立马响应。
对其它线程来说,run loop默认是没有启动的,如果你需要更多的线程交互则可以手动配置和启动,如果线程只是去执行一个长时间的已确定的任务则不需要。
在任何一个 Cocoa 程序的线程中,都可以通过以下代码来获取到当前线程的 run loop 。
NSRunLoop *runloop = [NSRunLoop currentRunLoop];
runloop的mode作用是什么?
1.model 主要是用来指定事件在运行循环中的优先级的,分为:
NSDefaultRunLoopMode(kCFRunLoopDefaultMode):默认,空闲状态
UITrackingRunLoopMode:ScrollView滑动时
UIInitializationRunLoopMode:启动时
NSRunLoopCommonModes(kCFRunLoopCommonModes):Mode集合
2.苹果公开提供的 Mode 有两个:
- NSDefaultRunLoopMode(kCFRunLoopDefaultMode)
- NSRunLoopCommonModes(kCFRunLoopCommonModes)
以+ scheduledTimerWithTimeInterval...的方式触发的timer,在滑动页面上的列表时,timer会暂定回调,为什么?如何解决?
RunLoop只能运行在一种mode下,如果要换mode,当前的loop也需要停下重启成新的。利用这个机制,ScrollView滚动过程中NSDefaultRunLoopMode(kCFRunLoopDefaultMode)的mode会切换到UITrackingRunLoopMode来保证ScrollView的流畅滑动:只能在NSDefaultRunLoopMode模式下处理的事件会影响scrllView的滑动。
如果我们把一个NSTimer对象NSDefaultRunLoopMode(kCFRunLoopDefaultMode)添加到主运行循环中的时候, ScrollView滚动过程中会因为mode的切换,而导致NSTimer将不再被调度。
同时因为mode还是可定制的,所以:Timer计时会被scrollView的滑动影响的问题可以通过将timer添加到NSRunLoopCommonModes(kCFRunLoopCommonModes)来解决。
猜想runloop内部是如何实现的?
一般来讲,一个线程一次只能执行一个任务,执行完成后线程就会退出。如果我们需要一个机制,让线程能随时处理事件但并不退出,通常的代码逻辑 是这样的:
int main(int argc, char * argv[]) {
//程序一直运行状态
while(AppIsRunning) {
//睡眠状态,等待唤醒事件
id whoWakesMe = SleepForWakingUp();
//得到唤醒事件
id event = GetEvent(whoWakesMe);
//开始处理事件
HandleEvent(event);}
return0;
}