美文网首页ios
(三) runLoop 的实际应用场景

(三) runLoop 的实际应用场景

作者: 意一ineyee | 来源:发表于2017-09-15 10:51 被阅读110次

    目录

    一 . NSTimer

    1. 从 timer 的创建方式了解 timer 是否被添加到 runLoop
    2. timer 被添加到 runLoop 不同运行模式的问题
    3. 在子线程中添加 timer
    4. 既然已经说到 timer 了, 就说点额外的, 使用 timer 时应该注意的点

    二. 防止 tableView 滑动卡顿

    三. 线程常驻(线程保活)


    一 . NSTimer

    前面说到 runLoop 的时候, 我们知道 runLoop 被唤醒的事件源里面有一个是 timer, 其实这个 timer 上层对应的就是我们常用的 NSTimer.

    • NSTimer 就是基于 runLoop 在运行的, 当它被添加到 runLoop 之后, runLoop 就会根据它的时间间隔来注册相应的时间点, 到时间点之后 timer 就会唤醒 runLoop 来触发 timer 指定的事件. 因此在使用 timer 的时候我们必须先把 timer 添加到 runLoop 中, 并且还得添加到 runLoop 的指定模式中, 它才能起作用, 否则它是不起作用的.
    • 其次 runLoop 为了节省资源并不会在非常准确的时间调用定时器, 比方说现在有一个 timer 的时间间隔为 1s, runLoop 在第一次循环中执行完 timer 的回调后立马进入了第二个循环, 但是在第二个循环中需要做一个很大计算量的任务, 计算时间超过了 1s, 那么 timer 的第二个回调就会被错过, 这次回调错过了就错过了不会说是把这次回调延后执行, 而是会等到下个时间点直接执行 timer 的第三次回调(因此 timer 里面提供了一个 tolerance 的属性来设置这种偏差的容忍度, 它标示了当注册的时间点到后你可以容忍多大的误差)
    1. 从 timer 的创建方式了解 timer 是否被添加到 runLoop

    timer 的创建方式主要有两种, 一种是 timerWithXXX, 另一种是 scheduledWithXXX, 如下 :

    + (NSTimer *)timerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo;
    + (NSTimer *)timerWithTimeInterval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block;
    
    + (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo;
    + (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block;
    

    两者的 最大区别 就是 : timerWithXXX 只会创建一个定时器, 而 scheduledWithXXX 除了会创建一个定时器之外, 还会把创建的这个定时器自动添加到当前线程的 runLoop 下, 并且是添加到了 runloop 的 defaultMode 下.

    接下来我们验证一下 :

    代码段 1 :
    
    - (void)viewDidLoad {
        [super viewDidLoad];
    
        [NSTimer timerWithTimeInterval:1 target:self selector:@selector(timerAction) userInfo:nil repeats:YES];
    }
    
    - (void)timerAction {
        
        NSLog(@"timerAction");
    }
    

    运行之后, 发现 timer 并不起作用, 那我们用另一种创建方法来试一下 :

    代码段 2 :
    
    - (void)viewDidLoad {
        [super viewDidLoad];
    
    //    [NSTimer timerWithTimeInterval:1 target:self selector:@selector(timerAction) userInfo:nil repeats:YES];
        [NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(timerAction) userInfo:nil repeats:YES];
    }
    
    - (void)timerAction {
        
        NSLog(@"timerAction");
    }
    

    运行, 发现 timer 起作用了.

    那么针对代码段 1, 我们知道 timer 不起作用的原因就是 timer 没有被添加到 runLoop 中, 那接下来我们尝试手动将 timer 添加到 runLoop 中 :

    代码段 3 :
    
    - (void)viewDidLoad {
        [super viewDidLoad];
    
        NSTimer *timer = [NSTimer timerWithTimeInterval:1 target:self selector:@selector(timerAction) userInfo:nil repeats:YES];
        [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
        
    //    [NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(timerAction) userInfo:nil repeats:YES];
    }
    
    - (void)timerAction {
        
        NSLog(@"timerAction");
    }
    

    运行, 发现 timer 起作用了. 因此也就验证了我们的结论 :

    • timerWithXXX 只会创建一个定时器, 我们需要手动将该定时器添加到指定 runLoop 的指定模式下; 而 scheduledWithXXX 除了会创建一个定时器之外, 还会把这个定时器自动添加到当前线程的 runLoop 下, 并且是添加到了 runloop 的 defaultMode 下.
    • timer 必须添加某个 runLoop 的指定模式下才能起作用, 否则它不起作用.
    2. timer 被添加到 runLoop 不同运行模式的问题

    在 1 中我们已经看到了 timer 只有被添加到 runLoop 中才能正常起作用, 那这里我们主要探讨下 timer 添加在 runLoop 不同的模式下会有什么样的情况.

    第一篇文章中我们说到过 每一个 runLoop 都是拥有多个 runLoopMode 的, 不同的情况下 runLoop 工作在不同的模式下, 比方说我们刚打开一个 App 什么也不做, 那么主线程的 runLoop 就已经创建好并启动了, 并且是工作在 defaultMode 下, 那么此时如果我们滑动了一个 scrollView, 主线程的 runLoop 就会切换到 trackingMode 模式下, 这时就会导致 defaultMode 模式下的事件源不被触发, 而只有 trackingMode 下的事件源才会被触发, 我们举例子验证一下 :

    - (void)viewDidLoad {
        [super viewDidLoad];
    
        // 添加一个 scrollView
        UIScrollView *scrollView = [[UIScrollView alloc] initWithFrame:CGRectMake(0, 100, 100, 200)];
        scrollView.backgroundColor = [UIColor redColor];
        scrollView.contentSize = CGSizeMake(100, 1000);
        [self.view addSubview:scrollView];
        
        // 在主线程 runLoop 的 defaultMode 模式下添加一个 timer
        NSTimer *timer = [NSTimer timerWithTimeInterval:1 target:self selector:@selector(timerAction) userInfo:nil repeats:YES];
        [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
    }
    
    - (void)timerAction {
        
        NSLog(@"timerAction");
    }
    

    运行, 会发现 timer 正常工作, 但是当我们滑动 scrollView 的时候, timer 就不起作用了, 正是因为此时主线程的 runLoop 已经切换到了 trackingMode 下, 而 timer 是添加在 defaultMode 下的一个事件源.

    那么如果我们想让 timer 在滑动 scrollView 的时候也正常工作, 那么我们可以采用下面两种办法都可以 :

    // 手动将 timer 添加在 trackingMode 下, 第一篇中我们也说过一个 mode item 是可以添加在多个 runLoopMode 下的
    [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
    [[NSRunLoop currentRunLoop] addTimer:timer forMode:UITrackingRunLoopMode];
    
    // 或者直接将 timer 添加在 commonModes 下, 第一篇中我们也说过默认情况下 commonModes 其实就是 defaultMode 和 trackingMode 的组合
    [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
    
    3. 在子线程中添加 timer

    其实就是线程保活的一个例子, 只不过是在常驻的子线程里添加了一个 timer 事件而已.
    接下来我们再做一个比较有趣的实验, 就是在子线程中添加 timer, 来加深一下我们对 timer 和 runLoop 及 runLoopMode 关系的理解.

    @interface ViewController ()
    
    @property (strong, nonatomic) NSThread *subThread;
    
    @end
    
    @implementation ViewController
    
    - (void)viewDidLoad {
        [super viewDidLoad];
    
        self.subThread = [[NSThread alloc] initWithTarget:self selector:@selector(subThreadAction) object:nil];
        [self.subThread start];
    }
    
    - (void)subThreadAction {
        
        NSLog(@"子线程启动了");
        
        [NSTimer scheduledTimerWithTimeInterval:1 repeats:YES block:^(NSTimer * _Nonnull timer) {
            
            NSLog(@"timerAction");
        }];
    }
    

    运行, 我们发现控制台输出 :

    2017-09-14 10:29:52.173 Test[1806:73768] 子线程启动了
    

    定时器并没有用, 而我们知道 scheduledWithXXX 是会将 timer 添加到当前线程 runLoop 中的并且会添加到 defaultMode 模式下, 但为啥没有起作用呢?

    回想一下第一篇中 runLoop 和线程的关系, 我们知道了, 原来子线程的 runLoop 根本不会自动创建, 而是等你获取的时候才会创建, 并且创建了 runLoop 之后我们还得自己启动(这个会在 三. 线程常驻 中详细讲到). 那么我们启动一下子线程的 runLoop 试一下 :

    - (void)subThreadAction {
        
        NSLog(@"子线程启动了");
    
        // 注意这些时间一定要添加在 runLoop 启动之前
        [NSTimer scheduledTimerWithTimeInterval:1 repeats:YES block:^(NSTimer * _Nonnull timer) {
            
            NSLog(@"timerAction");
        }];
        
        // 为子线程创建一个 runLoop, 并且添加一个空的 port, 工作在 defaultMode 下
        [[NSRunLoop currentRunLoop] addPort:[NSPort port] forMode:NSDefaultRunLoopMode];
        // 启动 runLoop
        [[NSRunLoop currentRunLoop] run];
    }
    

    运行, 发现可以了.

    直接把 timer 作为 mode item 添加到 runLoopMode 也可以

    - (void)subThreadAction {
        
        NSLog(@"子线程启动了");
    
        // 注意这些时间一定要添加在 runLoop 启动之前
        NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:1 repeats:YES block:^(NSTimer * _Nonnull timer) {
            
            NSLog(@"timerAction");
        }];
        
        // 当然这里最好是直接把 timer 作为 mode item 添加到 defaultMode 下, 因为只要有 mode item runLoop 就不会退出循环嘛, timer 和 source 都一样的, 既然已经创建了 timer, 何不直接用
        [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
        // 启动 runLoop
        [[NSRunLoop currentRunLoop] run];
    }
    
    4. 既然已经说到 timer 了, 就说点额外的, 使用 timer 时应该注意的点

    一步一步地看看平常使用 NSTimer 会有什么问题 :

    首先我们会创建两个控制器, ViewController 和 ViewController1, 为的是有个导航栏的栈, 内容如下 :

    @interface ViewController ()
    
    @end
    
    @implementation ViewController
    
    - (void)viewDidLoad {
        [super viewDidLoad];
    
    }
    
    - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
        
        ViewController1 *vc1 = [ViewController1 new];
        [self.navigationController pushViewController:vc1 animated:YES];
    }
    
    @end
    
    @interface ViewController1 ()
    
    @property (weak,  nonatomic) NSTimer *timer;
    
    @end
    
    @implementation ViewController1
    
    - (void)viewDidLoad {
        [super viewDidLoad];
        
        self.timer = [NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(timerAction) userInfo:nil repeats:YES];
        
        NSLog(@"开始===%@", self.timer);
    }
    
    - (void)timerAction {
        
        static int i = 0;
        i ++;
        
        NSLog(@"%d", i);
    }
    
    - (void)dealloc {
        
        NSLog(@"ViewController1 销毁了");
    }
    
    @end
    

    运行, 点击 vc 进入 vc1, 发现一切正常, 一秒钟打印一次.

    这时我们点击返回按钮, 返回 vc, 发现 ViewController1 的 dealloc 方法并没有走, 而且 timer 还有效着, 一直搁那打印呢. 为什么?

    • 重点一 :
      我们查看 NSTimer 的 API 可以发现里面有一段描述是这样的 :
    • Note in particular that run loops maintain strong references to their timers, so you don’t have to maintain your own strong reference to a timer after you have added it to a run loop.

    • 特别声明, 当我们把一个 timer 添加到 runLoop 的时候, runLoop 会对这个 timer 进行一次强引用, 所以你不必对 timer 进行强引用.

    这也是我们为什么在创建 timer 属性的时候, 采用 weak 修饰的原因, 如果你还用 strong 修饰 timer 的话, 又会造成 ViewController1 对 timer 的一个强引用, 这样的话即便调用了 [timer invalidate], timer 的还是会存在一个强引用, timer 照样不会释放, 会继续起作用, 所以千万不要使用 strong 来修饰 timer.

    而我们在创建 timer 的时候 :

    self.timer = [NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(timerAction) userInfo:nil repeats:YES];
    

    又导致 timer 强引用了 target(即 self, 即 ViewController1), 所以 因为 timer 释放不掉, 导致 ViewController1 也无法释放. 所以接下来我们要看看 一个 timer 怎么被释放.

    • 重点二 :
      我们查看 timer 的 invalidate 方法, 该 API 有如下说明 :
    • This method is the only way to remove a timer from an NSRunLoop object. The NSRunLoop object removes its strong reference to the timer, either just before the invalidate method returns or at some later point.

    • If it was configured with target and user info objects, the receiver removes its strong references to those objects as well.

    • You must send this message from the thread on which the timer was installed. If you send this message from another thread, the input source associated with the timer may not be removed from its run loop, which could prevent the thread from exiting properly.

    • invalidate 是将 timer 从 runLoop 中移除的唯一方法, 调用该方法后 runLoop 就会移除对 timer 的强引用, timer 也就释放了

    • 如果你在实例化 timer 的时候配置了它的 target 或者 userInfo, 那么 timer 在释放的同时也会移除对这些对象的强引用

    • 此外, 你得保证你的 timer 是在哪个线程上创建的就得在哪个线程上调用该方法释放 timer, 否则 timer 也不能从 runLoop 上移除掉

    好了, 那我们就知道了, 那就调用一下 - invalidate 呗, 这样 timer 就正常释放, 同时 ViewController1 也就释放掉了, 就完事了.
    但是在哪里调用呢? 既然 ViewController1 的 - dealloc 必须得等 ViewController1 释放之后才走, 那 - dealloc 就放不了了, 那我就在 ViewController1 的 - viewWillDisappear 里呗, 但是这个, 我一侧滑返回就会导致 timer 停掉, 当我松手之后并不想返回, timer 却已经完蛋了, 这个完全不合理. 那我写在 - viewDidDisappear 里可以了吧, 这个倒是还行, - viewDidDisappear 的必须得等界面完全消失之后才会调用的, 侧滑返回期间是不会调用的, 可以. 但是还有一种情况, 就是我在 ViewController1 里面又 push 出了下一级界面, 这时我想让 timer 继续, 那如果把 timer 放在 - viewDidDisappear 里面好像就没办法了, 一 push 出下一级, timer 就废了, 所以这样的情况, 还得考虑, ViewController1 的 - dealloc 真的没法调用吗?(其实可以转移 timer 的 target, 不让 ViewController1 作为 target 不就可以了嘛)

    那, 问题就出在 timer 强引用了 ViewController1, 我们试图释放 timer 间接地释放 ViewController1, 但是似乎有的情况不能满足, 那我们可以不让 timer 强引用 ViewController1 啊, 用 __weak 试下呗 :

    - (void)viewDidLoad {
        [super viewDidLoad];
        
        __weak typeof(self) weakSelf = self;
        self.timer = [NSTimer scheduledTimerWithTimeInterval:1 target:weakSelf selector:@selector(timerAction) userInfo:nil repeats:YES];
        
        NSLog(@"开始===%@", self.timer);
    }
    

    运行, 好像没有什么效果, 为什么? 为什么 block 可以, 这里就不可以呢?

    这里说一下 __weak 是怎么打破循环引用的 :
    未使用 __weak 之前, self 强引用 block, block 内部又会强引用 self, 造成循环引用
    而使用 __weak 是创建了一个指向 self 的弱引用 weakSelf, 那我们在 block 内部捕获 weakSelf 这个指向 self 的弱引用, 而不是直接对 self 进行强引用, 因此可以实现 block 不持有 self
    但是我们为什么又在 block 内部创建了一个对 weakSelf 的强引用呢? 是为了避免在执行 block 内部代码之前 self 被释放掉, 如果 self 被释放掉了, 那么对 weakSelf 也就是 nil 了, 此时若向 weakSelf 发消息会崩掉.
    所以在 block 刚开始执行的时候, 我们立即生成了一个 strongSelf 来指向 weakSelf, 这样其实 block 内部其实是强引用着 self 的, 只不过这个强引用是临时的, 只是在 block 执行期间存在, 因此 block 一执行完, 这个循环引用就打破了, 所以不会存在问题.
    通过上面我们可以得到一个结论 : 一个强引用指向一个弱引用指向的对象相当于是强引用对象
    

    所以 timer 这里, 即便是传给 timer 一个 weakSelf, 但是你点击查看这个 API, 会发现官方介绍说 timer 总是在强引用这个 target, 也就是说 当你传了一个 weakSelf 的时候, 其实是让 timer 强引用了弱引用指向的对象, 所以 timer 还是强引用着 target, 所以这里和直接传 self 的区别就是 : 如果在这两句代码执行完 timer 就能释放掉对 target 的强引用才能达到我们目的(就像 block 在执行完会释放掉内部对 self 的强应用一样), 然而并不能达到目的, 因为 timer 一直被 runLoop 持有这没释放掉, 所以也就没法释放 target. 所以 weakSelf 是没法达到想要的效果的.

    还是不行啊, 这时我们会回想起系统其实是为我们提供了 timer 的 block 创建方式的, 其实苹果也是考虑到了这一点才提供了这种方式, 因此我们可以采用 block 的方式来创建 timer, 这样就可以避免 timer 持有当前控制器了, 试一下 :

    @interface ViewController1 ()
    
    @property (weak,  nonatomic) NSTimer *timer;
    
    @end
    
    @implementation ViewController1
    
    - (void)viewDidLoad {
        [super viewDidLoad];
        
        self.timer = [NSTimer scheduledTimerWithTimeInterval:1 repeats:YES block:^(NSTimer * _Nonnull timer) {
            
            static int i = 0;
            i ++;
            
            NSLog(@"%d", i);
        }];
    
        
        NSLog(@"开始===%@", self.timer);
    }
    
    - (void)dealloc {
        
        [self.timer invalidate];
        NSLog(@"ViewController1 销毁了");
    }
    
    @end
    

    运行, 返回的时候, 发现 ViewController1 可以正常的销毁了, 所以 timer 的 invalidate 也就可以写在 ViewController1 的 dealloc 里了.

    但是还有一个问题, 就是苹果提供的这个 timer 的 block 创建方式仅仅在 iOS10 之后才可以使用, 所以所这种方法也是有点缺陷的, 那怎么办呢?

    + (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block API_AVAILABLE(macosx(10.12), ios(10.0), watchos(3.0), tvos(10.0));
    

    那看来我们还是得用非 block 的创建方式了, 但是又会造成 timer 强引用 ViewController1 无法释放的问题, 怎么办呢?
    既然是因为 timer 的 target 是 ViewController1 才导致无法释放, 那我们转移一下 target, 不让 timer 的target 是 ViewController1 就可以了吧. 怎么个转移法呢?
    我们可以给 NSTimer 添加一个分类, 自定义一个 NSTimer 的创建方法将系统非 block 的创建方法包一层, 让 NSTimer 自己作为自己的 target, 同时在内部操作的 selector 里以 block 的方式调用外界 timer 的实现(因为针对 timer 的使用 block 把代码放在一块更爽一些).

    再进一步, 我们可以使用 runtime 替换掉系统原生的 block 创建 timer 的方法, 让它不仅支持 iOS10, 还可以兼容低版本的系统. 如下 :

    #import "NSTimer+ShiftTarget.h"
    #import <objc/runtime.h>
    
    @implementation NSTimer (ShiftTarget)
    
    + (void)load {
        
        static dispatch_once_t onceToken;
        dispatch_once(&onceToken, ^{
            
            Method originalMethod = class_getInstanceMethod(self, @selector(timerWithTimeInterval:repeats:block:));
            Method swizzledMethod = class_getInstanceMethod(self, @selector(yy_timerWithTimeInterval:repeats:block:));
            class_addMethod(self, @selector(timerWithTimeInterval:repeats:block:), method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod));
            method_exchangeImplementations(originalMethod, swizzledMethod);
            
            Method originalMethod1 = class_getInstanceMethod(self, @selector(scheduledTimerWithTimeInterval:repeats:block:));
            Method swizzledMethod1 = class_getInstanceMethod(self, @selector(yy_scheduledTimerWithTimeInterval:repeats:block:));
            
            class_addMethod(self, @selector(scheduledTimerWithTimeInterval:repeats:block:), method_getImplementation(swizzledMethod1), method_getTypeEncoding(swizzledMethod1));
            method_exchangeImplementations(originalMethod1, swizzledMethod1);
        });
    }
    
    // 对 NSTimer 的创建包一层
    + (NSTimer *)yy_timerWithTimeInterval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block {
        
        // 把外界传进来的 block 变量作为 userInfo 传递给我们内部创建好的 timer
        return [self timerWithTimeInterval:interval target:self selector:@selector(timerFire:) userInfo:block repeats:repeats];
    }
    
    + (NSTimer *)yy_scheduledTimerWithTimeInterval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block {
        
        return [self scheduledTimerWithTimeInterval:interval target:self selector:@selector(timerFire:) userInfo:block repeats:repeats];
    }
    
    + (void)timerFire:(NSTimer *)timer {
        
        // 接收到 userInfo
        if ([timer.userInfo isKindOfClass:NSClassFromString(@"NSBlock")]) {
            
            // 将 userInfo 强转为 block
            void (^block)(NSTimer *timer) = timer.userInfo;
            
            // 调用 block, 然后就会去外界找 block 的实现
            block(timer);
        }
    }
    
    @end
    

    再次使用, 发现 ok 了, 一切问题就这么解决了.

    接下来对 NSTimer 的使用做个总结 :

    • (1) timer 必须添加到 runLoop 中, 而且必须添加到指定的 mode 中才能起作用, 否则不起作用.
    • (2) 使用 timerWithXXX 创建 timer 不会自动将 timer 添加到 runLoop 中, 我们需要手动将 timer 添加到 runLoop 中, 否则 timer 不起作用; 使用 scheduledWithXXX 创建的 timer 会自动添加到当前线程的 runLoop 中, 而且是添加在了 defaultMode 下.
    • (3) 使用非 block 的方式创建 timer 会造成 timer 对 target 的强引用导致 target 无法正常释放, 这样就导致有的情况下我们想要在 target 的 dealloc 方法里释放 timer 不能实现, 但是非 block 创建方式可以兼容iOS10 以下系统; 使用 block 方式创建 timer 可以避免 timer 对 target 的强应用, target 可以正常释放, 我们也可以顺利地在 target 的 dealloc 里面释放 timer, 但是 block 创建方式仅支持 iOS10 之后的系统.
    • (4) 因此为了兼容更低的系统, 我们舍弃了系统原生的 block 创建方式, 采用非 block 创建方式, 但是怎么解决 timer 对 target 的强引用呢?我们通过给 NSTimer 添加一个分类, 让 timer 自己作为自己的 target, 从而通过转移 target 实现了 timer 不对控制器强引用的效果, 并且采用了更优雅的 block 实现方式, 用 runtime 替换掉我们舍弃的系统原生的 block 创建方式. 这样我们继续使用系统 block 创建方式, 因为我们已经用非 block 方式替换了它的实现, 可以兼容低版本系统, 也可以避免 timer 对 target 的强引用, 而且 timer 使用 block 创建方式更为优雅.
    • (5) timer 不要用 strong 修饰, 因为 timer 被添加到 runLoop 的时候会被 runLoop 强引用, 如果你再让 ViewController 强引用 timer, 那么即便调用了 [timer invalidate], timer 照样还是被强引用着, 如果创建方式不对, 则 timer 还会继续存在着, 继续跑着. 使用 weak 修饰之后 [timer invalidate] 之后就不必写 timer == nil 了.
    • (6) 如果想要 timer 仅在当前控制器起作用, 那么我们可以在 -viewDidDisappear 里调用 [timer invalidate], 但是不要在 - viewWillDisappear 写, 侧滑返回会出问题; 如果想要 timer 在当前控制器起作用, 并且 push 出下一级控制器的时候依旧起作用, 就在 - dealloc 调用 [timer invalidate]. invalidate 是将 timer 从 runLoop 中销毁的唯一方式, timer == nil 不行

    二. 防止 tableView 滑动卡顿

    防止 tableView 滑动卡顿我们利用的正是 runLoop 可以工作在不同模式下这一特点 : 我们只需要让 imageView 添加图片的操作在 defaultMode 模式下执行, 而在 trackingMode 下不执行就可以了.

    有时在项目中我们会遇到 tableView 滑动卡顿的现象, 那么此时我们可以通过设置在滑动的时候不加载图片资源, 而等到滑动结束之后再加载, 这样就可以避免 tableView 的滑动卡顿.
    其实核心就这一句代码 :

    [cell.imageView performSelector:@selector(setImage:) withObject:[UIImage imageNamed:@"testImage"] afterDelay:0 inModes:@[NSDefaultRunLoopMode]];
    

    三. 线程常驻(线程保活)

    想要达到线程常驻, 我们只需要在子线程中启动子线程所对应的 runLoop 就可以了.

    这里会一步一步地说明常驻子线程的办法 :

    现在是 第一步 : 先创建一个子线程 :

    - (void)viewDidLoad {
        [super viewDidLoad];
        
        // 创建一个线程并启动
        NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(threadAction:) object:nil];
        [thread start];
    }
    
    @interface ViewController ()
    
    @property (strong, nonatomic) NSThread *thread;
    
    @end
    
    @implementation ViewController
    
    - (void)viewDidLoad {
        [super viewDidLoad];
        
        // 创建一个线程并启动
        self.thread = [[NSThread alloc] initWithTarget:self selector:@selector(threadAction) object:nil];
        [self.thread start];
    }
    
    - (void)threadAction {
        
        NSLog(@"我们开辟的子线程为===%@", [NSThread currentThread]);
    }
    

    并且我们动态改变一下 NSThread 的 dealloc 方法, 以便看它什么时候销毁, 如下 :

    + (void)load {
        
        static dispatch_once_t onceToken;
        dispatch_once(&onceToken, ^{
            
            Method originalMethod = class_getInstanceMethod(self, NSSelectorFromString(@"dealloc"));
            Method targetMethod = class_getInstanceMethod(self, NSSelectorFromString(@"yy_dealloc"));
            
            method_exchangeImplementations(originalMethod, targetMethod);
        });
    }
    
    - (void)yy_dealloc {
        
        [self yy_dealloc];
        NSLog(@"=========== 子线程被销毁了 ===========");
    }
    

    运行, 控制台输出如下 :

    2017-09-13 17:22:47.506 Runloop[8870:442635] 我们开辟的子线程为===<NSThread: 0x600000078000>{number = 3, name = (null)}
    2017-09-13 17:22:47.509 Runloop[8870:442635] =========== 子线程被销毁了 ===========
    

    可见, 这个 线程确实是在执行完任务之后立马就被销毁了.

    第二步 : 既然销毁的这么快, 那我们用 强引用让它销毁的慢点 吧, 如下 :

    @interface ViewController ()
    
    @property (strong, nonatomic) NSThread *thread;
    
    @end
    
    @implementation ViewController
    
    - (void)viewDidLoad {
        [super viewDidLoad];
        
        // 创建一个线程并启动
        self.thread = [[NSThread alloc] initWithTarget:self selector:@selector(threadAction) object:nil];
        [self.thread start];
    }
    
    - (void)threadAction {
        
        NSLog(@"我们开辟的子线程为===%@", [NSThread currentThread]);
    }
    

    运行, 控制台输出如下 :

    2017-09-13 17:23:21.630 Runloop[8904:443661] 我们开辟的子线程为===<NSThread: 0x60800006d640>{number = 3, name = (null)}
    

    可以发现, 没有再走 dealloc 方法了, 线程没有被销毁, 那此时我们 尝试使用一下这个被强引用着的没被销毁的子线程, 如下 :

    - (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
        
        [self performSelector:@selector(runOnSubThread) onThread:self.thread withObject:nil waitUntilDone:NO];
    }
    
    - (void)runOnSubThread {
        
        NSLog(@"%s", __func__);
    }
    

    但是当我们点击屏幕的时候, 会发现 runOnSubThread 方法并不会被触发, 也就是说 即便一个子线程可以保证不被销毁, 但是如果我们想再次给它添加一些操作, 它是不会作用的, 因为一个线程只会从头到尾直直地执行一次啊, 并不会循环的, 此时我们就可以通过 runLoop 来达到线程常驻.

    第三步 :
    我们把 子线程内部(即子线程操作的代码) 改成下面这样就可以了 :

    - (void)threadAction {
        
        NSLog(@"我们开辟的子线程为===%@", [NSThread currentThread]);
        
        [[NSRunLoop currentRunLoop] addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
        [[NSRunLoop currentRunLoop] run];
    }
    

    运行, 点击屏幕, 发现可以正常的在子线程中添加操作了 :

    2017-09-13 17:39:22.576 Runloop[9110:455072] 我们开辟的子线程为===<NSThread: 0x608000266f00>{number = 3, name = (null)}
    2017-09-13 17:39:23.673 Runloop[9110:455072] -[ViewController runOnSubThread]
    2017-09-13 17:39:24.310 Runloop[9110:455072] -[ViewController runOnSubThread]
    2017-09-13 17:39:24.464 Runloop[9110:455072] -[ViewController runOnSubThread]
    

    那这两行代码具体做了些什么啊, 我们来说明一下 :

    [[NSRunLoop currentRunLoop] addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
    [[NSRunLoop currentRunLoop] run];
    
    • 前面我们说过苹果没有为我们提供直接创建 runLoop 的 API, 而是提供了两个获取 runLoop 的方法, 就是 [NSRunLoop mainRunLoop] 和 [NSRunLoop currentRunLoop], 而子线程的 runLoop 正是在这个获取的时候才会被创建的, 你不获取就不会创建(除了主线程的 runLoop 是程序一启动就创建好并启动的), 所以 [NSRunLoop currentRunLoop] 的主要目的就是创建出当前子线程的 runLoop.
    • addPort 的作用是为了保证 runLoop 不会退出, 能够正常循环, 我们前面也说过如果一个 runLoop 的 runLoopMode 里面没有 timer, source, observer, 或者 runLoop 超时, 或者外部强制终止 runLoop 都会导致 runLoop 退出. 所以这里 add 一个空的 port 就是模拟添加了一个 source, 从而保证 runLoop 的正常运行. 当然我们也可以添加 timer 也可以.
    • 上面也说了只有主线程的 runLoop 是默认启动的, 所以 [[NSRunLoop currentRunLoop] run] 就是为了启动子线程的 runLoop.

    那么通过这种方式我们就是实现了常驻线程, 那么我们就可以在此常驻线程中做一些事情了, 一般就是通过下面这个方法在指定的线程中执行指定的事件 :

    [self performSelector:@selector(runOnSubThread) onThread:self.thread withObject:nil waitUntilDone:NO];
    

    实际应用 : 例如我们每隔一段时间就得在子线程中做一些操作, 我们就可以利用常驻线程来实现, 而不是隔一段时间创建一个子线程, 销毁, 再创建.

    相关文章

      网友评论

        本文标题:(三) runLoop 的实际应用场景

        本文链接:https://www.haomeiwen.com/subject/mixlsxtx.html