iOS开发-NSTimer探究

NSTimer 的使用

1、NSTimer 的创建

我们经常会使用下面四种常用的 NSTimer 的创建方法,都是类方法。

Target-action:
+ (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;

首先使用 Target-action 的方法创建 NSTimer:

 NSTimer *timer = [NSTimer timerWithTimeInterval:1 target:self selector:@selector(log) userInfo:nil repeats:YES];

使用 timerWithTimeInterval 开头的类方法,单是这样创建是不够的,NSTimer 只有加到 RunLoop 中的某个 mode 下才能运行(RunLoop 的相关内容就不在这里展开了),所以我们还需要手动把 NSTimer 加到 RunLoop 中的 NSDefaultRunLoopMode 下:

[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];

使用 scheduledTimerWithTimeInterval 开头的类方法,则不需要手动把 NSTimer 加到 RunLoop 中,因为方法内默认将 NSTimer 添加到 RunLoop 中的 NSDefaultRunLoopMode 下。

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

2、 NSTimer 的销毁

NSTimer的唯一销毁方法 invalidate,本质上是将 NSTimer 从 RunLoop 中移除,撤销 RunLoop 对 NSTimer 的强引用。

- (void)invalidate;

[timer invalidate];

 在对 NSTimer 使用 invalidate 方法之后,最好还将 NSTimer 置为 nil,这是一种良好的规范和习惯。

timer = nil;

NSTimer 其实还有一个 fire 方法:如果 NSTimer 不重复,则在触发后会自动失效。

- (void)fire;

 而 fire 方法并不会将 NSTimer 从 RunLoop 中移除,所以不管怎样最后一定是要调用 invalidate 方法的,这也是我们不怎么看见 fire 方法的原因。

注意,使用 NSTimer 一定要记得销毁。

NSTimer 的循环引用

1、首先我们创建一个 OneViewController,我们先看一下 OneViewController 什么时候销毁:

@implementation OneViewController

- (void)viewDidLoad {
    [super viewDidLoad];
}

- (void)dealloc {
    NSLog(@"dealloc OneViewController");
}

-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    NSLog(@"从OneViewController返回!");
    [self.navigationController popViewControllerAnimated:YES];
}

我们把OneViewController push出来,再pop出去,看一下结果:

2019-02-18 23:46:45.621 --------[11119:550693] 从OneViewController返回!
2019-02-18 23:46:46.128 --------[11119:550693] dealloc OneViewController

由此可知,OneViewController 一被 pop 出去立即就销毁了,所以 OneViewController 的生命周期没有问题。 

2、然后我们给 OneViewController 添加 NSTimer,并在 dealloc 中将 NSTimer 销毁:

@property(nonatomic, strong) NSTimer *time;

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

- (void)log {
    NSLog(@"self.time 出来了!");
}

- (void)dealloc {
    NSLog(@"dealloc OneViewController");
    [self.time invalidate];
    self.time = nil;
}

 我们再次进行相同的操作,看一下结果: 

​2019-02-19 00:00:34.431 --------[11223:557177] self.time出来了!
2019-02-19 00:00:35.273 --------[11223:557177] 从OneViewController返回!
2019-02-19 00:00:35.430 --------[11223:557177] self.time出来了!
2019-02-19 00:00:36.431 --------[11223:557177] self.time出来了!

可以看出,pop 之后 OneViewController 并没有被 dealloc,并且 NSTimer 还在运行。 

显然,NSTimer 和 OneViewController 构成了循环引用:self -> time -> self,所以 NSTimer 与 OneViewController 都无法被销毁。

有人可能会说是 target:self 的问题,那么我们试一下 weakSelf:

__weak typeof (self) weakSelf = self;
self.time = [NSTimer scheduledTimerWithTimeInterval:1 target:weakSelf selector:@selector(log) userInfo:nil repeats:YES];

我们再次进行相同的操作,看一下结果: 

2019-02-19 00:34:55.365 --------[11275:564482] self.time出来了!
2019-02-19 00:34:55.482 --------[11275:564482] 从OneViewController返回!
2019-02-19 00:34:56.365 --------[11275:564482] self.time出来了!
2019-02-19 00:34:57.366 --------[11275:564482] self.time出来了!

从结果来看,使用 weakSelf 对 NSTimer 而言同样会形成循环引用。

解决 NSTimer 循环引用的方法

1、合适的时机启动和销毁 NSTimer

解决 NSTimer 的循环引用,我们首先会想到的方法应该是在 OneViewController dealloc 之前就销毁 NSTimer,这样循环就被打破了。

