NSRunLoop的使用
2020-08-25 本文已影响0人
想聽丿伱說衹愛我
版本:iOS13.6
本文仅介绍api的作用
若想了解NSRunloop,可点击NSRunloop 简单细说。
若想了解NSRunloop的使用场景,可点击NSRunLoop-使用场景分析
NSRunLoop
@interface NSRunLoop : NSObject
//返回当前线程的运行循环
//若当前线程没有运行循环,系统会先创建再返回
//刚创建的运行循环是没有运行的,需要调用run来使其运行
@property (class, readonly, strong) NSRunLoop *currentRunLoop;
//返回主线程的运行循环
//主线程的运行循环是一直运行的
//在main.h文件的UIApplicationMain方法就已经被创建
@property (class, readonly, strong) NSRunLoop *mainRunLoop
//返回运行循环的模式
//详见下面的NSRunLoopMode
@property (nullable, readonly, copy) NSRunLoopMode currentMode;
//返回底层的CoreFoundation的运行循环
- (CFRunLoopRef)getCFRunLoop CF_RETURNS_NOT_RETAINED;
//向运行循环添加定时器
//常用的NSTimer的scheduled方法默认是向主线程的运行循环中添加定时器
//但是以NSDefaultRunLoopMode模式添加的
//若要将定时器从运行循环中移除,可调用定时器的invalidate方法
//详见下面的例1
- (void)addTimer:(NSTimer *)timer forMode:(NSRunLoopMode)mode;
//向运行循环中添加一个端口
//实际作用就是向运行循环添加一个任务。
//详见下面的例2
- (void)addPort:(NSPort *)aPort forMode:(NSRunLoopMode)mode;
//从运行循环中移动一个端口
- (void)removePort:(NSPort *)aPort forMode:(NSRunLoopMode)mode;
//在该模式下执行一次运行循环,并返回下一个定时器计划启动的日期
//如果该模式下没有任务则输出nil
//详见下面的例3
- (nullable NSDate *)limitDateForMode:(NSRunLoopMode)mode;
//在该模式下执行一次运行循环,直到时间到达时强制退出循环。
//若循环中没有任务则也会提前退出循环
//详见下面的例4
- (void)acceptInputForMode:(NSRunLoopMode)mode beforeDate:(NSDate *)limitDate;
@end
- NSRunLoopMode
//默认的模式,系统大部分操作都会运行在该模式下
NSDefaultRunLoopMode
//当滚动scorllView时系统会将模式从NSDefaultRunLoopMode变成该模式
//停止滚动后自动变回为NSDefaultRunLoopMode
UITrackingRunLoopMode
//该模式相当于UITrackingRunLoopMode和NSDefaultRunLoopMode的集合
NSRunLoopCommonModes
//该模式在app启动时使用,之后会变为NSDefaultRunLoopMode
UIInitializationRunLoopMode
- 例1
static NSInteger count = 0;
[NSTimer scheduledTimerWithTimeInterval:1 repeats:YES block:^(NSTimer * _Nonnull timer) {
NSLog(@"%ld", count++);
}];
输出:
2020-08-25 10:05:44.753718+0800 0
2020-08-25 10:05:45.753350+0800 1
2020-08-25 10:05:46.753257+0800 2
2020-08-25 10:05:53.286614+0800 3
2020-08-25 10:05:53.753050+0800 4
2020-08-25 10:05:54.753188+0800 5
2020-08-25 10:05:55.753261+0800 6
解释:
可见2-3的时候,时间是46-53,这是因为此时我在拖动tableView。
scheduledTimerWithTimeInterval实际上是向主线程的运行循环添加定时器,而模式是NSDefaultRunLoopMode。
当拖动tableView时,系统会自动将模式变成UITrackingRunLoopMode。
使得在NSDefaultRunLoopMode模式下运行的定时器不触发。
若要解决这个问题,可将定时器添加到NSRunLoopCommonModes下
static NSInteger count = 0;
NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:1 repeats:YES block:^(NSTimer * _Nonnull timer) {
NSLog(@"%ld", count++);
}];
[[NSRunLoop mainRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
输出:
2020-08-25 10:13:36.345133+0800 0
2020-08-25 10:13:37.345137+0800 1
2020-08-25 10:13:38.345402+0800 2
2020-08-25 10:13:39.346007+0800 3
2020-08-25 10:13:40.345442+0800 4
2020-08-25 10:13:41.345892+0800 5
2020-08-25 10:13:42.345575+0800 6
可见不论怎么拖动tableView,定时器都会每秒调用block一次
- 例2
//全局并发队列
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
NSPort *port = [NSPort port];
dispatch_async(queue, ^{
//创建该线程的运行循环
NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
//向运行循环中添加一个端口,将运行循环有事可做,而不会从运行中退出
[runLoop addPort:port forMode:NSDefaultRunLoopMode];
//移除端口
// [runLoop removePort:port forMode:NSDefaultRunLoopMode];
//运行runLoop
[runLoop run];
//下面的代码不会执行,除非运行循环没有任务可做而导致退出才会执行run后面的代码
NSLog(@"runLoop退出");
});
若将移除端口的注释取消掉,run时因为运行循环中没有任务,所有run会直接返回,从而执行下面的log
- 例3
//获取下一次的运行时间
__block NSDate *date = [[NSRunLoop mainRunLoop] limitDateForMode:NSDefaultRunLoopMode];
NSLog(@"%@", date);
__block NSInteger count = 0;
//在主线程的运行循环中添加定时器
[NSTimer scheduledTimerWithTimeInterval:1 repeats:YES block:^(NSTimer * _Nonnull timer) {
count++;
if (count >= 3) {
//3次后退出定时器
[timer invalidate];
}
//获取下一次的运行时间
date = [[NSRunLoop mainRunLoop] limitDateForMode:NSDefaultRunLoopMode];
NSLog(@"%@", date);
}];
输出:
2020-08-25 11:01:34.317331+0800 Tue Aug 25 11:01:34 2020
2020-08-25 11:01:35.318131+0800 Tue Aug 25 11:01:35 2020
2020-08-25 11:01:36.318621+0800 Tue Aug 25 11:01:36 2020
2020-08-25 11:01:37.318028+0800 Tue Aug 25 11:01:44 2020
解释:
最后的打印时间为37秒 而输出的下一次的运行时间为44秒。
因为此时定器器已经失效,而从运行循环中移除,所以不会再每隔1秒运行一次。
- 例4
dispatch_async(dispatch_get_global_queue(0, 0), ^{
//创建该线程的运行循环
NSLog(@"%@", [NSDate date]);
NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:1 repeats:YES block:^(NSTimer * _Nonnull timer) {
NSLog(@"timer");
}];
[runLoop addTimer:timer forMode:NSDefaultRunLoopMode];
[runLoop acceptInputForMode:NSDefaultRunLoopMode beforeDate:[NSDate dateWithTimeIntervalSinceNow:5]];
NSLog(@"%@", [NSDate date]);
});
输出:
Tue Aug 25 11:21:11 2020
timer
timer
timer
timer
Tue Aug 25 11:21:16 2020
5秒后到了限定时间,即使循环还有事可做,也会强制退出。
NSRunLoopConveniences扩展
@interface NSRunLoop (NSRunLoopConveniences)
//开始运行循环
//若循环中没有任务,该方法会直接返回,退出循环
//主线程的运行循环是默认开启的,因为主线程一直有任务可做,所以不会退出循环
//该方法相当于重复调用runMode:beforeDate:方法来保持无限循环
//不要在主线程中调用该方法,会一直卡住
- (void)run;
//开始运行循环直到时间到达时强制退出循环。
//相当于run方法新增一个时间来防止无限循环。
- (void)runUntilDate:(NSDate *)limitDate;
//在该模式下开始运行循环直到时间到达时强制退出循环。
//若运行循环的任务处理完成或到达限制时间则返回YES
//若循环无法启动则返回NO,即开启一个无任务的循环会返回NO
//该方法与acceptInputForMode的作用相同,只不过多了返回值
//详见下面的例5
- (BOOL)runMode:(NSRunLoopMode)mode beforeDate:(NSDate *)limitDate;
//在该模式下执行block
//iOS10以后可用
- (void)performInModes:(NSArray<NSRunLoopMode> *)modes block:(void (^)(void))block;
//执行block(默认模式下)
//iOS10以后可用
- (void)performBlock:(void (^)(void))block;
@end
- 例5
dispatch_async(dispatch_get_global_queue(0, 0), ^{
//创建该线程的运行循环
NSLog(@"%@", [NSDate date]);
NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
//此时运行循环里没有任务 run会直接返回 退出循环
[runLoop run];
NSLog(@"%@", [NSDate date]);
//添加一个端口 让循环不退出
[runLoop addPort:[NSPort port] forMode:NSDefaultRunLoopMode];
//3秒后强制退出循环
[runLoop runUntilDate:[NSDate dateWithTimeIntervalSinceNow:3]];
NSLog(@"%@", [NSDate date]);
//4秒后强制退出循环
BOOL run = [runLoop runMode:NSDefaultRunLoopMode beforeDate:[NSDate dateWithTimeIntervalSinceNow:4]];
NSLog(@"%@ %@", @(run), [NSDate date]);
});
输出:
Tue Aug 25 14:19:23 2020
Tue Aug 25 14:19:23 2020
Tue Aug 25 14:19:26 2020
1 Tue Aug 25 14:19:30 2020
解释:
runMode方法返回YES 表示时间已到
__block BOOL shouldKeepRunning = YES;
dispatch_async(dispatch_get_global_queue(0, 0), ^{
NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
//添中一个端口 让循环有任务可做
[runLoop addPort:[NSPort port] forMode:NSDefaultRunLoopMode];
NSLog(@"停止代码运行");
while (shouldKeepRunning && [runLoop runMode:NSDefaultRunLoopMode beforeDate:[NSDate dateWithTimeIntervalSinceNow:1]]) {
NSLog(@"暂停");
};
NSLog(@"重新开始代码运行");
});
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
//3秒后置为NO 将while停止 并使while下面的代码能重新执行
shouldKeepRunning = NO;
});
输出:
2020-08-25 14:35:49.205727+0800 停止代码运行
2020-08-25 14:35:50.206808+0800 暂停
2020-08-25 14:35:51.207092+0800 暂停
2020-08-25 14:35:52.207612+0800 暂停
2020-08-25 14:35:52.207788+0800 重新开始代码运行
解释:
通过while将代码运行暂停。
可通过将shouldKeepRunning置为NO或等运行循环的任务完成后调用runMode时返回NO来跳出while,
从而使代码能继续运行。
NSOrderedPerform扩展
@interface NSRunLoop (NSOrderedPerform)
//在运行循环上执行选择器,该循环必须是正在运行的
//target 要执行选择器 最好是没有返回值的且参数是一个id类型的
//arg 选择器的参数
//order 优先级 值越小优先级越高
//如比生成自动释放池时order为-2147483647 销毁自动释放池时order为2147483647
//modes 模式的数组 当循环运行在该模式下时,才会执行选择器
//详见下面的例6
- (void)performSelector:(SEL)aSelector target:(id)target argument:(nullable id)arg order:(NSUInteger)order modes:(NSArray<NSRunLoopMode> *)modes;
//取消运行循环要执行的选择器,该循环必须是正在运行的
//必须aSelector target arg与performSelector方法的这三个参数全都相同才能取消
- (void)cancelPerformSelector:(SEL)aSelector target:(id)target argument:(nullable id)arg;
//取消运行循环要执行的选择器,该循环必须是正在运行的
//这些选择器是属于对象target的
- (void)cancelPerformSelectorsWithTarget:(id)target;
@end
- 例6
dispatch_async(dispatch_get_global_queue(0, 0), ^{
NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
//添加一个端口 使循环一直运行
[runLoop addPort:[NSPort port] forMode:NSDefaultRunLoopMode];
//执行选择器test: 循环必须是运行的
[runLoop performSelector:@selector(test:) target:self argument:@"currentRunLoop" order:0 modes:@[NSDefaultRunLoopMode]];
//取消所有在self上的执行选择器的操作
// [runLoop cancelPerformSelectorsWithTarget:self];
[runLoop run];
});
- (void)test:(NSString *)arg {
NSLog(@"%@", arg);
}
输出:
currentRunLoop
通常是使用主线程的运行循环来执行该方法,因为运行循环是一直运行的,而不用在线程中新建运行循环
[[NSRunLoop mainRunLoop] performSelector:@selector(test:) target:self
argument:@"mainRunLoop" order:0 modes:@[NSDefaultRunLoopMode]];
//单独取消选择器
[[NSRunLoop mainRunLoop] cancelPerformSelector:@selector(test:) target:self
argument:@"mainRunLoop"];
//取消self全部选择器
//[[NSRunLoop mainRunLoop] cancelPerformSelectorsWithTarget:self];
NSObject的NSDelayedPerforming扩展
@interface NSObject (NSDelayedPerforming)
//在当前线程的运行循环上延迟执行选择器,该循环必须是正在运行的
//target 要执行选择器 最好是没有返回值的且参数是一个id类型的
//arg 选择器的参数
//delay 延迟时间 单位为秒
//modes 模式的数组 当循环运行在该模式下时,才会执行选择器
//该方法就是向循环中添加一个任务,所以不用给循环添加一个端口来使循环运行
//详见下面的例7
- (void)performSelector:(SEL)aSelector withObject:(nullable id)anArgument afterDelay:(NSTimeInterval)delay inModes:(NSArray<NSRunLoopMode> *)modes;
//与上面一致 只向NSDefaultRunLoopMode模式添加任务
- (void)performSelector:(SEL)aSelector withObject:(nullable id)anArgument afterDelay:(NSTimeInterval)delay;
//取消运行循环要执行的选择器,循环必须是正在运行的
//必须aTarget aSelector anArgument与performSelector方法的这三个参数全都相同才能取消
+ (void)cancelPreviousPerformRequestsWithTarget:(id)aTarget selector:(SEL)aSelector object:(nullable id)anArgument;
//取消运行循环要执行的选择器,循环必须是正在运行的
//这些选择器是属于对象aTarget的
+ (void)cancelPreviousPerformRequestsWithTarget:(id)aTarget;
@end
- 例7
dispatch_async(dispatch_get_global_queue(0, 0), ^{
NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
NSLog(@"performSelector start");
[self performSelector:@selector(test:) withObject:@"currentRunLoop" afterDelay:2];
NSLog(@"performSelector end");
//通过该方法取消对应的选择器执行
// [self.class cancelPreviousPerformRequestsWithTarget:self selector:@selector(test:) object:@"currentRunLoop"];
//通过该方法取消self所有的选择器执行
// [self.class cancelPreviousPerformRequestsWithTarget:self];
// NSLog(@"performSelector cancle");
[runLoop run];
NSLog(@"runLoop exit");
});
输出:
2020-08-25 15:41:39.226095+0800 performSelector start
2020-08-25 15:41:39.226272+0800 performSelector end
2020-08-25 15:41:41.228094+0800 currentRunLoop
2020-08-25 15:41:41.228286+0800 runLoop exit
解释:
该方法不用添加一个端口来使循环一直运行,performSelector方法本身就是向循环添中一个任务。
performSelector end打印时间在currentRunLoop前面2秒,说是performSelector是立即返回的。
runLoop exit打印时间紧跟在currentRunLoop之后,表示当选择器执行完成后,运行循环就自动停止。
当取消注释后,输出:
performSelector start
performSelector end
performSelector cancle
runLoop exit
该选择器不再执行。
该方法也通常使用在主线程中,代替dispatch_after来延迟选择器的执行。
[self performSelector:@selector(test:) withObject:@"mainRunLoop" afterDelay:2];