NSTimer大家都很熟悉,觉得用起来也很简单.然而,由NSTimer引起的循环引用,不经历过一次,一般很难查.
下面看一段代码:
- (void)viewDidLoad {
[super viewDidLoad];
_timer = [NSTimer scheduledTimerWithTimeInterval:0.1
target:self
selector:@selector(p_doSomeThing)
userInfo:nil
repeats:YES];
}
- (void)p_doSomeThing {
// doSomeThing
}
- (void)p_stopDoSomeThing {
[self.timer invalidate];
self.timer = nil;
}
- (void)dealloc {
[self.timer invalidate];
}
上面的代码主要是利用定时器重复执行p_doSomeThing方法,在合适的时候调用p_stopDoSomeThing方法使定时器失效.
能看出问题吗?在开始讨论上面代码问题之前,需要对NSTimer做一点说明.NSTimer的
scheduledTimerWithTimeInterval: target: selector: userInfo: repeats:
方法的最后一个参数为YES时,NSTimer会保留目标对象,等到自身失效才释放.执行完任务后,一次性的定时器会自动失效;重复性的定时器,需要主动调用invalidate
方法才会失效.
了解scheduledTimerWithTimeInterval: target: selector: userInfo: repeats:
最后一个参数含义之后,你发现何处出现了循环引用?
创建定时器时,当前控制器
(创建定时器的那个控制器,为了描述方便,简称当前控制器)引用而定时器了(为什么因引用定时器?后续要用到这个定时器对象),在给定时器添加任务时,定时器保留了self
(当前控制器).这里就出现了循环引用.
如果能在合适的时候打破循环引用,就不会有问题了.此时有两种选择:
1.控制器不再引用定时器
2.定时器不再保留当前控制器
第一种是不可行的,那么就只有第二种方法了.也就是合适的时候调用p_stopDoSomeThing
方法.然而,合适的时机很难找到.假如这是一个验证码倒计时程序,你可以在倒计时结束时调用p_stopDoSomeThing
方法.但是你不能确定用户一定会等倒计时结束才返回到上一级页面.或许你想在dealloc
方法中使定时器失效,那你就太天真了.此时定时器还保留着当前控制器,此方法是不可能调用的,因此会出现内存泄漏.或许在倒计时程序中,你可以重写返回方法,先调用p_stopDoSomeThing
再返回,但这不是一个好主意.
该问题出现的根本原因就是无法确保一定会调用p_stopDoSomeThing
方法.针对这一问题,有些人会选择自己实现一个不保留目标对象的定时器.这里,并不打算采用那种从头写起的方法,正如AFN作者所说的
无数开发者尝试自己做一个简陋而脆弱的系统来实现网络缓存的功能,殊不知 NSURLCache 只要两行代码就能搞定且好上 100 倍。
这里采用block块的方法为NSTimer增加一个分类,具体细节看代码(程序员最好的语言是代码).
//.h文件
#import <Foundation/Foundation.h>
@interface NSTimer (SGLUnRetain)
+ (NSTimer *)sgl_scheduledTimerWithTimeInterval:(NSTimeInterval)inerval
repeats:(BOOL)repeats
block:(void(^)(NSTimer *timer))block;
@end
//.m文件
#import "NSTimer+SGLUnRetain.h"
@implementation NSTimer (SGLUnRetain)
+ (NSTimer *)sgl_scheduledTimerWithTimeInterval:(NSTimeInterval)inerval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block{
return [NSTimer scheduledTimerWithTimeInterval:inerval target:self selector:@selector(sgl_blcokInvoke:) userInfo:[block copy] repeats:repeats];
}
+ (void)sgl_blcokInvoke:(NSTimer *)timer {
void (^block)(NSTimer *timer) = timer.userInfo;
if (block) {
block(timer);
}
}
@end
//控制器.m
#import "ViewController.h"
#import "NSTimer+SGLUnRetain.h"
//定义了一个__weak的self_weak_变量
#define weakifySelf \
__weak __typeof(&*self)weakSelf = self;
//局域定义了一个__strong的self指针指向self_weak
#define strongifySelf \
__strong __typeof(&*weakSelf)self = weakSelf;
@interface ViewController ()
@property(nonatomic, strong) NSTimer *timer;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
__block NSInteger i = 0;
weakifySelf
self.timer = [NSTimer sgl_scheduledTimerWithTimeInterval:0.1 repeats:YES block:^(NSTimer *timer) {
strongifySelf
[self p_doSomething];
NSLog(@"----------------");
if (i++ > 10) {
[timer invalidate];
}
}];
}
- (void)p_doSomething {
}
- (void)dealloc {
// 务必在当前线程调用invalidate方法,使得Runloop释放对timer的强引用(具体请参阅官方文档)
[self.timer invalidate];
}
@end
在使用中,最需要注意的就是下面这段代码:
weakifySelf
self.timer = [NSTimer sgl_scheduledTimerWithTimeInterval:0.1 repeats:YES block:^(NSTimer *timer) {
strongifySelf
[self p_doSomething];
NSLog(@"----------------");
if (i++ > 10) {
[timer invalidate];
}
}];
weakifySelf
宏定义了一个弱引用的weakSelf
,strongifySelf
宏又根据weakSelf
定义了一个强引用的self.这样在block内使用self和其他地方一样.这里的self
和block外的self
指向一样,但是不同的变量.
2017.1.6日补充:
在iOS10中,新的定时器即使不被引用,也可以正常运行,但是这依然会导致target不能释放,上面的方法依然十分有用。iOS10中,定时器的API新增了block方法,实现原理和这一样,只不过我这里用的是分类,而系统是在原始类中直接添加方法,最终的行为是一致的。