概念
RunLoop 就像她的名字一样,就是跑环,就是一个死循环。是一个可以随时休眠,随时唤醒的死循环。
那么一个手机App为什么会一直运行?而且在接受到用户点击的时候,会做出反应?这些都离不开RunLoop。
iOS App启动的时候,就会自动启动一个RunLoop。一直在循环监听着用户的各种操作,并作出反应。每个线程都有一个RunLoop,但是,只有主线程的RunLoop是默认开启的。可以这样理解:
1. RunLoop 是iOS消息机制的处理模式
NSRunLoop的主要作用:控制NSrunLoop里面的线程的执行和休眠,在有事做的时候,使当前的RunLoop控制线程工作,没事做的时候让当前的NS RunLoop控制线程休息。
2.NSRunLoop就是一只在循环检测,从线程start到线程的end,检测inputSource(点击,双击等操作)同步事件,检测timeSource(计时器)同步操作。检测到输入源会执行处理函数,首先会产生通知,corefunction向线程添加runloop observers来监听事件,意在监听事件发生时来做处理。
iOS main函数中的RunLoop
int main(int argc, char * argv[]) { @autoreleasepool { //一旦程序启动会开启一个RunLoop 一直循环监听用户的点击事件 触摸事件 定时器事件等 并且一直不会返回。
保证程序一直运行,直到程序结束。这个默认的RunLoop就是跟主线程相关的。 int rs = UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); return rs; } }
iOS中RunLoop对象。
iOS中有两套API来访问和使用RunLoop
一套是 Foundation -> NSRunLoop oc封装的
一套是 Core Foundtion ->CFRunLoopRef C语言调用
但是 这两套API 是等价的,NSRunLoop就是基于CFRunLoopRef的一层OC包装。所以要研究NSRunLoop还是要研究CFRunLoopRef 。
RunLoop和线程的关系
1. 每条线程都有唯一的一个与之对应的RunLoop对象
2. 主线程的RunLoop已经自动创建好了,自线程的RunLoop需要自动创建
3. RunLoop在第一次获取时创建,在线程结束的时销毁。
RunLoop中的相关类
1.CFRunLoopRef
2.CFRunLoopModeRef RunLoop模式
3.CFRunLoopSourceRef 事件源 输入源
4.CFRunLoopTimerRef
5.CFRunLoopObserverRef
CFRunLoopModeRef 代表RunLoop的运行模式 系统提供了5中运行模式:
default模式:几乎包括所有输入源(除NSConnection) NSDefaultRunLoopMode模式
connection模式:处理NSConnection事件,属于系统内部,用户基本不用
UITrackingRunLoopMode: 界面跟踪 Mode,用于 ScrollView 追踪触摸滑动,保证界面滑动时不受其他 Mode 影响。
UIInitializationRunLoopMode: 在刚启动 App 时第进入的第一个 Mode,启动完成后就不再使用。
NSRunLoopCommonModes: 包含了多种模式:default, modal, 和tracking modes。
一个RunLoop包含若干个Mode 每个Model又包含若干个Source/Timer/Observer
每次RunLoop启动时,只能指定其中一个Mode,这个mode被称作currentMode
如果需要切换Mode 只能tuichuLoop,再重新指定一个Mode进入。
CFRunLoopTimerRef
CFRunLoopTimerRef 就是基于时间的触发器
基本上说的就是NSTimer,他会收到RunLoop的mode的影响
GCD的定时器不会受到RunLoop的Mode的影响。
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event { //这种方式启动的定时器 会自动加入到系统创建的RunLoop中 //[NSTimer scheduledTimerWithTimeInterval:2.0 target:self selector:@selector(show) userInfo:nil repeats:YES]; //使用这种方法创建的定时器 必须添加到定时器中 否则不会有作用 NSTimer *timer = [NSTimer timerWithTimeInterval:2.0 target:self selector:@selector(show) userInfo:nil repeats:YES]; /* 如果是NSDefaultRunLoopMode 这样虽然定时器会工作,但是当拖动UITextView 的时候定时器就不在工作了,因为RunLoop有四种模式,它会在这四种模式中来回切换,当UITextView拖动时,RunLoop会进入UITrackingRunLoopMode模式,这时,就不再执行其他模式中的timer事件。当不再拖动时,会再次进入NSDefaultRunLoopMode模式,进行定时器事件。 如果换成 UITrackingRunLoopMode 模式,只有在UI拖动时,才会执行定时器事件。 那么如果你需要在UI拖动时不影响定时器事件的执行,我们可以使用NSRunLoopCommonModes 这其实不是一种模式,而是一种模式集合,包括UITrackingRunLoopMode 和 NSDefaultRunLoopMode。 NSTimer 计时不准确就是因为RunLoop在各种模式中自动切换进行的原因。GCD的计时是比较准确的 */ [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes]; } - (void)show { NSLog(@"show -----"); }
GCD Timer
- (void)GCDTimer { //第一步 创建队列 dispatch_queue_t queue = dispatch_get_global_queue(0, 0); //第二步 创建一个GCD定时器 /* 第一个参数 表明创建的是一个定时器 第四个参数 表示事件运行在哪个线程中 */ dispatch_source_t sourceTimer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0,0, queue); self.timer = sourceTimer; //设置时间间隔 /* 第一个参数 定时器 第二个参事 定时器 开始时间 第三个参数 从现在开始间隔时间 第四个参数 精准度 GCD 的单位是纳秒 */ dispatch_source_set_timer(sourceTimer, DISPATCH_TIME_NOW, 2.0 *NSEC_PER_SEC, 0*NSEC_PER_SEC); //设置事件 dispatch_source_set_event_handler(sourceTimer, ^{ //要执行的任务 NSLog(@"GCDTimer"); }); //启动定时器 dispatch_resume(sourceTimer); }
CFRunLoopSourceRef (事件源 输入源)
分类
Source0: 不是基于端口的 用户主动触发的事件。
Source1: 基于端口的 通过内核和其他线程相互发送消息
RunLoop 的消息类型
根据上图我们可以将消息分为二种类型,第一种类型又可以细分为三种,此三种共同点就是它们都是异步执行的
port ->source1
监听程序的Mach ports,Mach ports是一个比较底层的东西,可以简单的理解为:内核通过port这种方式将信息发送,而mach则监听内核发来的port信息,然后将其整理,打包发给runloop。
Customer:->source0
很明显,由开发人员自己发送。不仅仅是发送,过程的话相当复杂,苹果也提供了一个CFRunLoopSource来帮助处理。由于很少用到,可以简单说下核心,但是对帮助我们理解runloop却很有帮助:
1.定义输入源(数据结构)
2.将输入源添加到runloop,那么这样就有了接受者,即为R1
3.协调输入源的客户端(单独线程),专门监听消息,然后将消息打包成runloop能够处理的样式,即第一步定义的输入源。它类似Mach的功能
4.谁来发送消息的问题?上面的machport是由内核发送的。自定义的当然要我们自己发送了。。。首先必须是另一个线程来发送(当然如果只是测试的话可以和第三步在同一个线程),先发送消息给输入源,然后唤醒R1,因为R1一般处于休眠状态,然后R1根据输入源来做相应的处理
Selector Sources
NSObject类提供了很多方法供我们使用,这些方法是添加到runloop的,所以如果没有开启runloop的话,不会运行(不过有个坑,请看下面介绍)。
/// 主线程 performSelectorOnMainThread:withObject:waitUntilDone: performSelectorOnMainThread:withObject:waitUntilDone:modes: /// 指定线程 performSelector:onThread:withObject:waitUntilDone: performSelector:onThread:withObject:waitUntilDone:modes: /// 针对当前线程 performSelector:withObject:afterDelay: performSelector:withObject:afterDelay:inModes: /// 取消,在当前线程,和上面两个方法对应 cancelPreviousPerformRequestsWithTarget: cancelPreviousPerformRequestsWithTarget:selector:object:
下面提供的方法是在指定的线程运行aSelector,一般情况下aSelector会添加到指定线程的runloop。但,如果调用线程和指定线程为同一线程,且wait参数设为YES,那么aSelector会直接在指定线程运行,不再添加到runloop。
performSelectorOnMainThread:withObject:waitUntilDone:
performSelectorOnMainThread:withObject:waitUntilDone:modes:
performSelector:onThread:withObject:waitUntilDone:
performSelector:onThread:withObject:waitUntilDone:modes:
其实这也很好理解,假设这种情况也添加到指定线程的runloop,我们可以这样反向理解:1,当前线程runloop还没有开启,那么aSelector就不会被执行,然而你却一直在等待,造成线程卡死。2,当前线程runloop已经开启,那么调用performSelector这个方法的位置肯定是处于runloop的callout方法里面,在这里等待runloop再callout从而调用aSelector方法完成,显然也是死等待,线程卡死。。。
还有一些performSelector方法,是不会添加到runloop的,而是直接执行,可以按照上面的特殊情况进行理解。方法列举如下:
- (id)performSelector:(SEL)aSelector; - (id)performSelector:(SEL)aSelector withObject:(id)object; - (id)performSelector:(SEL)aSelector withObject:(id)object1 withObject:(id)object2;
看到这里,是否感觉有些乱???只要记住没有延迟或者等待的都不会添加到runloop,有延迟或者等待的还有排除上面提到的特殊情况
CFRunLoopObservers 观察者
首先它并不属于事件源(不会影响runloop的生命周期),它比较特殊,用于观察runloop自身的一些状态的,有以下几种:
1.进入RunLoop kCFRunLoopEntry
2.RunLoop即将执行定时器 kCFRunLoopBeforeTimers
3.RunLoop即将执行输入源 kCFRunLoopBeforeSources
4.RunLoop即将休眠 kCFRunLoopBeforeWaiting
5.RunLoop被唤醒 在处理完唤醒它的事件之前 kCFRunLoopAfterWaiting
6.退出 kCFRunLoopExit
//给RunLoop添加一个监听者 - (void)observer { //创建监听者 /** param1: 给observer分配存储空间 param2: 需要监听的状态类型:kCFRunLoopAllActivities监听所有状态 param3: 是否每次都需要监听,如果NO则一次之后就被销毁,不再监听,类似定时器的是否重复 param4: 监听的优先级,一般传0 param5: 监听到的状态改变之后的回调 return: 观察对象 */ CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(kCFAllocatorDefault, kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) { switch (activity) { case kCFRunLoopEntry: NSLog(@"RunLoop进入"); break; case kCFRunLoopBeforeTimers: NSLog(@"即将处理timer"); break; case kCFRunLoopBeforeSources: NSLog(@"即将处理input Sources"); break; case kCFRunLoopBeforeWaiting: NSLog(@"即将睡眠"); break; case kCFRunLoopAfterWaiting: NSLog(@"从睡眠中唤醒,处理完唤醒源之前"); break; case kCFRunLoopExit: NSLog(@"退出"); break; default: break; } }); /* *第一个参数 RunLoop *第二个参数 监听者 *第三个参数 要监听RunLoop在哪种运行模式下的状态 */ [NSTimer scheduledTimerWithTimeInterval:3 target:self selector:@selector(doFireTimer) userInfo:nil repeats:NO]; CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopDefaultMode); } - (void)doFireTimer { NSLog(@"---fire---"); }