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