OSX / iOS 系统中,有2套API来访问和使用 RunLoop
。html
CFRunLoopRef
是在 CoreFoundation
框架内的,它提供了纯 C 函数的 API,全部这些 API 都是线程安全的。NSRunLoop
是基于 CFRunLoopRef
的封装,提供了面向对象的 API,可是这些 API 不是线程安全的。因此要了解 RunLoop
内部结构,须要多研究 CFRunLoopRef
层面的API(Core Foundation
层面)NSRunLoop
和 CFRunLoopRef
都表明着 RunLoop
对象。git
main
函数中的 RunLoop
(主运行循环):第14行代码的 UIApplicationMain
函数内部就启动了一个 RunLoop
。因此 UIApplicationMain
函数一直没有返回,保持了程序的持续运行。这个默认启动的 RunLoop
是跟主线程相关联的。 程序员
一种 Runloop 运行模式就是一个要监控的 Input 和 Timer 事件源的集合或者是一个要通知的 Runloop 观察者的集合。每次运 行Runloop,都要指定一个运行模式(显示地或者隐式地)。在 Runloop 的运行期间,只有和当前运行模式相关的源才能被监控和容许发送事件。类似的,只有和当前运行模式相关的观察者才会被通知 Runloop 的行为。和其余模式相关的源会保留新的事件直到 Runloop 运行在了合适的模式才会分发。github
在咱们的代码中,咱们能够经过字符串来标识模式。Cocoa和Core Foundation定义了一个默认模式和几个普通的有用的模式,这些模式都是用字符串来标识的。咱们能够用一个字符串当作名字来自定义一个模式,虽然咱们自定义模式的名字是随意的,可是模式的内容不是随意的,在咱们本身建立的要用的模式中至少要添加一个 Input 源、 Timer 源或者 Runloop 观察者。面试
在 Runloop 的特殊阶段咱们但是使用运行模式来过滤咱们不想要的源的事件,大多数的状况下,Runloop 都运行在系统提供的默认模式下,然而 Model Panel 可能运行在“模式”模式,当运行在这个模式期间,只有和这个模式相关的事件源才会发送事件到咱们的线程。对于第二线程来讲,咱们一般使用自定义模式来阻止低优先级的事件源在其余关键处理的时间内发送事件。安全
注意:运行模式不是根据事件类型划分的,而是根据事件源划分的。咱们不能经过模式来匹配鼠标按下事件或者键盘事件,可是咱们能够用运行模式来监听一组不一样的Port、暂时挂起Timers或者改变当前被监控的事件源和Runloop观察者。bash
下面列举了一些Cocoa和Core Foundation定义的标准模式:网络
NSDefaultRunLoopMode
:默认的运行模式,用于大部分操做,除了NSConnection对象事件。NSConnectionReplyMode
:用来监控NSConnection对象的回复的,不多可以用到。NSModalPanelRunLoopMode
:用于标明和Mode Panel相关的事件。NSEventTrackingRunLoopMode
:用于跟踪触摸事件触发的模式(例如UIScrollView上下滚动)。NSRunLoopCommonModes
:是一个模式集合,当绑定一个事件源到这个模式集合的时候就至关于绑定到了集合内的每个模式。Cocoa 应用默认包含 Default、Panel、Event Tracking 模式,Core Foundation 只包含 Default 模式,咱们能够经过 CFRunLoopAddCommonMode
添加模式。Runloop接收来自两种源的事件:app
这两种事件源都是使用应用指定的事件处理方法来处理到达的事件。框架
下面的图显示了Runloop和事件源的概念结构。 Input sources异步的分发事件到响应的处理器,而后引发runUntilDate:(由线程相关的Runloop对象调用)方法退出。 Timer sources同步分发事件到相应的处理器可是不会引发Runloop退出。
备注:
Input Sources 异步地分发事件到线程。大概有两种类型的 Input Sources,Port-based类型的输入源监控着应用的Mach端口,自定义的输入源监控着自定义的事件源。NSRunloop不关心输入源的类型。两种输入源惟一的不一样是输入源的触发方式,Port-based输入源是由系统内核触发的,而自定义的输入源要咱们本身触发。建立输入源的时候咱们就给给输入源添加指定的模式。下面是一些输入源:
Port-Based Sources
Cocoa 和 Core Foundation 提供了类和接口用来建立 Port-Based 源,Cocoa 只要建立 NSPort 对象,并添加到 NSRunloop 中就能够啦,NSPort负责输入源的建立和配置。Core Foundation 须要手动的常见 port 和输入源。
Custom Input Sources
咱们要用到CFRunLoopSourceRef函数建立输入源,并定义几个回调函数用于配置输入源、处理事件和删除输入源。事件的触发机制要咱们本身定义。
Cocoa Perform Selector Sources
Cocoa定义了能够在任何线程上执行方法的事件源,在想要执行的线程上执行方法是顺序执行的,避免了多个方法在线程上执行的同步问题。Perform Selector Sources在方法执行完以后就会本身从NSRunloop中删除。
Perform Selector Sources要求目标线程的NSRunloop必须是运行的,主线程默认是运行的。NSRunloop在一次迭代过程当中会处理全部的Perform Selector调用,而不是一次迭代处理一个Perform Selector调用。NSObject中定义的Perform Selector方法以下
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:
延迟执行是在NSRunloop的下一次迭代中过了指定的延迟事件才执行。取消操做是针对延迟执行方法的。
Timer Sources 同步地在未来的一个肯定的时间分发事件到咱们的线程。Timers 可让线程通知本身去处理一些事情。Timers 不是一个实时的机制,当 Timers 触发的时候 NSrunloop 恰好正在执行处理函数,Timer s会等待 NSRunloop 调用本身的处理函数。
Timers 能够建立一次性的和重复性的事件,当建立重复性的事件的时候,Timers 只会根据规划好的触发时间来从新规划触发时间,而不是根据确切的触发时间。并且因为延迟触发丢失了几回触发的话,Timers 只会补充一次触发。
不像是事件源同样在事件触发的时候执行处理函数。NSRunloop 观察者是在 NSRunloop 几个执行的特定的点触发。NSRunloop 能够观察的几个事件是:
建立观察者的方法是 CFRunLoopObserverRef,咱们能够通 过Core Foundation 方法添加到指定的 NSRunloop。观察者也能够建立一次性的和重复性的。一次性的观察者触发以后就会从 NSRunloo p中删除。
Core Foundation
中关于 RunLoop
的5个类
CFRunLoopRef
CFRunLoopModeRef
CFRunLoopSourceRef
CFRunLoopTimerRef
CFRunLoopObserverRef
注:RunLoop 若是没有这些东西会直接退出CFRunLoopModeRef表明RunLoop的运行模式:一个 RunLoop 包含若干个 Mode,每一个Mode又包含若干个 Source/Timer/Observer
每次RunLoop启动时,只能指定其中一个 Mode,这个Mode被称做 CurrentMode 若是须要切换 Mode,只能退出 Loop,再从新指定一个 Mode 进入 这样作主要是为了分隔开不一样组的 Source/Timer/Observer,让其互不影响。
系统默认注册了5个Mode:(前两个跟最后一个经常使用)
kCFRunLoopDefaultMode
:App的默认Mode,一般主线程是在这个Mode下运行UITrackingRunLoopMode
:界面跟踪 Mode,用于 ScrollView 追踪触摸滑动,保证界面滑动时不受其余 Mode 影响UIInitializationRunLoopMode
: 在刚启动 App 时第进入的第一个 Mode,启动完成后就再也不使用GSEventReceiveRunLoopMode
: 接受系统事件的内部 Mode,一般用不到kCFRunLoopCommonModes
: 这是一个占位用的Mode,不是一种真正的Mode按照官方文档的分类:
按照函数调用栈的分类
函数调用栈
CFRunLoopTimerRef 是基于时间的触发器,基本上说的就是 NSTimer (CADisplayLink 也是加到 RunLoop),它受 RunLoop 的 Mode 影响。
GCD的定时器不受 RunLoop 的 Mode 影响。
CFRunLoopObserverRef是观察者,可以监听RunLoop的状态改变 能够监听的时间点有如下几个
使用
- (void)observer {
// 建立observer
CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(CFAllocatorGetDefault(), kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
NSLog(@"----监听到RunLoop状态发生改变---%zd", activity);
});
// 添加观察者:监听RunLoop的状态
CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopDefaultMode);
// 释放Observer
CFRelease(observer);
}
特别注意
/*
CF的内存管理(Core Foundation)
1.凡是带有Create、Copy、Retain等字眼的函数,建立出来的对象,都须要在最后作一次release
* 好比CFRunLoopObserverCreate
2.release函数:CFRelease(对象);
*/
复制代码
场景还原:拖拽时模式由 NSDefaultRunLoopMode 进入 UITrackingRunLoopMode ,NSTimer 再也不响应图片中止轮播,将计时器改为 NSRunLoopCommonModes 模式下两种模式均可运行。
- (void)timer {
NSTimer *timer = [NSTimer timerWithTimeInterval:2.0 target:self selector:@selector(run) userInfo:nil repeats:YES];
// 定时器只运行在NSDefaultRunLoopMode下,一旦RunLoop进入其余模式,这个定时器就不会工做
// [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
// 定时器只运行在UITrackingRunLoopMode下,一旦RunLoop进入其余模式,这个定时器就不会工做
// [[NSRunLoop currentRunLoop] addTimer:timer forMode:UITrackingRunLoopMode];
// 定时器会跑在标记为common modes的模式下
// 标记为common modes的模式:UITrackingRunLoopMode和NSDefaultRunLoopMode兼容
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
}
- (void)timer2 {
// 调用了scheduledTimer返回的定时器,已经自动被添加到当前runLoop中,并且是NSDefaultRunLoopMode
NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:2.0 target:self selector:@selector(run) userInfo:nil repeats:YES];
// 修改模式
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
}
复制代码
需求:当用户在拖拽时(UI交互时)不显示图片,拖拽完成时显示图片
// 只在NSDefaultRunLoopMode模式下显示图片
// inModes:设置运行模式
[self.imageView performSelector:@selector(setImage:) withObject:[UIImage imageNamed:@"placeholder"] afterDelay:3.0 inModes:@[NSDefaultRunLoopMode]];
复制代码
应用场景: 常常在后台进行耗时操做,如:监控联网状态,扫描沙盒等 不但愿线程处理完事件就销毁,保持常驻状态
- (void)run {
//addPort:添加端口(就是source) forMode:设置模式
[[NSRunLoop currentRunLoop] addPort:[NSPort port] forMode:NSDefaultRunLoopMode];
//启动RunLoop
[[NSRunLoop currentRunLoop] run];
/*
//另外两种启动方式
[NSDate distantFuture]:遥远的将来 这种写法跟上面的run是一个意思
[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
不设置模式
[[NSRunLoop currentRunLoop] runUntilDate:[NSDate distantFuture]];
*/
}
复制代码
退出-退出当前线程[NSThread exit];
复制代码
- (void)run {
while (1) {
[[NSRunLoop currentRunLoop] run];
}
}
复制代码
在休眠前(kCFRunLoopBeforeWaiting)进行释放,处理事件前建立释放池,中间建立的对象会放入释放池。
特别注意:在启动 RunLoop 以前建议用 @autoreleasepool {...} 包裹。
意义:建立一个大释放池,释放 {} 期间建立的临时对象,通常好的框架的做者都会这么作。
- (void)execute {
@autoreleasepool {
NSTimer *timer = [NSTimer timerWithTimeInterval:2.0 target:self selector:@selector(run) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
[[NSRunLoop currentRunLoop] run];
}
复制代码
通常的NSTimer定时器由于受到RunLoop,会存在时间不许时的状况。 上文有提到GCD不受RunLoop影响,下面简单的说一下它的使用
/** 定时器(这里不用带*,由于 dispatch_source_t 就是个类,内部已经包含了*) */
@property (nonatomic, strong) dispatch_source_t timer;
int count = 0;
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
// 得到队列
// dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
dispatch_queue_t queue = dispatch_get_main_queue();
// 建立一个定时器(dispatch_source_t本质仍是个OC对象)
self.timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
// 设置定时器的各类属性(几时开始任务,每隔多长时间执行一次)
// GCD的时间参数,通常是纳秒 NSEC_PER_SEC(1秒 == 10的9次方纳秒)
// 什么时候开始执行第一个任务
// dispatch_time(DISPATCH_TIME_NOW, 3.0 * NSEC_PER_SEC) 比当前时间晚3秒
dispatch_time_t start = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.0 * NSEC_PER_SEC));
uint64_t interval = (uint64_t)(1.0 * NSEC_PER_SEC);
dispatch_source_set_timer(self.timer, start, interval, 0);
// 设置回调
dispatch_source_set_event_handler(self.timer, ^{
NSLog(@"------------%@", [NSThread currentThread]);
count++;
// if (count == 4) {
// // 取消定时器
// dispatch_cancel(self.timer);
// self.timer = nil;
// }
});
// 启动定时器
dispatch_resume(self.timer);
}
复制代码
每条线程都有惟一的一个与之对应的 RunLoop 对象;
主线程的 RunLoop 已经自动建立好了,子线程的RunLoop须要主动建立;
RunLoop在第一次获取时建立,在线程结束时销毁;
获取RunLoop对象
// 工做线程 须要程序员手工写代码让runloop运行起来
[NSRunLoop currentLoop]runUntilDate:]
// Foundation
[NSRunLoop currentRunLoop]; // 得到当前线程的RunLoop对象
[NSRunLoop mainRunLoop]; // 得到主线程的RunLoop对象
// Core Foundation
CFRunLoopGetCurrent(); // 得到当前线程的RunLoop对象
CFRunLoopGetMain(); // 得到主线程的RunLoop对象
复制代码
线程安全性
基于 Cocoa 的接口不是线程安全的,基于 Core Foundation 的接口是线程安全的。
什么是RunLoop?
在开发中如何使用RunLoop?什么应用场景?
在异步线程中下载不少图片。若是失败了,该如何处理?请结合runloop来谈谈解决方案?
答:(提示:在异步线程中启动一个runloop从新发送网络图片)
(1)从新下载图片
(2)利用 runloop 的输入源回到主线程刷新 UIImageView。
苹果官方文档
CFRunLoop官方文档
NSRunLoop官方文档
CFRunLoopRef
NSRunloop的使用