一、浅识RunLoop
RunLoop这个家伙在iOS开发中,我们一直在用,却从未注意过他,甚至都不从见过他的面孔,那个这个神秘的家伙究竟是做什么的?首先,我们先来观察一下我们的程序运行机制。
无论是面向对象的语言或是面向过程的语言,代码的执行终究是面向过程的。线程也一样,一个线程从开始代码执行,到结束代码销毁。就像HELLO WORLD程序,打印出字符串后程序就结束了,那么,我们的app是如何实现如下这样的机制的呢:app从运行开始一直处于待命状态,接收到类似点击事件等用户交互后执行相应操作,完成后继续等待交互响应,直到我们将程序杀死。通过这个过程的分析,我们可能会猜到,我们执行的主线程一定是在一个死循环中,没有任务的时候进行休眠,接收到任务后被激活执行任务。现在我们可以理解了,这样一个管理线程执行任务的机制就是RunLoop机制,线程在执行中的休眠与激活就是由RunLoop对象进行管理的。
二、RunLoop与线程的关系
上面我们说到,RunLoop是用来管理线程的,那么他们直接有着怎样的关系,又是怎样进行交互的呢。事实上,每一个线程中都有一个Runloop对象,可以通过具体方法获得。这里有一点需要我们注意,官方文档上描述,虽然每一个线程中都可以获取RunLoop对象,但是并不是每一个线程中都有这个实例对象,我们可以这样理解:如果我们不获取runloop,这个runloop就不存在,我们获取时,如果不存在,就会去创建。在主线程中,这个MainRunLoop是默认创建并运行激活的。
三、认识NSRunLoop
NSRunLoop是Cocoa框架中的类,与之对应,在Core Fundation中是CFRunLoopRef类。这两者的区别是前者不是线程安全的,而后者是线程安全的。我们这里只来讨论NSRunLoop的属性和方法:
+ (NSRunLoop *)currentRunLoop;
获取当前线程的RunLoop:有则获取,无则创建
+ (NSRunLoop *)mainRunLoop ;
获取主线程的RunLoop
@property (readonly, copy) NSString *currentMode;
获取当前runloop的执行模式,两种模式如下:
NSString * const NSDefaultRunLoopMode;
默认模式,接收大部分输入源的响应
NSString * const NSRunLoopCommonModes;
多种模式的集合
- (CFRunLoopRef)getCFRunLoop;
获取RunLoop的CFRunLoopRef对象
- (void)addTimer:(NSTimer *)timer forMode:(NSString *)mode;
将定时器添加到runloop中
- (void)addPort:(NSPort *)aPort forMode:(NSString *)mode;
添加输入源端口到runloop中,NSPort对象可以理解为详细的载体,会传递消息与其代理。
- (void)removePort:(NSPort *)aPort forMode:(NSString *)mode;
将某个输入源端口移除
- (NSDate *)limitDateForMode:(NSString *)mode;
获取下个响应时间
解释:例如定时器的执行,其并不是按时间的间隔进行调用方法,而是在定时器注册到runloop中后,runloop会设置一个一个的时间点进行调用,比如10,20,30。如果错过了某个时间点,定时器并不会延时调用,而是直接等待下一个时间点调用,所以定时器并不是精准的。
- (void)acceptInputForMode:(NSString *)mode beforeDate:(NSDate *)limitDate;
在某个时间期限前接收响应
- (void)run;
开始运行
- (void)runUntilDate:(NSDate *)limitDate;
到某个时间点运行
- (BOOL)runMode:(NSString *)mode beforeDate:(NSDate *)limitDate;
在某个期限前运行
四、RunLoop的应用
正如前面所说,我们一直在使用他,却很少见到他。并且,我们在大多数情况下,都不需要显式的创建或者启动RunLoop,有两种情况,我们却必须手动设置它:
1、在分线程中使用定时器
定时器的实现便是基于runloop的,平时我们使用定时器你或许并没有对runloop做什么操作,那是因为主线程的runloop默认是开启运行的,如果我们在分线程中也需要重复执行某一动作,如下:
1
2
3
4
5
6
7
8
9
10
11
12
|
- (
void
)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
queue = dispatch_queue_create(
"myQueue"
, DISPATCH_QUEUE_CONCURRENT);
dispatch_async(queue, ^{
NSTimer * timer = [NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(
time
) userInfo:nil repeats:YES];
});
}
-(
void
)
time
{
NSLog(@
"run"
);
}
|
你会发现,程序运行后并没有打印任何信息,方法并没有被调用,我们必须在线程中手动的执行如下代码:
1
|
[[NSRunLoop currentRunLoop] run];
|
定时器才能正常工作。
2、当你在线程中使用如下方法时
某些延时函数和选择器在分线程中的使用,我们也必须手动开启runloop,这些方法如下:
@interface NSObject (NSDelayedPerforming)
- (void)performSelector:(SEL)aSelector withObject:(id)anArgument afterDelay:(NSTimeInterval)delay inModes:(NSArray *)modes;
- (void)performSelector:(SEL)aSelector withObject:(id)anArgument afterDelay:(NSTimeInterval)delay;
+ (void)cancelPreviousPerformRequestsWithTarget:(id)aTarget selector:(SEL)aSelector object:(id)anArgument;
+ (void)cancelPreviousPerformRequestsWithTarget:(id)aTarget;
- (void)performSelector:(SEL)aSelector target:(id)target argument:(id)arg order:(NSUInteger)order modes:(NSArray *)modes;
- (void)cancelPerformSelector:(SEL)aSelector target:(id)target argument:(id)arg;
- (void)cancelPerformSelectorsWithTarget:(id)target;
五、补充
RunLoop更强大的地方在于对消息的监听,因为CFRunLoopRef的线程安全优势,我们通常会更多使用后者。
细心的你可能会发现,输入源被注册进Runloop中时会有方法进行remove,但是定时器却没有,但是定时器中的invalidate方法可以将其从runloop中移除,正如官方文档的说明:invalidate是重要也是唯一的可以将定时器从runloop的注销的方法,所以如果我们创建了定时器,就一定要在不使用时调用invalidate方法。我不知道apple为何将定时器的方法分离开来,可能的原因是让开发者更少的显式调用runloop的方法,你若是知道原因,恳请留言指导。