RunLoop
顾名思义就是能够一直循环运行的机制。这种机制一般称为“消息循环机制”,其原理大体以下:html
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
是线程安全的。git
NSRunLoopMode
咱们在使用
NSRunLoop
时,会常常须要设置其mode
属性。常见的mode
属性主要包括:NSDefaultRunLoopMode
、UITrackingRunLoopMode
和NSRunLoopCommonModes
。github
程序应用大部分状况下是处于
NSDefaultRunLoopMode
状态,只有当scrollView
滑动时,主线程RunLoop
会自动切换为UITrackingRunLoopMode
状态。安全
不一样的
mode
影响到咱们设置的监听者(好比Timer
或CADisplayLink
)是否会被回调。好比在主线程中,设置Timer
为NSDefaultRunLoopMode
属性,当应用在滑动时,Timer
的方法是不会被回调的,由于滑动过程当中,RunLoop
会切换为UITrackingRunLoopMode
状态,而它只是监听了NSDefaultRunLoopMode
状态。app
在主线程中设置
Timer
或CADisplayLink
,咱们一般都会设置为NSRunLoopCommonModes
属性,表示在NSDefaultRunLoopMode
和UITrackingRunLoopMode
状态下都会进行监听,避免滑动时,没法回调。异步
NSRunLoop
的使用NSTimer
能够尝试将
NSRunLoopCommonModes
改为NSDefaultRunLoopMode
,那么timerFired:
函数在scrollview
滑动的时候,就不会被定时调用了,直到滑动中止。async
- (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
方法并不会调用。为什么在子线程就没法生效呢?oop
a. 线程和RunLoop
是一一对应的,且互相独立,好比主线程对应mainRunLoop
,而子线程也是有它本身所对应的RunLoop
。 b. 主线程的RunLoop
在应用启动的时候就开始run
了,而子线程是须要主动调用其run
方法来启动。post
- (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
是不会的。