美文网首页iOS程序员移动端开发
iOS 定时器(NSTimer/CADisplayLink)循环

iOS 定时器(NSTimer/CADisplayLink)循环

作者: JustEverOnce | 来源:发表于2019-03-11 16:57 被阅读114次

    前言:最近呢,稍微有点空闲的时间,就细看了下年前接手的代码,年前基本上都是新增功能,改一些前任所留下来的bug,上面一直忙着催进度,所以没有细看具体的各模块代码,看到相关定时器的代码,感觉需要改进的部分还有很大空间,这份代码经历的人手比较多,时间也比较久远,所以整份代码的质量也是层次不齐,看到这里,就简单整理下定时器相关的内容,这里主要侧重循环引用部分,发现问题 --> 部分解决方案 --> 初步解决方案 --> 最终合理的解决方案

    iOS定时器分类:

    NSTimer

    • 创建方式(类对象创建,不需要手动添加至RunLoop)
        /**
         NSTimer
    
         @param NSTimeInterval 等待时间(单位是秒,即就是每个几秒执行一次)
         @param target 执行对象
         @param selector 执行方法(定时器执行的方法)
         @param userInfo 标识信息(一般不用)
         @param repeats 是否重复执行
         @return NSTimer对象(定时器)
         */
        NSTimer* timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(timerAction:) userInfo:nil repeats:YES];
    
        /**
         NSTimer block的创建方式
    
         @param TimerInterval 等待时间
         @param repeats 是否重复执行
         @param block 执行的代码
         @return NSTimer对象
         */
        NSTimer* timer = [NSTimer scheduledTimerWithTimeInterval:1.0 repeats:YES block:^(NSTimer * _Nonnull timer) {
           
            //重复执行的代码
        }];
    
    #pragma mark --- 这种方式比较冗余,所以开发中使用较少(个人经验)
        SEL action = @selector(timerAction:);
        NSInvocation* invocation = [NSInvocation invocationWithMethodSignature:[[self class] instanceMethodSignatureForSelector:action]];
        [invocation setTarget:self];
        [invocation setSelector:action];
        NSTimer* timer = [NSTimer scheduledTimerWithTimeInterval:1.0 invocation:invocation repeats:YES];
    
    • 创建方式(类对象创建,需要手动添加至RunLoop)
    #pragma mark --- 参数参考上述参数
        NSTimer* timer = [NSTimer timerWithTimeInterval:1.0 target:self selector:@selector(timerAction:) userInfo:nil repeats:YES];
        [[NSRunLoop mainRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
    
    #pragma mark --- 参数参考上述参数
        NSTimer* timer = [NSTimer timerWithTimeInterval:1.0 repeats:YES block:^(NSTimer * _Nonnull timer) {
    
            NSLog(@"-----");
        }];
        [[NSRunLoop mainRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
    
    #pragma mark --- 参数参考上述参数
        SEL action = @selector(timerAction:);
        NSInvocation* invocation = [NSInvocation invocationWithMethodSignature:[[self class] instanceMethodSignatureForSelector:action]];
        [invocation setTarget:self];
        [invocation setSelector:action];
        NSTimer* timer = [NSTimer timerWithTimeInterval:1.0 invocation:invocation repeats:YES];
        [[NSRunLoop mainRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
    
    • 创建方式(实例方法创建)
        NSTimer* timer = [[NSTimer alloc] initWithFireDate:[NSDate date] interval:1.0 target:self selector:@selector(timerAction:) userInfo:nil repeats:YES];
        [[NSRunLoop mainRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
    
        NSTimer* timer = [[NSTimer alloc] initWithFireDate:[NSDate date] interval:1.0 repeats:YES block:^(NSTimer * _Nonnull timer) {
    
            NSLog(@"----");
        }];
    
        [[NSRunLoop mainRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
    

    NSTimer创建小结:

    1.上述创建方式(类对象创建,不需要手动添加至RunLoop)最终所得的NSTimer对象会被默认添加至NSRunLoop中,所以创建之后就可以启动,也就是执行对应的timerAction方法
    2.其他的创建方式最终得到的NSTimer对象需要手动添加至对应的NSRunLoop中,不然定时器无法启动,绝大多数情况我们使用[[NSRunLoop mainRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];即可
    3.NSTimer创建方式,以scheduledTimerWithTimeInterval开头的方式不需要手动添加至NSRunLoop中,否则需要手动添加至NSRunLoop
    4.[NSRunLoop mainRunLoop] addTimer:<#(nonnull NSTimer *)#> forMode:(nonnull NSRunLoopMode)这里的NSRunLoopMode解释一下,API里面有2种方式:NSDefaultRunLoopMode/NSRunLoopCommonModes

    FOUNDATION_EXPORT NSRunLoopMode const NSRunLoopCommonModes API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0));
    

    记得这个NSRunLoopMode底层不止2中,这个后面有机会再说,NSDefaultRunLoopMode为默认模式,上述1.1的创建方式下NSTimer
    对应的NSRunLoopMode模式就是NSDefaultRunLoopMode,如下所示


    image

    (如果页面有交互,最常见的如UIScrollerView、UITableview、UITextView等滑动,定时器会在互动的过程中暂定工作,滑动结束后NSTimer会再次工作),NSRunLoopCommonModes模式下可以避免这种情况,页面滑动不会对定时器产生影响

    NSTimer 其他常用API

    • @property (copy) NSDate *fireDate;
      如果没有设置改属性,则默认从当前时间启动,该属性常用来管理定时器的启动与停止(这里是停止,而不是销毁定时器)
    self.timer.fireDate = [NSDate distantFuture];//停止定时器
    
    self.timer.fireDate = [NSDate distantPast];//重新启动定时器
    
    • @property (readonly) NSTimeInterval timeInterval;
      定时器的时间间隔,即就是上面设置的TimerInterval
    • - (void)invalidate;
      销毁定时器,一般当我们不用定时器的时候,调用此方法对定时器惊醒销毁,减少不必要的开销(定时器对性能消耗比较大,同时也比较电量)
    • @property (readonly, getter=isValid) BOOL valid;
      定时器是否可用,1表示可用,0表示不可用

    CADIsplayLink

    • 创建方式
    #pragma mark --- 参数参考上述参数
        CADisplayLink* displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(displayLinkAction:)];
        [displayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
    

    注意:同样创建需要添加至NSRunLoop中,对应的NSRunLoopMode同NSTimer一样

    • 其他常用API
    /* Removes the receiver from the given mode of the runloop. This will
     * implicitly release it when removed from the last mode it has been
     * registered for. */
    
    - (void)removeFromRunLoop:(NSRunLoop *)runloop forMode:(NSRunLoopMode)mode;
    
    /* Removes the object from all runloop modes (releasing the receiver if
     * it has been implicitly retained) and releases the 'target' object. */
    
    - (void)invalidate;
    
    • 1.removeFromRunLoop 从给定的RunLoop模式(看注释的意思是会有特殊的情况下,该定时器可能加入了多种RunLoop模式),并且最后一个Runloop模式下时会被释放
      1. invalidate 从所有的RunLoop模式中移除,并且被释放(一般我都用这种,比较简单快捷,参数比较少,同NSTimer类似)
    • 3.frameInterval/preferredFramesPerSecond
     * display link fires. Default value is one, which means the display
     * link will fire for every display frame. Setting the interval to two
     * will cause the display link to fire every other display frame, and
     * so on. The behavior when using values less than one is undefined.
     * DEPRECATED - use preferredFramesPerSecond. */
    
    @property(nonatomic) NSInteger frameInterval
      API_DEPRECATED("preferredFramesPerSecond", ios(3.1, 10.0), 
                     watchos(2.0, 3.0), tvos(9.0, 10.0));
    
    /* Defines the desired callback rate in frames-per-second for this display
     * link. If set to zero, the default value, the display link will fire at the
     * native cadence of the display hardware. The display link will make a
     * best-effort attempt at issuing callbacks at the requested rate. */
    
    @property(nonatomic) NSInteger preferredFramesPerSecond
        API_AVAILABLE(ios(10.0), watchos(3.0), tvos(10.0));
    

    frameInterval 在iOS10.0以后弃用,改用preferredFramesPerSecond,看注释这个可以大概可以当做回调速率,以每秒的帧数为单位,如果设置为0(默认值)的时候与硬件相关,iOS设备帧数为60FPS,所以就可以解释一秒调用60次了,更改该属性即可控制定时器的间隔时间,例如:如果一秒钟调用2次,那么可以设置preferredFramesPerSecond为30(60/30),一秒钟调用1次,可以设置preferredFramesPerSecond为1(60/60),注意如果preferredFramesPerSecond小于1,那就相当于默认值。

      1. 还有一些其他的API,可以参考相关的文档,不怎么常用,这里不做解释
        CADisplayLink小结:
        4.1、创建必须手动添加至对应的NSRunLoop中,添加让是不同于NSTimer(NSTimer是在RunLoop对应的模式下添加定时器,CADisplayLink是添加到对应模式的RunLoop中,这是定时器的方法),如下所示:[displayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
        4.2、默认情况下,CADispalyLink定时器调用时间间隔与屏幕渲染保持一致,相对NSTimer精度要高很多,目前iOS设备为60FPS,即就是一分钟屏幕渲染60次,也就是说CADisplayLink定时器每隔1/60秒调用一次,因此适合做一些UI的重绘,动画等,个人觉得使用场景不如NSTimer广泛

    定时器基本就简单介绍到这里(还有一个GCD的定时器,后面再说),接下来说说NSTimer、CADisplayLink使用不当导致的循环引用,这个才是重点呀(敲黑板,看这里,这里以NSTimer为例)
    首先来说下dealloc方法(这里只说ARC环境下,系统自动调用),该方法使用场景一般分以下几点:

    • 该类被release的时候会自动调用
    • 该对象应用计数[retain count]为0的时候会被自动调用
    • 该对象被置为nil的时候会被自动调用
    通常情况下,我们的定时器都是直接或者间接的依赖于控制器,iOS开发中 当控制器出栈的时候我们希望该控制器销毁,也就是说自动调用delloc方法
    @interface AnotherViewController ()
    /**
     NSTimer定时器
     */
    @property (nonatomic, strong) NSTimer* timer;
    @end
    
    - (void)viewDidLoad {
        [super viewDidLoad];
        self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(timerMethod:) userInfo:nil repeats:YES];
    }
    - (void)timerMethod:(NSTimer*) timer{
    
        NSLog(@"%s", __func__);
    }
    -(void)dealloc{
    
        NSLog(@"%s", __func__);
        [self.timer invalidate];
        self.timer = nil;
    }
    

    上面的代码由于循环引用导致控制器出栈的时候没有被调用,简单分析如下:


    图片.png

    NSTimer与Controller相互引用,导致没有办法释放,所以最终的结果就是dealloc方法没有调用,这里主要是由于NSTimer的target参数,现提供一种思路,借助第三方对象来打破这种引用,具体思路如下:


    图片.png

    思路有了,具体代码实现就相对比较容易了,如下所示:

    #import <Foundation/Foundation.h>
    @interface JCProxy : NSObject
    @property (weak, nonatomic) id target;
    +(instancetype) proxyWithTarget:(id) target;
    
    @end
    
    #import "JCProxy.h"
    
    @implementation JCProxy
    
    +(instancetype) proxyWithTarget:(id) target{
        JCProxy* proxy = [[JCProxy alloc] init];
        proxy.target = target;
        return proxy;
    }
    
    #pragma mark --- 消息转发
    -(id)forwardingTargetForSelector:(SEL)aSelector{
        return self.target;
    }
    //
    //-(NSMethodSignature*) methodSignatureForSelector:(SEL)aSelector{
    //    
    //}
    //
    //-(void)forwardInvocation:(NSInvocation *)anInvocation{
    //    
    //}
    
    @end
    

    注意点:
    1、target的声明使用weak关键字
    2、我们希望NSTimer的方法最终实现在Controller中,这里采用消息转发(我们只需要实现forwardingTargetForSelector即可),消息转发后面再说,目前主要解决循环引用
    这里最终NSTimer的使用就采用如下方式,其他与上面完全一致

    self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:[JCProxy proxyWithTarget:self] selector:@selector(timerMethod:) userInfo:nil repeats:YES];
    

    至此,当前Controller出栈时就会自动调用dealloc方法,运行结果如下:


    图片.png

    到这里,基本上已经解决NSTimer/CADisplayLink循环引用的问题了,问题是解决了,但是不够专业,iOS中我们有专门处理这类问题的NSProxy
    具体代码实现如下:

    #import <Foundation/Foundation.h>
    @interface JCKProxy : NSProxy
    
    @property (weak, nonatomic) id target;
    
    +(instancetype) proxyWithTarget:(id) target;
    
    @end
    
    #import "JCKProxy.h"
    
    @implementation JCKProxy
    
    +(instancetype) proxyWithTarget:(id) target{
        
    #pragma mark --- NSProxy没有init方法,直接alloc就可使用
        JCKProxy* proxy = [JCKProxy alloc];
        proxy.target = target;
        return proxy;
    }
    
    #pragma mark --- 消息转发
    //返回方法签名
    -(NSMethodSignature*)methodSignatureForSelector:(SEL)sel{
        
        return [self.target methodSignatureForSelector:sel];
    }
    
    -(void)forwardInvocation:(NSInvocation *)invocation{
        
        [invocation invokeWithTarget:self.target];
    }
    
    #pragma mark --- NSProxy这里没有这个方法
    //-(id)forwardingTargetForSelector:(SEL)aSelector{
    //    
    //    return self.target;
    //}
    
    @end
    

    最终的使用如下:

    self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:[JCKProxy proxyWithTarget:self] selector:@selector(timerMethod:) userInfo:nil repeats:YES];
    

    当前Controller出栈时候同样会自动调用dealloc方法,运行结果如下:


    图片.png

    这里补充一点,很多时候循环引用可以通过weak来解决,在NSTimer的使用中,weak仅仅可以解决很少的一部分,不具有代表性,weak解决循环引用一般使用在block中,因为NSTimer的创建中是可以通过block创建的,如下方法所示:

    + (NSTimer *)timerWithTimeInterval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block API_AVAILABLE(macosx(10.12), ios(10.0), watchos(3.0), tvos(10.0));
    + (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));
    

    这种情况下,其实也是可以使用weak来打破循环引用的,如下所示:

    #pragma mark --- weakSelf
        __weak typeof(self) weakSelf = self;
        self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 repeats:YES block:^(NSTimer * _Nonnull timer) {
    
            //这里timer weakSelf.time都是一样的
    //        [weakSelf timerMethod:timer];
            [weakSelf timerMethod:weakSelf.timer];
        }];
    

    最终的运行结果如下:

    图片.png
    NSTimer 解决循环引用总结:
    1、block方式创建的NSTimer,可以使用__weak typeof(self) weakSelf = self;解决,其他方式创建的不行,CADisplayLink没有block创建方式,所以不适用
    2、推荐方式 NSProxy方式解决因为NSTimer、CADisplayLink引起的循环引用
    NSProxy注意事项:
    有争议的解释.png

    1、上面这张图是网上的解释,这里必须说明一点,这张图是有错误的,NSProxy是没有init、initWith方法的,何来实现之说
    2、这个类专门用来做消息转发的(效率高),所以必须实现
    -(NSMethodSignature*)methodSignatureForSelector:(SEL)sel -(void)forwardInvocation:(NSInvocation *)invocation,没有-(id)forwardingTargetForSelector:(SEL)aSelector 方法
    3、上面解决循环引用的2种方式,一种继承自NSObject,一种继承自NSProxy,均采用消息转发,但是继承自NSProxy的效率更好,区别如下:

    • 继承自NSObject的JCProxy在查找timerMethod方法时,首先会从自己去找,没找到再去父类中查找,最终也没有timerMethod方法时候才会进行消息转发
    • 继承自NSProxy的JCKProxy在查找timerMethod方法时,发现自己没有该方法,它就直接进行消息转发,所以少了去父类查找的这一步骤,耗时会更少,效率会更好

    到这里,NSTimer、CADisplayLink 基本上算结束了,希望可以帮到有需要的同学,如有疑问,欢迎私戳

    相关文章

      网友评论

        本文标题:iOS 定时器(NSTimer/CADisplayLink)循环

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