RunLoop概念
一个APP之所以能在程序运行起来不停止,就是RunLoop的原因,RunLoop就像一个死循环,等待处理外部手机操作,网络请求以及内部通讯等命令,其实RunLoop是管理线程的一种机制,这种机制不仅在iOS上有,在Node.js中的EventLoop,Android中的Looper,都有类似的模式。
RunLoop作用
一个RunLoop是一个事件处理环,系统利用这个事件处理环来安排事务,协调输入的各种事件。RunLoop的目的是让你的线程在有工作的时候忙碌,没有工作的时候休眠。
RunLoop 实际上就是一个对象,这个对象管理了其需要处理的事件和消息,并提供了一个入口函数来执行上面 Event Loop 的逻辑。线程执行了这个函数后,就会一直处于这个函数内部 “接受消息->等待->处理” 的循环中,直到这个循环结束(比如传入 quit 的消息),函数返回。
RunLoop和线程
RunLoop是为了线程而生,没有线程,它就没有存在的必要。RunLoop是线程的基础架构部分。线程和RunLoop之间是以键值对的形式一一对应的,其中key是thread,value是RunLoop。
RunLoop中API
CocoaTouch和CoreFundation都提供了Runloop对象方便配置和管理线程的RunLoop。OSX/iOS 系统中,提供了两个这样的对象:NSRunLoop 和 CFRunLoopRef。
CFRunLoopRef 是在 CoreFoundation 框架内的,它提供了纯 C 函数的 API,所有这些 API 都是线程安全的。
NSRunLoop 是基于 CFRunLoopRef 的封装,提供了面向对象的 API,但是这些 API 不是线程安全的。
CocoaTouch层面提供的API比较简单:
每个线程,包括程序的主线程都有与之相应的RunLoop对象。
主线程的RunLoop默认是启动的,通过[NSRunLoop mainRunLoop]获得。子线程的RunLoop如果要启动需要手动调用。在任何一个CocoaTouch程序的线程中,都可以通过NSRunLoop *runloop = [NSRunLoop currentRunLoop]来获取到当前线程的RunLoop。开启子线程RunLoop,可以使用[[NSRunLoop currentRunLoop]runUntilDate:[NSDate distantFuture]];
那么,开启的RunLoop什么时候销毁呢?答案是当该线程销毁时,该线程的RunLoop肯定被销毁或者RunLoop的mode为空的时候销毁,那么怎么判断mode为空呢?答案是该mode中没有observer,source,timer事件就为空。
CoreFundation层面提供的API相对更多一点:
在 CoreFoundation 里面关于 RunLoop 有5个类:
1.CFRunLoopRef
它只提供了两个自动获取的函数:CFRunLoopGetMain() 和 CFRunLoopGetCurrent()。
2.CFRunLoopModeRef
一个 RunLoop 包含若干个 Mode,每个 Mode 又包含若干个 Source/Timer/Observer。但是每次RunLoop运行时,只能指定其中一个Mode,这个Mode被称作 CurrentMode。如果需要切换 Mode,只能退出 Loop,再重新指定一个 Mode 进入。这样做主要是为了分隔开不同组的 Source/Timer/Observer,让其互不影响。
3.CFRunLoopSourceRef
处理事件源。Source有两个版本:Source0 和 Source1。分别处理不同事件,Source0处理外部交互事件,Source1处理内部通信等事件。
4.CFRunLoopTimerRef
处理Timer事件。
5.CFRunLoopObserverRef
观察者,监听RunLoop不同状态。
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
kCFRunLoopEntry = (1UL << 0), // 即将进入Loop
kCFRunLoopBeforeTimers = (1UL << 1), // 即将处理 Timer
kCFRunLoopBeforeSources = (1UL << 2), // 即将处理 Source
kCFRunLoopBeforeWaiting = (1UL << 5), // 即将进入休眠
kCFRunLoopAfterWaiting = (1UL << 6), // 刚从休眠中唤醒
kCFRunLoopExit = (1UL << 7), // 即将退出Loop
};
-(void)observerRunLoop
{
//监听kCFRunLoopDefaultMode下的RunLoop状态
CFRunLoopMode mode =kCFRunLoopDefaultMode;
CFRunLoopObserverRef observer=CFRunLoopObserverCreateWithHandler(CFAllocatorGetDefault(), 0, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
switch (activity) {
case kCFRunLoopEntry:
SDLog(@"RunLoop启动");
break;
case kCFRunLoopBeforeWaiting:
SDLog(@"RunLoop即将休眠");
break;
case kCFRunLoopAfterWaiting:
SDLog(@"RunLoop被唤醒");
break;
case kCFRunLoopBeforeTimers:
SDLog(@"RunLoop即将处理Timers");
break;
case kCFRunLoopBeforeSources:
SDLog(@"RunLoop即将处理Sources");
break;
case kCFRunLoopExit:
SDLog(@"RunLoop退出");
break;
default:
break;
}
});
CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer, mode);
}
RunLoop关于mode的应用
RunLoop包含5中mode(模式),每种模式接受不同的事件源。
a. kCFRunLoopDefaultMode: App的默认 Mode,通常主线程是在这个 Mode 下运行的。
b. UITrackingRunLoopMode: 界面跟踪 Mode,用于 ScrollView 追踪触摸滑动,保证界面滑动时不受其他 Mode 影响。
c. UIInitializationRunLoopMode: 在刚启动 App 时第进入的第一个 Mode,启动完成后就不再使用。
d.GSEventReceiveRunLoopMode: 接受系统事件的内部 Mode,通常用不到。
e. kCFRunLoopCommonModes: 这是一个占位的 Mode,没有实际作用。
每种mode中又包含三种item(soures,observer,timer),如果一个Mode中一个item都没有,则这个RunLoop会直接退出。我们的RunLoop要想工作,必须要让它存在一个Item(source,observer或者timer),主线程之所以能够一直存在,并且随时准备被唤醒就是因为系统为其添加了很多item。
mode:最常见的两种模式,默认模式(空闲)NSDefaultRunLoopMode,UI模式UITrackingRunLoopMode,比如UI相关事件(滑动,点击等),就是主线程RunLoop在UITrackingRunLoopMode模式下进行监视处理的。
1.例如:performSelector方法
//performSelector默认是在当前RunLoop的默认模式下执行方法
[self performSelector:@selector(test) withObject:self];
//可以通过performSelector指定RunLoop模式的方式解决RunLoop问题
[self performSelector:@selector(test) withObject:self afterDelay:3 inModes:@[NSRunLoopCommonModes]];
2.例如:处理滑动时间和定时器冲突的问题
主线程调用timer,添加到NSDefaultRunLoopMode的RunLoop中,此时滑动scrollview,那么timer将停止打印,若添加到UITrackingRunLoopMode的RunLoop中,滑动scrollview,那么timer可以打印,但是不滑动时不打印。原因就是滑动时主线程RunLoop在UITrackingRunLoopMode模式下运行,timer如果放到该模式下就能检测到,如果放到NSDefaultRunLoopMode模式下就检测不到。如果想在两种模式下都检测到,就都需要添加,当然,iOS为我们提供了复合的Mode-NSRunLoopCommonModes,包含上述两种模式。
解决方法就是添加到NSRunLoopCommonModes。
NSTimer * timer=[NSTimer timerWithTimeInterval:1.0 repeats:YES block:^(NSTimer * _Nonnull timer) {
}];
[[NSRunLoop currentRunLoop]addTimer:timer forMode:NSRunLoopCommonModes];
[timer setFireDate:[NSDate distantPast]];
还有种解决办法,就是子线程执行timer,或者使用dispatch_source_set_timer在全局并行队列执行。
dispatch_async(dispatch_get_global_queue(0, 0), ^{
timer=[NSTimer timerWithTimeInterval:1.0 repeats:YES block:^(NSTimer * _Nonnull timer) {
}];
[[NSRunLoop currentRunLoop]addTimer:timer forMode:NSRunLoopCommonModes];
[timer setFireDate:[NSDate distantPast]];
//注意要开启子线程的RunLoop,因为子线程RunLoop默认关闭
[[NSRunLoop currentRunLoop]runUntilDate:[NSDate distantFuture]];
});
timer=dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_global_queue(0, 0));
dispatch_source_set_timer(timer, DISPATCH_TIME_NOW, 1 * NSEC_PER_SEC, 0 * NSEC_PER_SEC);
dispatch_source_set_event_handler(timer, ^{
});
dispatch_resume(timer);
3.开启常驻子线程
NSRunLoop提供了添加item的API,也可以通过添加item让子线程RunLoop活下来。
[NSRunLoop currentRunLoop]addTimer:(nonnull NSTimer *) forMode:(nonnull NSRunLoopMode)
[[NSRunLoop currentRunLoop]addPort:[[NSPort alloc]init] forMode:NSDefaultRunLoopMode];
[NSRunLoop currentRunLoop]addObserver:(nonnull NSObject *) forKeyPath:(nonnull NSString *) options:(NSKeyValueObservingOptions) context:(nullable void *):
例如:让一个子线程处理处理完一个任务之后,再处理另一个任务。
/*单纯使用线程间通讯是做不到的,因为子线程一旦执行完任务就销毁了啊,无法再被唤醒,除非使用该子线程常驻不被销毁。
*/
[self performSelector:@selector(test) onThread:子线程 withObject:nil waitUntilDone:YES];
/*这样就可以考虑在子线程中开启该子线程RunLoop,并让RunLoop做任务让该子线程保持存活,那么做什么任务呢,根据之前的知识,可以是source事件,可以是timer事件,也可以是observer,推荐使用基于端口的source0事件。
*/
NSRunLoop *runloop=[NSRunLoop currentRunLoop];
[runloop addPort:[NSPort port]forMode:NSDefaultRunLoopMode];
[runloop run];
RunLoop的内部逻辑
RunLoop循环内部会不断创建和销毁自动释放池处理一些垃圾数据(使用过的变量等)
自动释放池第一次创建:当RunLoop启动时
自动释放池最后一次销毁:当RunLoop销毁时
自动释放池其他时间创建和销毁:当RunLoop即将进入休眠的时候,释放之前的自动释放池(回收数据),创建新的自动释放池。
RunLoop在常用SDK中应用场景
AFNetworking(相当于一个线程常驻的方式)
这个类是基于 NSURLConnection 构建的,其希望能在后台线程接收 Delegate 回调。为此 AFNetworking 单独创建了一个线程,并在这个线程中启动了一个 RunLoop:
+ (void)networkRequestThreadEntryPoint:(id)__unused object {
@autoreleasepool {
[[NSThread currentThread] setName:@"AFNetworking"];
NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
[runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
[runLoop run];
}
}
+ (NSThread *)networkRequestThread {
static NSThread *_networkRequestThread = nil;
static dispatch_once_t oncePredicate;
dispatch_once(&oncePredicate, ^{
_networkRequestThread = [[NSThread alloc] initWithTarget:self selector:@selector(networkRequestThreadEntryPoint:) object:nil];
[_networkRequestThread start];
});
return _networkRequestThread;
}
RunLoop 启动前内部必须要有至少一个 Timer/Observer/Source,所以 AFNetworking 在 [runLoop run] 之前先创建了一个新的 NSMachPort 添加进去了。通常情况下,调用者需要持有这个 NSMachPort (mach_port) 并在外部线程通过这个 port 发送消息到 loop 内;但此处添加 port 只是为了让 RunLoop 不至于退出,并没有用于实际的发送消息。
据说使用NSURLConnection的老前辈都需要通过RunLoop调试子线程的网络回调。
PerformSelecter
当调用 NSObject 的 performSelecter:afterDelay: 后,实际上其内部会创建一个 Timer 并添加到当前线程的 RunLoop 中。所以如果当前线程没有 RunLoop,则这个方法会失效。
当调用 performSelector:onThread: 时,实际上其会创建一个 Timer 加到对应的线程去,同样的,如果对应线程没有 RunLoop 该方法也会失效。
AsyncDisplayKit
ASDK 仿照 QuartzCore/UIKit 框架的模式,实现了一套类似的界面更新的机制:即在主线程的 RunLoop 中添加一个 Observer,监听了 kCFRunLoopBeforeWaiting 和 kCFRunLoopExit 事件,在收到回调时,遍历所有之前放入队列的待处理的任务,然后一一执行。
参考文章:https://blog.ibireme.com/2015/05/18/runloop/