最简单的方法就是在 viewWillAppear 中启动 NSTimer,然后在 viewWillDisappear 中销毁 NSTimer,成对出现,绝对没有问题。

-(void)viewWillAppear:(BOOL)animated {
    [super viewWillAppear:animated];
     self.time = [NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(log) userInfo:nil repeats:YES];
}

- (void)viewWillDisappear:(BOOL)animated {
    [super viewWillDisappear:animated];
    [self.time invalidate];
    self.time = nil;
}

缺点就是, 容易忘记,维护起来比较麻烦。

2、使用iOS10之后提供的 block方法

在iOS10之前,NSTimer 只有两个Target-action方法可用,这需要我们自己在 NSTimer 的分类中自定义 block 方法:

@implementation NSTimer (block)

+ (NSTimer *)sst_scheduledTimerWithTimeInterval:(NSTimeInterval)ti repeats:(BOOL)yesOrNo block:(void (^)(NSTimer *timer))block {
    return [self scheduledTimerWithTimeInterval:ti target:self selector:@selector(blcokInvoke:) userInfo:[block copy] repeats:yesOrNo];
}

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

@end

block 的实现原理很简单,主要是 target 变为了 NSTimer 类对象,这样 OneViewController 的释放就不被 NSTimer 所影响,这样我们就可以在 dealloc 中销毁 NSTimer 了。

苹果官方也注意到了这个问题,然后在iOS10中,给 NSTimer 新增了 block 方法,实现原理与此类似,这里采用分类为 NSTimer 添加了 block 方法,而苹果官方直接在原始类中直接添加 block 方法,殊途同归,使用流程也是一样的。

block(iOS10以后提供):
+ (NSTimer *)timerWithTimeInterval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block;
+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block;

3、引入中间类,替换 NSTimer 的 target

最常用的方法就是引入一个中间类,NSTimer 作为中间类的属性存在,让这个中间类弱引用 NSTimer 的 target,然后把 NSTimer 的 target 改为这个中间类。

然后 target、NSTimer 和中间类之间的持有关系就会如下图所示:

此时, target 的释放就不会被 NSTimer 以及中间类影响,可以正常释放,然后在 target dealloc 中释放中间类和 NSTimer 即可。

但是,最方便的使用方式,应该是能让中间类和 NSTimer 自己管理生命周期,在 target 中不需要手动去释放,只管创建和使用。

所以我提供了一个终极解决方案,利用 target 来让中间类和 NSTimer 自己管理生命周期,代码和原理都很简单,就不再赘述了。

#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

@interface SSTimer : NSObject

//我多添加了两个带有runLoopMode参数的方法,方便跟换runLoopMode

#pragma mark - clean timer 本身不需要手动调用,提供给想要在viewWillDisappear中释放timer的

- (void)cleanTimer;

#pragma mark - Target-action

+ (SSTimer *)timerWithTimeInterval:(NSTimeInterval)timeInterval target:(id)target selector:(SEL)selector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo;

+ (SSTimer *)timerWithTimeInterval:(NSTimeInterval)timeInterval target:(id)target selector:(SEL)selector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo runLoopMode:(NSRunLoopMode)runLoopMode;

+ (SSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)timeInterval target:(id)target selector:(SEL)selector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo;

#pragma mark - block 因为需要target来自动释放,所以需要多传一个target

+ (SSTimer *)timerWithTimeInterval:(NSTimeInterval)timeInterval target:(id)target repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block;

+ (SSTimer *)timerWithTimeInterval:(NSTimeInterval)timeInterval target:(id)target repeats:(BOOL)repeats runLoopMode:(NSRunLoopMode)runLoopMode block:(void (^)(NSTimer *timer))block;

+ (SSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)timeInterval target:(id)target repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block;

@end

NS_ASSUME_NONNULL_END
#import "SSTimer.h"

@interface SSTimer ()

@property (nonatomic, strong) NSTimer *timer;//真正的NSTimer

@property (nonatomic, assign) SEL selector;  //因为要替换timer的selector,所以SSTTimer需要存储target的selector

@property (nonatomic, weak) id target;       //弱引用原本是timer的target,将timer的target变为自己(SSTimer)

@end

@implementation SSTimer

#pragma mark - dealloc && clean

- (void)dealloc
{
    NSLog(@"dealloc SSTimer!");
    if (self.timer) {
        [self cleanTimer];
    }
}

- (void)cleanTimer
{
    NSLog(@"clean timer!");
    [self.timer invalidate];
    self.timer = nil;
}

#pragma mark - init

