定时器

作者: Jack1105 | 来源:发表于2020-10-27 14:34 被阅读0次
    重拾iOS.jpg

    关键词:NSTimer、CADisplayLink、GCD、RunLoop

    前言

    1. 开发中常用的定时器有哪些,优缺点是什么?
    2. 定时器的循环引用问题怎么解决?
    3. CADisplayLink、NSTimer是否准时?

    一、NSTimer和CADisplayLink

    1、NSTimer

    常用api有:

    + (NSTimer *)timerWithTimeInterval:(NSTimeInterval)ti invocation:(NSInvocation *)invocation repeats:(BOOL)yesOrNo;
    + (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)ti invocation:(NSInvocation *)invocation repeats:(BOOL)yesOrNo;
    
    + (NSTimer *)timerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo;
    + (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo;
    
    
    /// Creates and returns a new NSTimer object initialized with the specified block object. This timer needs to be scheduled on a run loop (via -[NSRunLoop addTimer:]) before it will fire.
    /// - parameter:  timeInterval  The number of seconds between firings of the timer. If seconds is less than or equal to 0.0, this method chooses the nonnegative value of 0.1 milliseconds instead
    /// - parameter:  repeats  If YES, the timer will repeatedly reschedule itself until invalidated. If NO, the timer will be invalidated after it fires.
    /// - parameter:  block  The execution body of the timer; the timer itself is passed as the parameter to this block when executed to aid in avoiding cyclical references
    + (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));
    
    /// Creates and returns a new NSTimer object initialized with the specified block object and schedules it on the current run loop in the default mode.
    /// - parameter:  ti    The number of seconds between firings of the timer. If seconds is less than or equal to 0.0, this method chooses the nonnegative value of 0.1 milliseconds instead
    /// - parameter:  repeats  If YES, the timer will repeatedly reschedule itself until invalidated. If NO, the timer will be invalidated after it fires.
    /// - parameter:  block  The execution body of the timer; the timer itself is passed as the parameter to this block when executed to aid in avoiding cyclical references
    + (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));
    
    /// Initializes a new NSTimer object using the block as the main body of execution for the timer. This timer needs to be scheduled on a run loop (via -[NSRunLoop addTimer:]) before it will fire.
    /// - parameter:  fireDate   The time at which the timer should first fire.
    /// - parameter:  interval  The number of seconds between firings of the timer. If seconds is less than or equal to 0.0, this method chooses the nonnegative value of 0.1 milliseconds instead
    /// - parameter:  repeats  If YES, the timer will repeatedly reschedule itself until invalidated. If NO, the timer will be invalidated after it fires.
    /// - parameter:  block  The execution body of the timer; the timer itself is passed as the parameter to this block when executed to aid in avoiding cyclical references
    - (instancetype)initWithFireDate:(NSDate *)date interval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block API_AVAILABLE(macosx(10.12), ios(10.0), watchos(3.0), tvos(10.0));
    
    - (instancetype)initWithFireDate:(NSDate *)date interval:(NSTimeInterval)ti target:(id)t selector:(SEL)s userInfo:(nullable id)ui repeats:(BOOL)rep NS_DESIGNATED_INITIALIZER;
    
    - (void)fire;
    

    在NSTimer的初始化方法中,以scheduled开头的方法,timer默认已经添加到了当前RunLoop中(以default mode形式添加)

    2、CADisplayLink

    常用api有:

    /* Create a new display link object for the main display. It will
     * invoke the method called 'sel' on 'target', the method has the
     * signature '(void)selector:(CADisplayLink *)sender'. */
    
    + (CADisplayLink *)displayLinkWithTarget:(id)target selector:(SEL)sel;
    
    /* Adds the receiver to the given run-loop and mode. Unless paused, it
     * will fire every vsync until removed. Each object may only be added
     * to a single run-loop, but it may be added in multiple modes at once.
     * While added to a run-loop it will implicitly be retained. */
    
    - (void)addToRunLoop:(NSRunLoop *)runloop forMode:(NSRunLoopMode)mode;
    
    /* 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、使用block方式初始化NSTimer;

    2、使用中间层WeakContainer;

    代码示例:

    新建SFWeakContainer类

    // .h
    @interface SFWeakContainer : NSObject
    
    - (instancetype)initWithTarget:(NSObject *)target;
    + (instancetype)containerWithTarget:(NSObject *)target;
    
    @end
    
    // .m
    @interface SFWeakContainer ()
    @property (nonatomic, weak) NSObject *target;
    @end
    
    @implementation SFWeakContainer
    
    - (instancetype)initWithTarget:(NSObject *)target {
        if (self = [super init]) {
            self.target = target;
        }
        return self;
    }
    + (instancetype)containerWithTarget:(NSObject *)target {
        SFWeakContainer *container = [[SFWeakContainer alloc]initWithTarget:target];
        return container;
    }
    
    // 备用接受者
    - (id)forwardingTargetForSelector:(SEL)aSelector {
        if (self.target && [self.target respondsToSelector:aSelector]) {
            return self.target;
        }else{
            return [super forwardingTargetForSelector:aSelector];
        }
    }
    
    @end
    

    测试:

    - (void)viewDidLoad {
        [super viewDidLoad];
        self.view.backgroundColor = [UIColor whiteColor];
        SFWeakContainer *weakContainer = [SFWeakContainer containerWithTarget:self];
        self.timer = [NSTimer scheduledTimerWithTimeInterval:1 target:weakContainer selector:@selector(timerEvent:) userInfo:nil repeats:YES];
    }
    
    - (void)timerEvent:(NSTimer *)timer {
        NSLog(@"定时器事件");
    }
    
    - (void)dealloc {
        [self.timer invalidate];
        self.timer = nil;
        NSLog(@"%s", __func__);
    }
    

    3、NSProxy消息转发;

    代码示例:

    新建SFProxy

    // .h
    @interface SFProxy : NSProxy
    - (instancetype)initWithTarget:(id)target;
    + (instancetype)proxyWithTarget:(id)target;
    @end
    
    // .m
    @interface SFProxy ()
    @property (nonatomic, weak) NSObject *target;
    @end
    
    @implementation SFProxy
    - (instancetype)initWithTarget:(id)target {
        _target = target;
        return self;
    }
    + (instancetype)proxyWithTarget:(id)target {
        return [[self alloc] initWithTarget:target];
    }
    
    // 消息转发
    - (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{
        if (self.target && [self.target respondsToSelector:aSelector]) {
            return [self.target methodSignatureForSelector:aSelector];
        }
        return [super methodSignatureForSelector:aSelector];
    }
    - (void)forwardInvocation:(NSInvocation *)anInvocation{
        SEL aSelector = [anInvocation selector];
        if (self.target && [self.target respondsToSelector:aSelector]) {
            [anInvocation invokeWithTarget:self.target];
        } else {
            [super forwardInvocation:anInvocation];
        }
    }
    @end
    

    测试:

    - (void)viewDidLoad {
        [super viewDidLoad];
        self.view.backgroundColor = [UIColor whiteColor];
        SFProxy *proxy = [SFProxy proxyWithTarget:self];
        self.timer = [NSTimer scheduledTimerWithTimeInterval:1 target:proxy selector:@selector(timerEvent:) userInfo:nil repeats:YES];
    }
    
    - (void)timerEvent:(NSTimer *)timer {
        NSLog(@"定时器事件");
    }
    
    - (void)dealloc {
        [self.timer invalidate];
        self.timer = nil;
        NSLog(@"%s", __func__);
    }
    

    具体怎么做,根据个人喜好选择,我这里有一个写好的方案:Crash防护(4)-NSTimer

    三、CADisplayLink、NSTimer是否准时?

    CADisplayLink、NSTimer底层都是靠RunLoop来实现的,也就是可以把它们理解成RunLoop所需要处理的事件。我们知道RunLoop可以拿来刷新UI,处理定时器(CADisplayLink、NSTimer),处理点击滑动事件等非常多的事情。这里,就需要来了解一下RunLoop是如何触发NSTimer任务的。RunLoop每循环一圈,都会处理一定的事件,会消耗一定的时间,但是具体耗时多少这个是无法确定的。

    假如你开启一个timer,隔1秒触发定时器事件,RunLoop会开始累计每一圈循环的用时,当时间累计够1秒,就会触发定时器事件。你有兴趣的话,是可以在RunLoop的源码里面找到时间累加相关代码的。可以借助下图来加深理解:

    image

    如果RunLoop在某一圈任务过于繁重,就可能出现如下情况

    image

    所以CADisplayLink、NSTimer是无法保证准时性的。

    四、GCD Timer

    1、GCD Timer 的简单使用

    @interface ViewController ()
    @property (nonatomic, strong) dispatch_source_t timer;
    @end
    
    @implementation ViewController
    
    - (void)viewDidLoad {
        [super viewDidLoad];
        // 初始化定时器
        self.timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_main_queue());
        // 间隔时间
        uint64_t intervalTime = 1.0;
        //误差时间
        uint64_t leewayTime = 0;
        // 延迟时间
        uint64_t delayTime = 0;
        // 开始时间
        dispatch_time_t startTime = dispatch_time(DISPATCH_TIME_NOW, delayTime*NSEC_PER_SEC);
        // 设置定时器时间
        dispatch_source_set_timer(self.timer, startTime, intervalTime * NSEC_PER_SEC, leewayTime * NSEC_PER_SEC);
        // 设置定时器回调事件
        dispatch_source_set_event_handler(self.timer, ^{
            // 定时器事件代码
            NSLog(@"GCD定时器事件");
            // 如果定时器不需要重复,可以在这里取消定时器
            dispatch_source_cancel(self.timer);
        });
        // 运行定时器
        dispatch_resume(self.timer);
        
    }
    

    2、GCD Timer 的封装

    代码如下:

    #import "SFGcdTimer.h"
    
    @interface SFGcdTimer ()
    @property (nonatomic, strong) dispatch_source_t timer;
    @property (nonatomic, strong) dispatch_queue_t queue;
    /**
     * 提问:苹果为什么要把NSTimer中的target设计成强引用关系,既然他会导致循环引用问题,为什么苹果不直接将NSTimer的target设计成弱引用关系?
     * 所以这里保留跟NSTimer类似的设计
     */
    @property (nonatomic, strong) NSObject *target;
    @property (nullable, retain) id userInfo;
    @property (nonatomic, assign) NSTimeInterval timeInterval;
    @end
    
    @implementation SFGcdTimer
    
    // MARK: target方式
    /// 初始化方法(target)
    /// @param interval 时间间隔
    /// @param delay 延迟时间
    /// @param aTarget 执行对象
    /// @param aSelector 执行方法
    /// @param userInfo 附带信息
    /// @param repeats 是否重复
    + (SFGcdTimer *)timerWithTimeInterval:(NSTimeInterval)interval delay:(NSTimeInterval)delay target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)repeats  {
        SFGcdTimer *timer = [[SFGcdTimer alloc] initWithTimeInterval:interval delay:delay target:aTarget selector:aSelector userInfo:userInfo repeats:repeats queue:nil];
        return timer;
    }
    
    /// 初始化方法(target)
    /// @param interval 时间间隔
    /// @param delay 延迟时间
    /// @param aTarget 执行对象
    /// @param aSelector 执行方法
    /// @param userInfo 附带信息
    /// @param repeats 是否重复
    /// @param queue 指定队列(默认主队列)
    + (SFGcdTimer *)timerWithTimeInterval:(NSTimeInterval)interval delay:(NSTimeInterval)delay target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)repeats queue:(dispatch_queue_t)queue {
        SFGcdTimer *timer = [[SFGcdTimer alloc] initWithTimeInterval:interval delay:delay target:aTarget selector:aSelector userInfo:userInfo repeats:repeats queue:queue];
        return timer;
    }
    
    /// 初始化方法(target)
    /// @param interval 时间间隔
    /// @param delay 延迟时间
    /// @param aTarget 执行对象
    /// @param aSelector 执行方法
    /// @param userInfo 附带信息
    /// @param repeats 是否重复
    /// @param queue 指定队列(默认主队列)
    - (instancetype)initWithTimeInterval:(NSTimeInterval)interval delay:(NSTimeInterval)delay target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)repeats queue:(dispatch_queue_t)queue {
        if (self = [super init]) {
            self.timeInterval = interval;
            self.queue = queue;
            self.target = aTarget;
            self.userInfo = userInfo;
            self.timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, self.queue);
            dispatch_source_set_timer(self.timer,
                                      dispatch_time(DISPATCH_TIME_NOW, delay * NSEC_PER_SEC), // 开始时间
                                      interval * NSEC_PER_SEC, // 间隔
                                      0 // 误差
                                      );
            dispatch_source_set_event_handler(self.timer, ^{
                if ([self.target respondsToSelector:aSelector]) {
                    [self.target performSelector:aSelector withObject:self];
                }
                if (!repeats) {
                    [self invalidate];
                }
            });
        }
        return self;
    }
    
    
    // MARK: block方式
    /// 初始化方法(block)
    /// @param interval 时间间隔
    /// @param delay 延迟时间
    /// @param repeats 是否重复
    /// @param block 执行block
    + (SFGcdTimer *)timerWithTimeInterval:(NSTimeInterval)interval delay:(NSTimeInterval)delay repeats:(BOOL)repeats block:(void (^)(SFGcdTimer *timer))block {
        SFGcdTimer *timer = [[SFGcdTimer alloc]initWithTimeInterval:interval delay:delay repeats:repeats block:block queue:nil];
        return timer;
    }
    
    
    /// 初始化方法(block)
    /// @param interval 时间间隔
    /// @param delay 延迟时间
    /// @param repeats 是否重复
    /// @param block 执行block
    /// @param queue 执行队列(默认主队列)
    + (SFGcdTimer *)timerWithTimeInterval:(NSTimeInterval)interval delay:(NSTimeInterval)delay repeats:(BOOL)repeats block:(void (^)(SFGcdTimer *timer))block queue:(dispatch_queue_t)queue {
        SFGcdTimer *timer = [[SFGcdTimer alloc]initWithTimeInterval:interval delay:delay repeats:repeats block:block queue:queue];
        return timer;
    }
    
    
    /// 初始化方法(block)
    /// @param interval 时间间隔
    /// @param delay 延迟时间
    /// @param repeats 是否重复
    /// @param block 执行block
    /// @param queue 执行队列(默认主队列)
    - (instancetype)initWithTimeInterval:(NSTimeInterval)interval delay:(NSTimeInterval)delay repeats:(BOOL)repeats block:(void (^)(SFGcdTimer *timer))block queue:(dispatch_queue_t)queue {
        if (self = [super init]) {
            self.timeInterval = interval;
            self.queue = queue;
            self.timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, self.queue);
            dispatch_source_set_timer(self.timer,
                                      dispatch_time(DISPATCH_TIME_NOW, delay * NSEC_PER_SEC), // 开始时间
                                      interval * NSEC_PER_SEC, // 间隔
                                      0 // 误差
                                      );
            dispatch_source_set_event_handler(self.timer, ^{
                if (block) {
                    block(self);
                }
                if (!repeats) {
                    [self invalidate];
                }
            });
        }
        return self;
    }
    
    /// 开启
    - (void)fire {
        dispatch_resume(self.timer);
    }
    
    /// 暂停
    - (void)pause {
        dispatch_suspend(self.timer);
    }
    
    /// 销毁
    - (void)invalidate {
        dispatch_source_cancel(self.timer);
    }
    
    
    #pragma mark - lazy load
    // 默认主队列
    - (dispatch_queue_t)queue {
        if (!_queue) {
            _queue = dispatch_get_main_queue();
        }
        return _queue;
    }
    
    @end
    

    [代码链接]

    GitHub:https://github.com/jack110530/SFCrash


    [相关参考]

    1. 比较一下iOS中的三种定时器
    2. 内存管理——定时器问题

    [相关思考]

    1. NSTimer和线程的关系
    2. 苹果为什么要把NSTimer中的target设计成强引用关系,既然他会导致循环引用问题,为什么苹果不直接将NSTimer的target设计成弱引用关系?

    相关文章

      网友评论

        本文标题:定时器

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