NSRunLoop对于我们来说相信并不陌生,甚至说对于我们的项目来说就像空气般的自然。
在ios/mac的编码中,我们不需要过多关心代码是如何执行的,比如我们知道当滑动手势时,tableView就会滚动,启动一个NSTimer之后,timer的方法就会定时执行,这些看似是条件反射般的动作,都是由NSRunLoop在帮我们:分发消息。
当然在文章之前我们需要强调一个概念,RunLoop并不是线程,也不是并发机制,但是它在线程中的作用至关重要,它提供了一种异步执行代码的机制。RunLoop的字面意思就是“运行回路”,听起来像是一个循环。实际它就是一个循环,它在循环监听着事件源,把消息分发给线程来执行。
一、RunLoop的概念
NSRunloop只处理两种源,1.输入源 2.时间源。
当它运转时,我们可以把他想成一个由事件在驱动的圆圈,在我们执行事件、手势、时间相应等等的操作时,需要有监听者,这时就有了源的概念。
虽然只处理两种源,但是输入源之下分为了:NSPort、自定义源 、performSelector:OnThread:delay: , 下面我们来简单说说这几种:
1.1 NSPort 基于端口的源
Cocoa和 Core Foundation 为使用端口相关的对象和函数创建的基于端口的源提供了内在支持。Cocoa中你从不需要直接创建输入源。你只需要简单的创建端口对象,并使用NSPort的方法将端口对象加入到run loop。端口对象会处理创建以及配置输入源。
NSPort一般分三种: NSMessagePort(基本废弃)、NSMachPort、 NSSocketPort。 系统中的 NSURLConnection 就是基于 NSSocketPort 进行通信的,所以当在后台线程中使用 NSURLConnection 时,需要手动启动RunLoop, 因为后台线程中的RunLoop默认是没有启动的。
<span style="font-family:Microsoft YaHei;font-size:14px;">NSURLConnection *connection = [[NSURLConnection alloc] initWithRequest:request delegate:self startImmediately:NO];//暂时不运行
[connection scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes];//用NSRunLoopCommonModes
[connection start];</span>
可以用 NSMachPort 作为线程之间的通讯通道。例如在主线程创建子线程时传入一个 NSPort 对象,这样主线程就可以和这个子线程通讯啦,如果要实现双向通讯,那么子线程也需要回传给主线程一个 NSPort
1.2 自定义输入源
在Core Foundation程序中,必须使用CFRunLoopSourceRef类型相关的函数来创建自定义输入源,接着使用回调函数来配置输入源。Core Fundation会在恰当的时候调用回调函数,处理输入事件以及清理源。常见的触摸、滚动事件等就是该类源,由系统内部实现。
一般我们不会使用该种源,第三种情况已经满足我们的需求。
1.3 performSelector:OnThread
Cocoa提供了可以在任一线程执行函数(perform selector)的输入源。和基于端口的源一样,perform selector请求会在目标线程上序列化,减缓许多在单个线程上容易引起的同步问题。而和基于端口的源不同的是,perform selector执行完后会自动清除出run loop。
此方法简单实用,使用也更广泛。以下是API:
<span style="font-family:Microsoft YaHei;font-size:14px;">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:</span>
这些API最后两个是取消当前线程中调用,其他API是在主线程或者当前线程下的Run Loop中执行指定的@selector。
1.4 定时源
定时源就是NSTimer了,定时源在预设的时间点同步地传递消息。因为Timer是基于RunLoop的,也就决定了它不是实时的。
需要注意的是 scheduledTimerWith**** 开头生成的Timer会自动帮你以默认 NSDefaultRunLoopMode 模式加载到当前的Run Loop中,而其他接口生成的Timer则需要你手动使用 -addTimer:forMode 添加到Run Loop中。需要额外注意的是Timer的触发不会让Run Loop返回。
<span style="font-family:Microsoft YaHei;font-size:14px;">[[NSRunLoop currentRunLoop] addTimer:_timer forMode:NSRunLoopCommonModes];
[[NSRunLoop currentRunLoop] runMode:NSRunLoopCommonModes beforeDate:[NSDate distantFuture]];</span>
1.5 伪代码
<span style="font-family:Microsoft YaHei;font-size:14px;">while (true) {
[[NSRunLoop currentRunLoop] runMode:NSRunLoopCommonModes beforeDate:[NSDate distantFuture]];
}</span>
注意:主线程会默认启动一个Runloop,而异步线程不会,while是为了防止RunLoop休眠,在没有“事件源”去驱动的时候,RunLoop会自动启动休眠模式。
二、RunLoop观察者
我们可以通过创建CFRunLoopObserverRef对象来检测RunLoop的工作状态,它可以检测RunLoop的以下几种事件:
Run loop入口
Run loop将要开始定时
Run loop将要休眠
Run loop被唤醒但又在执行唤醒事件前
Run loop终止
Run loop将要处理输入源
NSRunLoopCommonModes,并不是一个真正的runLoopMode, 也就是说这样的写法是错误的:
<span style="font-family:Microsoft YaHei;font-size:14px;">[[NSRunLoop currentRunLoop] runMode:NSRunLoopCommonModes beforeDate:[NSDate distantFuture]];</span>
这个写法并不会让 runloop 运行。
2.2 你应该知道一段,可以阻塞当前线程的代码
<span style="font-family:Microsoft YaHei;font-size:14px;">while (!complete) {
[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
}</span>
你应该知道这样一段代码可以阻塞当前线程,你可能会奇怪:RunLoop就是不停循环来检测源的事件,为什么还要加个 while 呢?
这是因为RunLoop的特性,RunLoop会在没有“事件源”可监听时休眠。也就是说如果当前没有合适的“源”被RunLoop监听,那么这步就跳过了,不能起到阻塞线程的作用,所以还是要加个while循环来维持。
同时注意:因为这段代码可以阻塞线程,所以请不要在主线程写下这段代码,因为它很可能会导致界面卡住。
三、还有一些RunLoop中长遇见的问题
3.1 当滚动UITable'View时,Timer暂停了?
有时我们在,滚动页面时发现NSTimer计时器不进行计时了,当滚动结束以后,才开始继续给出反应,这是因为 RunLoop 正在以 UITrackingRunLoopMode 的模式运行。 这个时候 RunLoop 只会处理与 UITrackingRunLoopMode “绑定”的源, 比如触摸、滚动等事件;而 NSTimer 是默认“绑定”到 NSRunLoopDefaultMode 上的, 所以 Timer 是事情是不会被 RunLoop 处理的,我们的看到的时定时器被暂停了!
常见的解决方案是把Timer“绑定”到 NSRunLoopCommonModes 模式上, 那么Timer就可以与
<span style="font-family:Microsoft YaHei;font-size:14px;">[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
</span>
这样这个Timer就可以和当前组中的两种模式 UITrackingRunLoopMode 和 kCFRunLoopDefaultMode 相关联了。 RunLoop在这两种模式下,Timer都可以正常运行了。
需要注意的是:NSTimer并不是非常准确,因此当我们用NSTimer来完成一些计时任务时,如果需要比较精确的话,最好还是要比较“时间戳”。
3.2 后台的NSURLConnection 不回调,Timer不运行?
我们知道每个线程都有它的RunLoop, 我们可以通过 [NSRunLoop currentRunLoop] 或 CFRunLoopGetCurrent() 来获取。但是主线程和后台线程是不一样的。主线程的 RunLoop 是一直在启动的,而后台线程的RunLoop是默认没有启动的。
后台线程的RunLoop没有启动的情况下的现象就是:“代码执行完,线程就结束被回收了”。就像我们简单的程序执行完就退出了。 所以如果我们希望在代码执行完成后还要保留线程等待一些异步的事件时,比如NSURLConnection和NSTimer, 就需要手动启动后台线程的RunLoop。
启动RunLoop,我们需要设定RunLoop的模式,我们可以设置 NSDefaultRunLoopMode 。 那默认就是监听所有时间源:
<span style="font-family:Microsoft YaHei;font-size:14px;">//Cocoa
[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
//Core Foundation
CFRunLoopRun();</span>
四、参考资料
1.RunLoop入门篇
2.iOS多线程编程Part 1/3 - NSThread & Run Loop
3.苹果官方文档Run Loops
4.Cocoa深入学习:NSOperationQueue、NSRunLoop和线程安全
感谢观看,学以致用更感谢~