iOS NSTimer 详解(runloop,timer销毁方式)

知识点

1、 基本使用

2、 runloop关系

3、 Timer销毁方式

关于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,然后利用消息转发,将消息传递回 我们的业务类中

WechatIMG16462.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销毁方式))