一、NSTimer 的使用方法
系统提供了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 添加到runloop。
-(void)createTimer0 {
/**定时器会自动启动*/
_timer = [NSTimer scheduledTimerWithTimeInterval:1.0f target:self selector:@selector(doSomething5) userInfo:nil repeats:YES];
}
-(void)createTimer00 {
/**定时器不会自动启动,需要添加到RunLoop中*/
_timer = [NSTimer timerWithTimeInterval:1.0f target:self selector:@selector(doSomething5) userInfo:nil repeats:YES];
}
二、NSTimer 和 VC 造成的循环引用
下面一个普通直接使用
#import "TestViewController.h"
@interface TestViewController ()
@property (nonatomic, strong) NSTimer *timer;
@property (nonatomic, assign) NSInteger indexNum;
@end
@implementation TestViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.indexNum = 0;
self.timer = [NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(doSomething) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSRunLoopCommonModes];
}
- (void)doSomething{
NSLog(@" ---当前indexNum: %ld----", (long)self.indexNum ++);
}
- (void)dealloc{
NSLog(@"************ 走进dealloc ***************");
[self.timer invalidate];
self.timer = nil;
}
@end
进入页面后, 倒计时触发,定时器指定方法开始执行。 点击返回按钮后, 不会触发 dealloc 方法, 但控制台依然在打印indexNum值,说明内存无法释放。
结论:VC 强持有 timer, 而timer的创建方法中参数target使得timer持有VC,造成循环引用。视图控制器在自动调用dealloc前,会判断有没有未销毁的NSTimer对象,如果有就不调用dealloc方法。
设想:target中的self编程弱引用weakSelf可以吗?答案是不可以,即使设置为弱引用self,delloc依然不会走
weak关键字适用于block,当block引用了块外的变量时,会根据修饰变量的关键字来决定是强引用还是弱引用,如果变量使用weak关键字修饰,那block会对变量进行弱引用,如果没有__weak关键字,那就是强引用。
但是NSTimer的 scheduledTimerWithTimeInterval:target方法内部不会判断修饰target的关键字,所以这里传self 和 weakSelf是没区别的,其内部会对target进行强引用,还是会产生循环引用。
无论如何创建。都会将NSTimer 加入到当前的RunLoop当中。所以RunLoop就持有该timer。即VC和timer相互引用,Runloop同时也引用timer。
三、NSTimer和VC循环引用解决方案:
方案一:手动销毁定时器
1.在viewDidDisappear里面释放定时器
@implementation TestViewController
- (void)viewDidDisappear:(BOOL)animated {
[super viewDidDisappear:animated];
[self.timer invalidate];
self.timer = nil;
}
- (void)viewDidLoad {
[super viewDidLoad];
self.indexNum = 0;
self.timer = [NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(doSomething) userInfo:nil repeats:YES];
}
- (void)doSomething{
NSLog(@" ---当前indexNum: %ld----", (long)self.indexNum ++);
}
- (void)dealloc{
NSLog(@"************ 走进dealloc ***************");
}
@end
2.在特定的场景,比如点击某一个按钮,释放定时器
方案二:创建一个NSObject对象作为定时器target对象,主要原理:利用forwardingTargetForSelector做消息转发
这个方案的实现方式主要是加入了一个中间者proxy,使得timer不直接持有self,而是持有proxy,让proxy对象弱引用self来解决循环引用。当定时器回调的时候,通过消息转发机制,把消息重定向给self。
代码如下:
@interface MLProxy : NSObject
+ (instancetype)createProxyWeithTarget:(id)target;
- (id)forwardingTargetForSelector:(SEL)aSelector;
@end
#import "MLProxy.h"
@interface MLProxy ()
@property (nonatomic, weak) id target;
@end
@implementation MLProxy
+ (instancetype)createProxyWeithTarget:(id)target{
MLProxy *proxy = [[self alloc] init];
proxy.target = target;
return proxy;
}
//消息重定向
- (id)forwardingTargetForSelector:(SEL)aSelector{
return self.target;
}
@end
@interface FirstViewController ()
@property (nonatomic,strong)NSTimer *timer;
@property (nonatomic,strong)MLProxy *proxy;
@property (nonatomic, assign) NSInteger indexNum;
@end
@implementation FirstViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.indexNum = 0;
[self addTimer];
}
-(void)addTimer{
self.proxy = [MLProxy createProxyWeithTarget:self];
// NSTimer
// 由于iOS的消息机制(objc_msgSend()), 系统会对self.proxy 发送一个scheduledTimerWithTimeInterval的消息由于MLProxyb并没有实现该方法,就会执行Runtime的消息转发机制。
_timer = [NSTimer scheduledTimerWithTimeInterval:1 target:self.proxy selector:@selector(timerAction) userInfo:nil repeats:YES];
}
- (void)timerAction{
NSLog(@"走到这里了");
NSLog(@"self.class=%@",[self class]);
NSLog(@" ---当前indexNum: %ld----", (long)self.indexNum ++);
}
- (void)dealloc{
NSLog(@"************ 走进dealloc ***************");
[_timer invalidate];
_timer = nil;
}
方案三:NSProxy
NSProxy我理解的其实它就是一个消息重定向封装的一个抽象类,类似一个代理人、中间件。可以通过继承它,并重写下面这两个方法来实现消息转发到另一个实例:
- (void)forwardInvocation:(NSInvocation *)invocation;
- (nullable NSMethodSignature *)methodSignatureForSelector:(SEL)sel
代码如下:
@interface XTProxy : NSProxy
@property (nonatomic, weak) id target;
@end
#import "XTProxy.h"
@interface XTProxy ()
@end
@implementation XTProxy
- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel {
return [self.target methodSignatureForSelector:sel];
}
- (void)forwardInvocation:(NSInvocation *)invocation {
[invocation invokeWithTarget:self.target];
}
@interface ThirdViewController ()
@property (nonatomic,strong)XTProxy *proxy;
@property (nonatomic,strong)NSTimer *timer;
@property (nonatomic, assign) NSInteger indexNum;
@end
@implementation ThirdViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.indexNum = 0;
[self addTimer];
}
-(void)addTimer {
self.proxy = [XTProxy alloc];
self.proxy.target = self;
_timer = [NSTimer scheduledTimerWithTimeInterval:1.0f target:self.proxy selector:@selector(timerAction) userInfo:nil repeats:YES];
}
- (void)timerAction{
NSLog(@"走到这里了");
NSLog(@"self.class=%@",[self class]);
NSLog(@" ---当前indexNum: %ld----", (long)self.indexNum ++);
}
- (void)dealloc{
NSLog(@"************ 走进dealloc ***************");
[_timer invalidate];
_timer = nil;
}
方案四:使用Apple提供的API类,但该方法仅限于ios10以上,有很多局限性
+ (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));
代码如下:
@interface FourViewController ()
@property (nonatomic,strong)NSTimer *timer;
@property (nonatomic, assign) NSInteger indexNum;
@end
@implementation FourViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.indexNum = 0;
[self addTimer];
}
-(void)addTimer {
__weak typeof(self) weakSelf = self;
if (@available(iOS 10.0, *)) {
_timer = [NSTimer scheduledTimerWithTimeInterval:1 repeats:YES block:^(NSTimer * _Nonnull timer) {
[weakSelf timerAction];
}];
}else {
NSLog(@"该方法不支持");
}
}
- (void)timerAction{
NSLog(@"走到这里了");
NSLog(@"self.class=%@",[self class]);
NSLog(@" ---当前indexNum: %ld----", (long)self.indexNum ++);
}
- (void)dealloc{
NSLog(@"************ 走进dealloc ***************");
[_timer invalidate];
_timer = nil;
}
@end
四、NSTimer不准确的问题探究及解决:
NSTimer不准确原因:
1、NSTimer定时器依赖于RunLoop,我们知道RunLoop每循环一次的时间是基于任务量的,即每一次的时间都不一定相同,如果RunLoop的任务过于繁重,可能会导致NSTimer不准时。
2、模式的切换,当创建的timer被加入到NSDefaultRunLoopMode时,此时如果有滑动UIScrollView的操作,runLoop 的mode会切换为TrackingRunLoopMode,而MainRunLoop处于 UITrackingRunLoopMode 的模式下,是不会处理 NSDefaultRunLoopMode 的消息(因为它们的RunLoop Mode不一样),所以timer会暂时停止;
对于精度要求不高的场景我们使用NSTimer没太大影响,对于上面2的滑动UIScrollView的导致定时器停止的问题下面一行代码即可解决:
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
解决方案:
使用GCD定时器,因为GCD定时器是基于系统内核的,所以不会受其他因素影响。