RunLoop概念与使用
2020/3/23 23:01:23
本文主要是介绍RunLoop概念与使用,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!
一、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
是不会的。
参考资料
这篇关于RunLoop概念与使用的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!
- 2022-10-05Swift语法学习--基于协议进行网络请求
- 2022-08-17Apple开发_Swift语言地标注释
- 2022-07-24Swift 初见
- 2022-05-22SwiftUI App 支持多语种 All In One
- 2022-05-10SwiftUI 组件参数简写 All In One
- 2022-04-14SwiftUI 学习笔记
- 2022-02-23Swift 文件夹和文件操作
- 2022-02-17Swift中使用KVO
- 2022-02-08Swift 汇编 String array
- 2022-01-30SwiftUI3.0页面反向传值