iOS开发-NSTimer

iOS开发-NSTimer_第1张图片
配图

1.初始化

+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)ti invocation:(NSInvocation *)invocation repeats:(BOOL)yesOrNo;
+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo;

以上初始化方法将以默认mode直接添加到当前的runloop中。

+ (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;

不带scheduled的初始化方法需要手动addTimer:forMode: 将timer添加到一个runloop中。

 /**
  *  创建一个timer , 并将它添加到当前线程的RunLoop
  */
timer = [NSTimer timerWithTimeInterval:1 target:self selector:@selector(repeat:) userInfo:@{@"key":@"value"} repeats:true];
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];

关于runloop及mode相关问题可以先看看这篇文章深入理解RunLoop

2.触发及销毁

  • fire

You can use this method to fire a repeating timer without interrupting its regular firing schedule. If the timer is non-repeating, it is automatically invalidated after firing, even if its scheduled fire date has not arrived.

cmd+shift+0打开官方文档,是说在重复执行的定时器中调用此方法后立即触发该定时器,但不会中断其之前的执行计划;在不重复执行的定时器中调用此方法,立即触发后,就会使这个定时器失效。比如我有个5秒的重复执行的定时器,不调用fire方法,定时器第一次执行方法是在5秒之后,如果调用fire方法则会立即执行。

  • invalidate

Stops the receiver from ever firing again and requests its removal from its run loop.
This method is the only way to remove a timer from an NSRunLoop object. The NSRunLoop object removes its strong reference to the timer, either just before the invalidate method returns or at some later point.
If it was configured with target and user info objects, the receiver removes its strong references to those objects as well.

这个是唯一一个可以将计时器从runloop中移出的方法,没有正确的使用该方法可能会导致定时器对象无法被释放,所以就有了下面探讨的问题。

iOS开发-NSTimer_第2张图片
NSTimer.png

3.循环引用问题

1.什么时候会出现循环引用?

我所理解的循环引用是两个对象相互强引用,或者多个对象强引用形成一个封闭的环。对象A强引用B,B再强引用A;或者A强引用B,B强引用C,C再强引用A...
接下来咱们回到NSTimer,首先你要明白几点细节:

  • timer会被添加到runloop中,否则不会运行,当然添加的runloop不存在也不会运行。
  • timer需要添加到runloop的一个或多个模式中,模式不对也不会运行。
  • runloop会对timer进行强引用,timer会对目标对象target进行强引用。
    看到最后一点应该差不多清晰了,当timer作为一个属性或者成员变量时,是被self强引用的,通常timer的目标target都是self,这个时候就导致循环引用了。

2.如何解决?

其实解决timer的循环引用,我们首先想到的应该是在合适的时机调用timer的invalidate方法,所以你可能会这样做

- (void)dealloc
{
    [_timer invalidate];
    _timer = nil;
}

但是在调delloc方法之前需要先释放定时器,而释放定时器又要走delloc方法,这显然是矛盾的。那再换一种思路

/**
 *在视图控制器视图消失的方法中释放定时器,
 *但是如果还有下一级新的控制器,则每次进入这个页面时都要重新添加定时器
 */
- (void)viewDidDisappear:(BOOL)animated
{
    [_timer invalidate];
    _timer = nil;
}

上面的确可以解决循环引用的问题,但不推荐,原因看注释。推荐下面的方法:

  • 引入中间类(一切问题都可以通过中间层解决)
//.h文件
@interface DXTimer : NSObject
- (void)cleanTimer;
@end

//.m文件
#import "DXTimer.h"
@interface DXTimer()
@property (nonatomic,strong) NSTimer *timer;
@end
@implementation DXTimer
- (instancetype)init
{
    if (self = [super init]) {
        _timer = [NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(timeOut:) userInfo:nil repeats:YES];
    }
    return self;
}
- (void)timeOut:(NSTimer *)timer
{
    NSLog(@"我还在呀...");
}
- (void)cleanTimer
{
    [_timer invalidate];
    _timer = nil;
}
//控制器中初始化中间类从而间接初始化定时器
- (void)testMyTimer
{
    _myTimer = [[DXTimer alloc] init];
}

/**
 *引入中间类时,只需要在delloc方法中调用中间类的释放定时器的方法
 */
- (void)dealloc
{
    [_myTimer cleanTimer];
}

引入DXTimer中间类后,引用关系是这样的,控制器强引用DXTimerDXTimer强引用timer,timer强引用DXTimer,控制器跟timer不存在相互引用关系,在控制器的delloc方法中调用DXTimercleanTimer方法释放定时器,从而使DXTimer也被释放。

  • 使用block

首先实现一个NSTimer的分类:

#import "NSTimer+Block.h"

@implementation NSTimer (Block)
+ (NSTimer *)dx_scheduledTimerWithTimeInterval:(NSTimeInterval)interval
                                       repeats:(BOOL)repeats
                                         block:(void (^)())block
{
    return [NSTimer scheduledTimerWithTimeInterval:interval target:self selector:@selector(dx_blockInvoke:) userInfo:[block copy] repeats:repeats];
}

+ (void)dx_blockInvoke:(NSTimer *)timer
{
    void (^block)(NSTimer *timer) = timer.userInfo;
    
    if (block) {
        block(timer);
    }
}
@end

控制器中初始化定时器:

//定义了一个__weak的self_weak_变量
#define weakifySelf  \
__weak __typeof(&*self)weakSelf = self

//局域定义了一个__strong的self指针指向self_weak
#define strongifySelf \
__strong __typeof(&*weakSelf)self = weakSelf

- (void)testBlockTimer
{
    weakifySelf;
    _blockTimer = [NSTimer dx_scheduledTimerWithTimeInterval:1 repeats:YES block:^{
        strongifySelf;
        [self timeOut];
    }];
}

- (void)timeOut
{
    NSLog(@"定时器还在...");
}

这里的weakifySelfstrongifySelf很关键,先定义了一个弱引用,令其指向self,然后使块捕获这个引用,而不直接去捕获普通的self变量。也就是说,self不会为计时器所保留。当块开始执行时,立刻生成strong引用,以保证实例在执行期间持续存活。

为避免循环引用的问题,iOS10定时器API新增了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));

关于定时器在面试中出现的频率还是挺高的,反正我好几次都被问到,所以决定写一篇总结。附上文章的一个简单demo代码NSTimer循环引用demo。

如果觉得文章对你有帮助,请不吝给个赞,谢谢!

你可能感兴趣的:(iOS开发-NSTimer)