RunLoop
顾名思义就是可以一直循环运行的机制。这种机制通常称为“消息循环机制”,其原理大致如下:
void loop() { initialize(); while(!quit) { id msg = get_next_message(); process_message(msg); } } 复制代码
在iOS中,
NSRunLoop
和CFRunLoopRef
就是实现“消息循环机制”的对象。其实NSRunLoop
本质是由CFRunLoopRef
封装的,提供了面向对象的API,而CFRunLoopRef
是一些面向过程的C
函数API。两者最主要的区别在于:NSRunLoop
是非线程安全的,意味着你不能用非当前线程去调用当前线程的NSRunLoop
,否则会出现意想不到的错误(You should never try to call the methods of an NSRunLoop object running in a different thread)。而CFRunLoopRef
是线程安全的。
NSRunLoopMode
我们在使用
NSRunLoop
时,会经常需要设置其mode
属性。常见的mode
属性主要包括:NSDefaultRunLoopMode
、UITrackingRunLoopMode
和NSRunLoopCommonModes
。
程序应用大部分情况下是处于
NSDefaultRunLoopMode
状态,只有当scrollView
滑动时,主线程RunLoop
会自动切换为UITrackingRunLoopMode
状态。
不同的
mode
影响到我们设置的监听者(比如Timer
或CADisplayLink
)是否会被回调。比如在主线程中,设置Timer
为NSDefaultRunLoopMode
属性,当应用在滑动时,Timer
的方法是不会被回调的,因为滑动过程中,RunLoop
会切换为UITrackingRunLoopMode
状态,而它只是监听了NSDefaultRunLoopMode
状态。
在主线程中设置
Timer
或CADisplayLink
,我们通常都会设置为NSRunLoopCommonModes
属性,表示在NSDefaultRunLoopMode
和UITrackingRunLoopMode
状态下都会进行监听,避免滑动时,无法回调。
NSRunLoop
的使用NSTimer
可以尝试将
NSRunLoopCommonModes
改成NSDefaultRunLoopMode
,那么timerFired:
函数在scrollview
滑动的时候,就不会被定时调用了,直到滑动停止。
- (void)startTimer { self.timer = [NSTimer timerWithTimeInterval:1.0 target:self selector:@selector(timerFired:) userInfo:nil repeats:YES]; [[NSRunLoop mainRunLoop] addTimer:self.timer forMode:NSRunLoopCommonModes]; } - (void)timerFired:(NSTimer *)timer { NSLog(@"fired timer in %@", [NSDate date]); } 复制代码
CADisplayLink
- (void)startDisplayLink { self.displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(displayLinkTick:)]; [self.displayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes]; } - (void)displayLinkTick:(CADisplayLink *)link { NSLog(@"tick display link in %@", [NSDate date]); } 复制代码
performSelector:withObject:afterDelay:
这里看似并没有使用到
NSRunLoop
,但其实是它内部会创建一个Timer
,并加Timer
加入到当前线程对应的NSRunLoop
中(This method sets up a timer to perform the aSelector message on the current thread’s run loop. )。
- (void)performSel { [self performSelector:@selector(performSelFired:) withObject:@"perform" afterDelay:3.0 inModes:@[NSRunLoopCommonModes]]; NSLog(@"performSelector start in %@", [NSDate date]); } - (void)performSelFired:(NSString *)object { NSLog(@"performSelector with obj: %@ in %@", object, [NSDate date]); } 复制代码
NSRunLoop
- (void)performInThread { __weak typeof(self) wSelf = self; dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ NSLog(@"performInThread start in %@", [NSDate date]); [wSelf performSelector:@selector(threadFired:) withObject:@"thread perform" afterDelay:3.0 inModes:@[NSDefaultRunLoopMode]]; }); } - (void)threadFired:(NSString *)object { NSLog(@"performInThread with obj: %@ in %@", object, [NSDate date]); } 复制代码
运行该代码,会发现threadFired
方法并不会调用。为何在子线程就无法生效呢?
a. 线程和RunLoop
是一一对应的,且互相独立,比如主线程对应mainRunLoop
,而子线程也是有它自己所对应的RunLoop
。
b. 主线程的RunLoop
在应用启动的时候就开始run
了,而子线程是需要主动调用其run
方法来启动。
- (void)performInThread { __weak typeof(self) wSelf = self; dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ NSLog(@"performInThread start in %@", [NSDate date]); NSRunLoop *runLoop = [NSRunLoop currentRunLoop]; [wSelf performSelector:@selector(threadFired:) withObject:@"thread perform" afterDelay:3.0 inModes:@[NSDefaultRunLoopMode]]; [runLoop run]; }); } 复制代码
获取到子线程对应的RunLoop
后,调用其run
方法就可以看到threadFired
被调用了。注意:RunLoop
是无法主动被创建的,只能通过在currentRunLoop
或mainRunLoop
获取到对应的RunLoop
。
假设在这里做一个修改,将[runLoop run];
方法提前,如下:
NSRunLoop *runLoop = [NSRunLoop currentRunLoop]; [runLoop run]; [wSelf performSelector:@selector(threadFired:) withObject:@"thread perform" afterDelay:3.0 inModes:@[NSDefaultRunLoopMode]]; 复制代码
修改后,会发现threadFired
函数又无法被调用了。这又是什么原因?
这时因为NSRunLoop
是需要source event
才会一直运行的,否则运行完会被终止。这里通常会有两种source event
:a.异步事件,通常为addPort
或performSelector:onThread
方法;b.Timer事件
,通常为addTimer
或performSelector:afterDelay
等方法。
所以,提前调用run
方法时,RunLoop
没有设置任何source event
,所以会立即终止,而执行到下面的performSelector
方法时,这时虽然设置了timer source
,但RunLoop
已经终止,自然也就无法响应了。
addPort
通过
addPort
方法可以使RunLoop
监听某个端口的事件,从而保证其一直运行。
- (void)addPort { __weak typeof(self) wSelf = self; dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ NSLog(@"start run addPort in %@", [NSDate date]); wSelf.thread = [NSThread currentThread]; NSRunLoop *runLoop = [NSRunLoop currentRunLoop]; [runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode]; [runLoop run]; }); for (NSInteger i = 1; i <= 3; i ++) { dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(5.0 * i * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ NSLog(@"start receive port msg in %@", [NSDate date]); [wSelf performSelector:@selector(receiveMsg) onThread:wSelf.thread withObject:nil waitUntilDone:NO]; }); } } - (void)receiveMsg { NSLog(@"receive msg in thread in %@", [NSDate date]); } 复制代码
这里通过注册NSMachPort
端口,来保证该线程的RunLoop
一直处于运行状态。
这里有个问题,NSRunLoop
设置的mode
为NSDefaultRunLoopMode
,那么是不是意味着当应用有scrollView
滑动时,会导致无法响应?答案是不会!这里可能很容易产生一个误解:只有mode
设置为NSRunLoopCommonModes
,才能保证在scrollView
滑动的情况下也会响应。其实是不对的,应该有个前提条件:主线程。因为只有mainRunLoop
才会在滑动时,切换为UITrackingRunLoopMode
,子线程中的RunLoop
是不会的。