美文网首页
NSTimer那些事

NSTimer那些事

作者: Sunli_ | 来源:发表于2017-03-17 18:20 被阅读448次

    定时器:

    • 它会被添加到Runloop,否则不会运行,当然添加的Runloop不存在也不会运行
    • 还要指定添加到的Runloop的哪个模式,而且还可以指定添加到Runloop的多个模式,模式不对也是不会运行的
    • Runloop会对timer有强引用,timer会对目标对象进行强引用(是否隐约的感觉到坑了。。。)
    • timer的执行时间并不准确,系统繁忙的话,还会被跳过去
    • invalidate调用后,timer停止运行后,就一定能从Runloop中消除吗?

    定时器的一般用法

    - (void)viewDidLoad {
        NSTimer *timer = [[NSTimer alloc] initWithFireDate:[NSDate date] interval:1 target:self selector:@selector(timerFire) userInfo:nil repeats:YES];
        [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
        self.timer = timer;
    }
    
    - (void)timerFire {
        NSLog(@"timer fire");
    }
    

    上面的代码就是我们使用定时器最常用的方式,可以总结为2个步骤:创建,添加到runloop

    系统提供了8个创建方法,6个类创建方法,2个实例初始化方法。

    • 有三个方法直接将timer添加到了当前Runloop default mode,而不需要我们自己操作,当然这样的代价是Runloop只能是当前Runloop,模式是Default mode
    + (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)ti invocation:(NSInvocation *)invocation repeats:(BOOL)yesOrNo;
    
    + (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(id)userInfo repeats:(BOOL)yesOrNo;
    
    + (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block;
    
    • 下面五种创建,不会自动添加到Runloop,还需调用addTimer:forMode:
    + (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:(id)userInfo repeats:(BOOL)yesOrNo;
    
    - (instancetype)initWithFireDate:(NSDate *)date interval:(NSTimeInterval)ti target:(id)t selector:(SEL)s userInfo:(id)ui repeats:(BOOL)rep;
    
    - (instancetype)initWithFireDate:(NSDate *)date interval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block;
    

    对上面所有方法参数做个说明:

    • ti(interval):定时器触发间隔时间,单位为秒,可以是小数。如果值小于等于0.0的话,系统会默认赋值0.1毫秒
    • invocation:这种形式用的比较少,大部分都是block和aSelector的形式
    • yesOrNo(rep):是否重复,如果是YES则重复触发,直到调用invalidate方法;如果是NO,则只触发一次就自动调用invalidate方法
    • aTarget(t):发送消息的目标,timer会强引用aTarget,直到调用invalidate方法
    • aSelector(s):将要发送给aTarget的消息,如果带有参数则应:- (void)timerFireMethod:(NSTimer *)timer声明
    • userInfo(ui):传递的用户信息。使用的话,首先aSelector须带有参数的声明,然后可以通过[timer userInfo]获取,也可以为nil,那么[timer userInfo]就为空
    • date:触发的时间,一般情况下我们都写[NSDate date],这样的话定时器会立马触发一次,并且以此时间为基准。如果没有此参数的方法,则都是以当前时间为基准,第一次触发时间是当前时间加上时间间隔ti
    • block:timer触发的时候会执行这个操作,带有一个参数,无返回值

    添加到runloop,参数timer是不能为空的,否则抛出异常
    - (void)addTimer:(NSTimer *)timer forMode:(NSRunLoopMode)mode
    另外,系统提供了一个- (void)fire方法,调用它可以触发一次:

    对于重复定时器,它不会影响正常的定时触发
    对于非重复定时器,触发后就调用了invalidate方法,既使正常的还没有触发

    NSTimer添加到NSRunLoop

    timer必须添加到Runloop才有效,很明显要保证两件事情,一是Runloop存在(运行),另一个才是添加。确保这两个前提后,还有Runloop模式的问题。

    一个timer可以被添加到Runloop的多个模式,比如在主线程中runloop一般处于NSDefaultRunLoopMode,而当滑动屏幕的时候,比如UIScrollView或者它的子类UITableView、UICollectionView等滑动时Runloop处于UITrackingRunLoopMode模式下,因此如果你想让timer在滑动的时候也能够触发,就可以分别添加到这两个模式下。或者直接用NSRunLoopCommonModes一个模式集,包含了上面的两种模式。

    但是一个timer只能添加到一个Runloop(Runloop与线程一一对应关系,也就是说一个timer只能添加到一个线程)。如果你非要添加到多个Runloop,则只有一个有效

    关于强引用的问题

    一般我们使用NSTimer如下图所示

    - (void)viewDidLoad {
        // 代码1
        NSTimer *timer = [[NSTimer alloc] initWithFireDate:[NSDate date] interval:1 target:self selector:@selector(timerFire) userInfo:nil repeats:YES];
        // 代码2  上文中提到有些初始化方法会自动添加Runloop 这里是主动调用 效果都一样
        [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode]; 
        // 代码3
        self.timer = timer;
    }
    
    - (void)timerFire {
        NSLog(@"timer fire");
    }
    

    假设代码中self(viewController)UINavgationController管理 并且timer与self之间为强引用

    timer和vc(self)Runloop之间的关系图
    如图所示 分析一下4根线的由来
    • L1:nav push 控制器的时候会强引用,即在push的时候产生;
    • L2:是在代码3的位置产生
    • L3:是在代码1的位置产生,至此L2与L3已经产生了循环引用,虽然timer还没有添加到Runloop
    • L4:是在代码2的位置产生
      我们常说的NSTimer会产生循环引用 其实就是由于timer会对self进行强引用造成的。

    打破循环引用

    解决循环引用,首先想到的方法就是让self对timer为弱引用weak或者time对target如self替换为weakSelf 然而这真的有用吗?

    例:

    @interface TimerViewController ()
    
    @property (nonatomic, weak) NSTimer *timer;
    
    @end
    
    @implementation TimerViewController
    
    - (void)dealloc {
        
        [self.timer invalidate];
         NSLog(@"dealloc");
    }
    
    - (void)viewDidLoad {
        [super viewDidLoad];
        
        NSTimer *timer = [NSTimer timerWithTimeInterval:1.0f target:self selector:@selector(count) userInfo:nil repeats:YES];
    
        [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
        
        self.timer = timer;
    }
    
    - (void)count {
         NSLog(@"count");
    }
    @end
    

    从上一个vc push进来之后, 定时器开始启动 控制台打印count, 然后pop回去, TimerViewController并没有走dealloc方法 控制台依然打印count。

    将self改成weakSelf效果依然一样

    - (void)viewDidLoad {
        [super viewDidLoad];
        
        
        __weak typeof(self) weakSelf = self;
        NSTimer *timer = [NSTimer timerWithTimeInterval:1.0f target:weakSelf selector:@selector(count) userInfo:nil repeats:YES];
    
        [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
        
        self.timer = timer;
    }
    

    分析:

    设置timer为weak

    我们想通过self对timer的弱引用, 在self中的dealloc方法中让timer失效来达到相互释放的目的。但是, timer内部本身对于self有一个强引用。并且timer如果不调用invalidate方法,就会一直存在,所以就导致了self根本释放不了, 进而我们想通过在dealloc中设置timer失效来释放timer的方法也就行不通了。

    设置self为weakSelf

    __weak修饰self为weakSelf与普通的self的区别就在于, 这时候weakSelf作为一个参数传入block或者别的实例变量, block或实例变量都不会持有他, 也就是self的引用计数不会加1 在一般情况下 这时候就可以打破循环引用, 但是timer的内部机制决定了它必须通过设置invalidate来停止计时并释放, 在此之前, timer会强引用target, 所以也就不存在timer释放weakSelf, 即循环引用还是存在。

    (别人对于weakstrong的解释 我觉得挺好的 多读几遍会有更多了解)

    1.(weakstrong)不同的是:当一个对象不再有strong类型的指针指向它的时候,它就会被释放,即使该对象还有_weak类型的指针指向它;
    2.一旦最后一个指向该对象的strong类型的指针离开,这个对象将被释放,如果这个时候还有weak指针指向该对象,则会清除掉所有剩余的weak指针

    解决办法

    总结: 要想解决循环引用的问题, 关键在于让timer失效即调用[timer invalidate]方法而不是各种weak。

    • 外部调用方法触发invalidate
    - (void)stopTimer {
        [self.timer invalidate];
    }
    
    • 如果在是在view中的定时器, 可以重写removeFromSuperview
    - (void)removeFromSuperview {
        [super removeFromSuperview];
        [self.timer invalidate];
    }
    
    • 将timer的target设为一个中间类
    @interface BreakTimeLoop ()
    
    // 这里必须用weak 不然释放不了
    @property (nonatomic, weak) id owner;
    
    @end
    
    
    @implementation BreakTimeLoop
    
    - (void)dealloc {
         NSLog(@"BreakTimeLoop dealloc");
    }
    
    - (instancetype)initWithOwner:(id)owner {
        if (self = [super init]) {
            self.owner = owner;
        }
        return self;
    }
    
    - (void)doSomething:(NSTimer *)timer {
        // 需要参数可以通过 timer.userInfo 获取
        [self.owner performSelector:@selector(count)];
    }
    

    在vc中

    @interface TimerViewController ()
    
    /** timer在这种情况下也可用strong **/
    @property (nonatomic, strong) NSTimer *timer;
    /** 中间类 这里用strong和weak无所谓 主要是breaker中对owner的引用为weak就行 **/
    @property (nonatomic, strong) BreakTimeLoop *breaker;
    
    @end
    
    @implementation TimerViewController
    
    - (void)dealloc {
        
        [self.timer invalidate];
         NSLog(@"TimerViewController dealloc");
    }
    
    - (void)viewDidLoad {
        [super viewDidLoad];
        
        self.breaker = [[BreakTimeLoop alloc] initWithOwner:self];
        // 这里的doSomething: 如果在.h中没有声明会报警告 不过不影响实现
        self.timer = [NSTimer timerWithTimeInterval:1.0f target:self.breaker selector:@selector(doSomething:) userInfo:nil repeats:YES];
    
        [[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSRunLoopCommonModes];
    }
    
    - (void)count {
         NSLog(@"count");
    }
    @end
    

    控制台打印:


    • NSTimer写一个分类
    @interface NSTimer (SLBreakTimer)
    
    + (NSTimer *)sl_scheduledTimerWithTimeInterval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)())block;
    
    @end
    
    @implementation NSTimer (SLBreakTimer)
    
    + (NSTimer *)sl_scheduledTimerWithTimeInterval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)())block {
        
        // copy将block放入堆上防止提前释放
        return [NSTimer scheduledTimerWithTimeInterval:interval target:self selector:@selector(sl_block:) userInfo:[block copy] repeats:repeats];
    }
    
    + (void)sl_block:(NSTimer *)timer {
        void (^block)() = timer.userInfo;
        if (block) {
            block();
        }
    }
    @end
    

    在vc中

    - (void)viewDidLoad {
        [super viewDidLoad];
        
        
        // 为weak防止循环引用
        __weak typeof(self) weakSelf = self;
        self.timer = [NSTimer sl_scheduledTimerWithTimeInterval:1.0f repeats:YES block:^{
            // 防止self提前释放
            __strong typeof(weakSelf) strongSelf = weakSelf;
            [strongSelf count];
        }];
        
        [[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSRunLoopCommonModes];
    }
    

    控制台打印为:

    invalidate方法有啥用

    在上文中, 我们所有做的一些都是围绕invalidate方法来做的。那么这个方法到底有什么用呢?

    • 将timer从Runloop中移除
    • 释放他自身持有的资源 比如target userInfo block

    注意:

    1. 如果timer的引用为weak,在调用了invalidate之后,timer会被释放(ARC会置空)如果在这之后还想用timer必须重新创建,所以 我们添加timer进Runloop之前可以通过timer的isValid方法判断是否可用.
    2. 如果invalidate的调用不在添加Runloop的线程,那么timer虽然会释放他持有的资源,但是它本身不会被释放,他所在的Runloop也不会被释放,也会导致内存泄漏。
    timer是否准时

    不准时

    • 第一种不准时:有可能跳过去
    1. 线程在处理耗时的事情时会发生。
    2. 还有就是timer添加到的Runloop模式不是Runloop当前运行的模式,这种情况经常发生。

    对于第一种情况我们不应该在timer上下功夫,而是应该避免这个耗时的工作。那么第二种情况,作为开发者这也是最应该去关注的地方,要留意,然后视情况而定是否将timer添加到Runloop多个模式。
    虽然跳过去,但是,接下来的执行不会依据被延迟的时间加上间隔时间,而是根据之前的时间来执行。比如:
    定时时间间隔为2秒,t1秒添加成功,那么会在t2、t4、t6、t8、t10秒注册好事件,并在这些时间触发。假设第3秒时,执行了一个超时操作耗费了5.5秒,则触发时间是:t2、t8.5、t10,第4和第6秒就被跳过去了,虽然在t8.5秒触发了一次,但是下一次触发时间是t10,而不是t10.5

    • 第二种不准时:不准点

    比如上面说的t2、t4、t6、t8、t10,并不会在准确的时间触发,而是会延迟个很小的时间,原因也可以归结为2点:

    1. RunLoop为了节省资源,并不会在非常准确的时间点触发
    2. 线程有耗时操作,或者其它线程有耗时操作也会影响

    iOS7以后,timer 有个属性叫做 Tolerance(时间宽容度,默认是0),标示了当时间点到后,容许有多少最大误差。
    它只会在准确的触发时间到加上Tolerance时间内触发,而不会提前触发(是不是有点像我们的火车,只会晚点。。。)。另外可重复定时器的触发时间点不受Tolerance影响,即类似上面说的t8.5触发后,下一个点不会是t10.5,而是t10 + Tolerance

    相关文章

      网友评论

          本文标题:NSTimer那些事

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