runtime.runloop

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
//默认的模式,系统大部分操作都会运行在该模式下
NSDefaultRunLoopMode 
//当滚动scorllView时系统会将模式从NSDefaultRunLoopMode变成该模式
//停止滚动后自动变回为NSDefaultRunLoopMode
UITrackingRunLoopMode
//该模式相当于UITrackingRunLoopMode和NSDefaultRunLoopMode的集合
NSRunLoopCommonModes
//该模式在app启动时使用,之后会变为NSDefaultRunLoopMode
UIInitializationRunLoopMode
    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一次
    //全局并发队列
    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
    //获取下一次的运行时间
    __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秒运行一次。
    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
    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
    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
    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];
上一篇 下一篇

猜你喜欢

热点阅读