美文网首页iOS开发iOS常用iOS学习笔记
iOS NSTimer 详解(runloop,timer销毁)

iOS NSTimer 详解(runloop,timer销毁)

作者: BlackStar暗星 | 来源:发表于2020-07-16 16:59 被阅读0次

    关于timer的调用分为两种

    • timerWithTimeInterval 开头
    • scheduledTimerWithTimeInterval 开头

    第一种里边有三种方法,分别是

    /// 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 ;
    
    + (NSTimer *)timerWithTimeInterval:(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;
    

    苹果给的备注写的很清楚

    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.
    凡是以第一种方式调用的,你需要一个runloop,才能让他正常使用。

    插播:关于fire、fireDate

    fire 和 fireDate 的作用基本一致,都是用来开始执行timer的,即便我们不主动调用,当timer达到要求时 即时间间隔为timerWithTimeInterval设置的值时,timer也会执行。唯一区别就是 firDate 可以指定 timer 在什么时候开始执行,而 fire 是立即执行,不设置的话就是timerWithTimeInterval后开始执行。我们可以把 fire 理解为 performSelect ,把 fireDate 理解为 performSelector afterDelay。当然,只能是当成,而不是真正意义上的“是” ,因为还涉及到了 repeat 的问题。
    还有一点很重要,fire 和 fireDate 他执行的 timer action (selector 参数),代表了 timer 的一次真正意义上的执行。什么意思呢,就是说,如果repeats=NO,并且TimeInterval>0,那么执行 fire 和不执行fire,timer action 都仅仅只会执行一次,区别在于执行的时间点不一样。比如说 TimeInterval = 3 ,我调用fire了,会立即执行 timer action ,但是3秒后,并不会执行下一下,设置的TimeInterval就失去了意义。如果不调用fire,那么会在3秒后调用一次 timer action

    下面一个一个方法进行分析:

    一、block 回调方式 timer

        __block NSInteger timerCount = 0;
        NSTimer *timer = [NSTimer timerWithTimeInterval:1 repeats:YES block:^(NSTimer * _Nonnull timer) {
            timerCount ++ ;
            if (timerCount>=5) {
                [timer invalidate];
                timer = nil;
            }
            NSLog(@"timer block 执行 %ld 次",timerCount);
        }];
        [timer fire];
        [[NSRunLoop currentRunLoop]addTimer:timer forMode:NSRunLoopCommonModes];
    

    可以发现,timer 没有指定target,也就是说,timer 并没有强持有self。根据这个原因我们可以认定,block timer 并不会影响 controller 的生命周期。
    验证:执行上述代码,查看结果

    2020-07-16 11:08:32.080114+0800 BSFrameworks_Example[94630:14248608] timer block 执行 1 次
    2020-07-16 11:08:33.080461+0800 BSFrameworks_Example[94630:14248608] timer block 执行 2 次
    2020-07-16 11:08:33.600657+0800 BSFrameworks_Example[94630:14248608] BSStudyObjcController dealloc
    2020-07-16 11:08:34.080959+0800 BSFrameworks_Example[94630:14248608] timer block 执行 3 次
    2020-07-16 11:08:35.080898+0800 BSFrameworks_Example[94630:14248608] timer block 执行 4 次
    2020-07-16 11:08:36.080533+0800 BSFrameworks_Example[94630:14248608] timer block 执行 5 次
    

    结果显示,正如我们猜想那样,controller 在timer没销毁前释放了。但是有趣的是controller释放后,timer依然继续执行,这是为什么呢?我猜可能是因为系统要循环执行timer的selector,但是因为没有指定target,所以他把timer放在了系统全局的一个地方,以便timer的继续执行(纯个人猜测)

    二、invocation timer

    + (NSTimer *)timerWithTimeInterval:(NSTimeInterval)ti invocation:(NSInvocation *)invocation repeats:(BOOL)yesOrNo;
    
        NSMethodSignature *signature = [self methodSignatureForSelector:@selector(timerAction)];
        NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:signature];
        invocation.target = self;
        invocation.selector = @selector(timerAction);
    
        NSTimer *invocationTimer = [NSTimer timerWithTimeInterval:1 invocation:invocation repeats:YES];
        [[NSRunLoop currentRunLoop]addTimer:invocationTimer forMode:NSRunLoopCommonModes];
    

    至于 invocation 是什么去看下消息转发就清楚了。这种形式的timer完全可以理解为消息转发。(invocation 是可以传参的,这里没写)

    三、target selector timer

    + (NSTimer *)timerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo;
    
    self.timer = [NSTimer timerWithTimeInterval:1 target:self selector:@selector(timerAction:) userInfo:nil repeats:YES];
    [[NSRunLoop currentRunLoop]addTimer:self.timer forMode:NSRunLoopCommonModes];
    

    关于 timer 的销毁

    对于第二种和第三种 timer 的使用方法,他们都会指定target,在timer没有销毁前,target 是不会释放的。

    既然timer的释放会影响到target的释放,那么我们肯定要优先处理timer的销毁。一般情况下 timer 的销毁我们都会在某条件下,使用如下的方式对timer进行销毁

    [self.timer invalidate];
    self.timer = nil;
    

    timer销毁后,如果target将要销毁,那么target就会执行dealloc方法,也就证明了 target 销毁了。

    利用消息转发,解决timer 强持self的问题

    利用系统的消息转发机制,我们可以通过建立一个中间对象作为target,然后利用消息转发,将消息传递回 我们的业务类中

    image.png

    转化成代码就是:

    //TimerTarget.h文件
    #pragma mark - 
    @interface TimerTarget : NSObject
    
    
    @property (nonatomic ,weak) BSLooperView * target;
    
    
    @end
    
    
    //TimerTarget.m文件
    @implementation TimerTarget
    
    - (NSMethodSignature *)methodSignatureForSelector:(SEL)sel {
        
        return [self.target methodSignatureForSelector:sel];
    }
    
    - (void)forwardInvocation:(NSInvocation *)invocation {
        [invocation invokeWithTarget:self.target];
    }
    
    @end
    
    //业务类.m
    
    //==============================
    // 属性
    //==============================
    /// 计时器
    @property (nonatomic ,strong) NSTimer *timer;
    
    /// 用于 解决 timer 强引用 self 的问题
    @property (nonatomic ,strong) TimerTarget *timerTarget;
    
    
    
    //==============================
    // 方法
    //==============================
    #pragma mark - 生命周期
    -(void)dealloc{
        
        NSLog(@"BSLooperView 释放");
        
        if (self.timer) {
            [self.timer invalidate];
            self.timer = nil;
        }
    }
    
    /// 创建timer
    -(void)creatTimer{
        
        [self.timer invalidate];
        self.timer = nil;
        
        if (!self.timer) {
            if (self.duration<0.5) {
                self.duration = 3;
            }
            
            /**
             * 本来要加将timer 加入 runloop中(子线程加入,启动runloppe)
             * 加入后,发现无法停止timer,暂时未找到解决方案
             * 加runloop的好处就是,如果 滚动视图 的父视图 是ScrollView
             * 那么 ScrollView 的滚动 不影响timer的执行
             * 不加入runloop会造成 scrollview在滑动的时候timer 是暂停的(卡主)
             */
            self.timerTarget = [[TimerTarget alloc]init];
            self.timerTarget.target = self;
            self.timer = [NSTimer scheduledTimerWithTimeInterval:self.duration target:self.timerTarget selector:@selector(looperTime) userInfo:nil repeats:YES];
        }
    }
    
    

    这样我们就解决了timer 强持self导致 self 无法调用 dealloc 的问题,然后我们在 dealloc 内销毁 timer 即可


    runloop 和 timer

    首先说下 scheduledTimerWithTimeInterval ,在苹果的api介绍里是这么描述的

    /// 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

    意思就是说他会在当前runloop的default mode 中 执行timer

    所以我们使用的时候只需要一行代码

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

    并不需要把 timer 加入到 runloop 中,因为 scheduled 的作用就是把 timer 加入到runloop中。
    下面我们把 timer 放在子线程中去执行,看看啥效果

    -(void)timerTest{
        
        dispatch_async(dispatch_get_global_queue(0, 0), ^{
            self.timer = [NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(timerAction) userInfo:nil repeats:YES];
            [self.timer fire];
        });
    }
    

    结果

    2020-07-16 14:48:01.283390+0800 BSFrameworks_Example[95489:14357750] timer 执行
    

    为什么 repeats = YES ,他就执行了一次呢 ?执行的这一次明显是 [self.timer fire]的作用。scheduledTimerWithTimeInterval 不是已经加入了 runloop了吗,为什么没有执行?其实很简单:对于runloop,在主线程中,系统已经帮我们开起了runloop了,但是对于子线程,是需要我们自己主动去启动runloop的,所以想要timer 正常执行还需要启动下 runloop

    -(void)timerTest{
        
        dispatch_async(dispatch_get_global_queue(0, 0), ^{
            self.timer = [NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(timerAction) userInfo:nil repeats:YES];
            [self.timer fire];
            [[NSRunLoop currentRunLoop]run];
        });
    }
    

    顺便说下 Runloop ,我们是不能主动创建Runloop的,在调用 [NSRunLoop currentRunLoop] 的时候,如果 runloop 没有,系统会自动帮我们创建,如果有,就会直接把存在的 runloop 给我们, 类似于懒加载。Runloop 与 线程 是一对一的,一个线程最多只能对应一个 Runloop 。


    timer 延迟性

    timer实际触发事件的时间,精度并没有那么准确,如果当前RunLoop正在执行一个复杂的连续性的运算,timer很可能会延时触发。目前苹果还为 timer 增加了 tolerance 属性,代表对 timer 误差的容忍度

    CADisplayLink

    相比timer来说, CADisplayLink 更加的精准

    A timer object that allows your application to synchronize its drawing to the refresh rate of the display.

    谷歌翻译:CADisplayLink是一个定时器,他允许您的应用程序用固定的刷新率将其图形同步绘制并展示

    CADisplayLink以我们指定的模式添加到RunLoop中,通常情况下他会以60次/秒的刷新率来执行selector。对于iOS设备,他的刷新频率是固定的,但是并不是说他的刷新频率一定是一成不变的,因为他还会受到一些其他因素影响,如:CPU处于繁忙状态,并不能保证60次/s的刷新率。这样就会跳过一些次数的回调。
    我们一般使用CADisplayLink用来检测屏幕是否卡顿,视频播放器的界面渲染等

    + (CADisplayLink *)displayLinkWithTarget:(id)target selector:(SEL)sel;
    
    - (void)addToRunLoop:(NSRunLoop *)runloop forMode:(NSString *)mode;
    
    - (void)removeFromRunLoop:(NSRunLoop *)runloop forMode:(NSString *)mode;
    
    //停止
    - (void)invalidate;
    

    用法很简单,创建时指定target和 selector然后加入到runloop中,没有 runloop 是无法使用的。销毁方法和 timer 类似

    [self.link invalidate];
    self.link = nil;
    
    GCD timer

    GCD timer的使用,苹果已经封装好了,直接调用即可,不需要管释放的问题

    //单次 repeats = NO ,时间间隔1.0s
    dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, 1.0 * NSEC_PER_SEC); 
    dispatch_after(popTime, dispatch_get_main_queue(), ^(void){ //回调任务});
    
    //循环 repeats = YES ,时间间隔2.0s
    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    dispatch_source_t  timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
    dispatch_source_set_timer(timer, dispatch_walltime(NULL, 0), 2.0 * NSEC_PER_SEC, 0); 
    
    dispatch_source_set_event_handler(timer, ^{
        if(指定条件){
           dispatch_source_cancel(timer);
        }  
    });
    
    dispatch_source_set_cancel_handler(timer, ^{
        //取消回调
    });
    //启动定时器
    dispatch_resume( timer);
    

    相关文章

      网友评论

        本文标题:iOS NSTimer 详解(runloop,timer销毁)

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