+ (SSTimer *)timerWithTarget:(_Nullable id)target selector:(_Nullable SEL)selector
{
    SSTimer *timerTarget = [[SSTimer alloc]init];
    timerTarget.target    = target;
    timerTarget.selector  = selector;
    return timerTarget;
}

#pragma mark - Target-action method

+ (SSTimer *)timerWithTimeInterval:(NSTimeInterval)timeInterval target:(id)target selector:(SEL)selector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo
{
    SSTimer *timerTarget = [SSTimer timerWithTarget:target selector:selector];
    timerTarget.timer = [NSTimer timerWithTimeInterval:timeInterval
                                                target:timerTarget
                                              selector:@selector(timerInvoke:)
                                              userInfo:userInfo
                                               repeats:yesOrNo];
    return timerTarget;
}

+ (SSTimer *)timerWithTimeInterval:(NSTimeInterval)timeInterval target:(id)target selector:(SEL)selector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo runLoopMode:(NSRunLoopMode)runLoopMode
{
    SSTimer *timerTarget = [SSTimer timerWithTarget:target selector:selector];
    timerTarget.timer = [NSTimer timerWithTimeInterval:timeInterval
                                                target:timerTarget
                                              selector:@selector(timerInvoke:)
                                              userInfo:userInfo
                                               repeats:yesOrNo];
    [[NSRunLoop currentRunLoop] addTimer:timerTarget.timer forMode:runLoopMode];
    return timerTarget;
}

+ (SSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)timeInterval target:(id)target selector:(SEL)selector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo
{
    SSTimer *timerTarget = [SSTimer timerWithTarget:target selector:selector];
    timerTarget.timer = [NSTimer scheduledTimerWithTimeInterval:timeInterval
                                                         target:timerTarget
                                                       selector:@selector(timerInvoke:)
                                                       userInfo:userInfo
                                                        repeats:yesOrNo];
    return timerTarget;
}

- (void)timerInvoke:(NSTimer *)timer
{
    //判断target是否被释放
    if (self.target) {
        if (![self.target respondsToSelector:self.selector]) {
            NSLog(@"未找到NSTimer的selector方法!");
            [self cleanTimer];
            return;
        }
        #pragma clang diagnostic push
        #pragma clang diagnostic ignored "-Warc-performSelector-leaks"
            [self.target performSelector:self.selector withObject:timer.userInfo];//执行target中的方法
        #pragma clang diagnostic pop
    } else {
        [self cleanTimer];
    }
}

#pragma mark - block method

+ (SSTimer *)timerWithTimeInterval:(NSTimeInterval)timeInterval target:(id)target repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block
{
    SSTimer *timerTarget = [SSTimer timerWithTarget:target selector:nil];
    timerTarget.timer = [NSTimer timerWithTimeInterval:timeInterval
                                                target:timerTarget
                                              selector:@selector(blcokInvoke:)
                                              userInfo:[block copy]
                                               repeats:repeats];
    return timerTarget;
}

+ (SSTimer *)timerWithTimeInterval:(NSTimeInterval)timeInterval target:(id)target repeats:(BOOL)repeats runLoopMode:(NSRunLoopMode)runLoopMode block:(void (^)(NSTimer *timer))block
{
    SSTimer *timerTarget = [SSTimer timerWithTarget:target selector:nil];
    timerTarget.timer = [NSTimer timerWithTimeInterval:timeInterval
                                                target:timerTarget
                                              selector:@selector(blcokInvoke:)
                                              userInfo:[block copy]
                                               repeats:repeats];
    [[NSRunLoop currentRunLoop] addTimer:timerTarget.timer forMode:runLoopMode];
    return timerTarget;
}

+ (SSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)timeInterval target:(id)target repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block
{
    SSTimer *timerTarget = [SSTimer timerWithTarget:target selector:nil];
    timerTarget.timer = [NSTimer scheduledTimerWithTimeInterval:timeInterval
                                                         target:timerTarget
                                                       selector:@selector(blcokInvoke:)
                                                       userInfo:[block copy]
                                                        repeats:repeats];
    return timerTarget;
}

- (void)blcokInvoke:(NSTimer *)timer
{
    //之前将要执行的block copy放在userInfo中,取出
    void (^block)(NSTimer *timer) = timer.userInfo;
    //判断target是否被释放
    if (self.target) {
        if (block) {
            block(timer);//执行block
        }
    } else {
        [self cleanTimer];//清除timer
    }
}

@end

猜你喜欢

转载自blog.csdn.net/qq_36557133/article/details/91355258