一、RunLoop概念
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是不会的。