NSTimer的循环引用
常见问题
日常开发中,经常会用到NSTimer定时器,一些不正确的写法,会导致NSTimer造成循环引用:
- (void)viewDidLoad {
[super viewDidLoad];
self.timer = [NSTimer timerWithTimeInterval:1 target:self selector:@selector(fireHome) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSRunLoopCommonModes];
}
- (void)fireHome {
num++;
NSLog(@"hello word - %d",num);
}
- (void)dealloc {
[self.timer invalidate];
self.timer = nil;
}
上述案例,一定会产生循环引用:
-
创建
NSTimer时,将self传入target,导致NSTimer持有self,而self又持有timer; -
使用
NSTimer的invalidate方法,可以解除NSTimer对self的持有; -
但案例中,
NSTimer的invalidate方法,由UIViewController的dealloc方法执行。但self被timer持有,只要timer有效,UIViewController的dealloc方法就不会执行。故此双方相互等待,造成循环引用。
target传入弱引用对象
对NSTimer的target参数传入一个弱引用的self,能否打破对self的强持有:
- (void)viewDidLoad {
[super viewDidLoad];
__weak typeof(self) weakSelf = self;
self.timer = [NSTimer timerWithTimeInterval:1 target:weakSelf selector:@selector(fireHome) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSRunLoopCommonModes];
}
- 肯定是不行的,因为在
timerWithTimeInterval内部,使用强引用对象接收target参数,所以外部定义为弱引用对象没有任何意义。
这种方式,类似于以下代码:
__weak typeof(self) weakSelf = self;
typeof(self) strongSelf = weakSelf;
在官方文档中,对target参数进行了明确说明:
target:定时器触发时指定的消息发送到的对象。计时器维护对该对象的强引用,直到它(计时器)失效。
和Block的区别,Block将捕获到的弱引用对象,赋值给一个强引用的临时变量,当Block执行完毕,临时变量会自动销毁,解除对外部变量的持有。
常规解决方案
更换API
使用携带Block的方法创建NSTimer,避免target的强持有:
+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)interval
repeats:(BOOL)repeats
block:(void (^)(NSTimer *timer))block;
在适当时机调用invalidate
根据业务需求,可以将NSTimer的invalidate方法写在viewWillDisappear方法中:
- (void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
self.timer = [NSTimer timerWithTimeInterval:1 target:self selector:@selector(fireHome) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSRunLoopCommonModes];
}
- (void)viewWillDisappear:(BOOL)animated {
[super viewWillDisappear:animated];
[self.timer invalidate];
self.timer = nil;
}
或者,写在didMoveToParentViewController方法中:
- (void)viewDidLoad {
[super viewDidLoad];
self.timer = [NSTimer timerWithTimeInterval:1 target:self selector:@selector(fireHome) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSRunLoopCommonModes];
}
- (void)didMoveToParentViewController:(UIViewController *)parent {
if (parent == nil) {
[self.timer invalidate];
self.timer = nil;
}
}
切断target强持有
除了常规解决方案,还可以通过切断target的强持有,解决循环引用的问题。
中介者模式
- (void)viewDidLoad {
[super viewDidLoad];
NSObject *objc = [[NSObject alloc] init];
class_addMethod([NSObject class], @selector(fireHome), (IMP)fireHomeObjc, "v@:");
self.timer = [NSTimer scheduledTimerWithTimeInterval:1 target:objc selector:@selector(fireHome) userInfo:nil repeats:YES];
}
void fireHomeObjc(id obj) {
num++;
NSLog(@"hello word - %d -%@",num, obj);
}
- (void)dealloc {
[self.timer invalidate];
self.timer = nil;
}
-
创建
NSObject实例对象objc,通过Runtime对NSObject增加fireHome方法,IMP指向fireHomeObjc的函数地址; -
创建
NSTimer,将objc传入target参数,这样避免NSTimer对self的强持有; -
当页面退出时,由于
self没有被NSTimer持有,正常调用dealloc方法:- 在
dealloc中,对NSTimer进行释放。此时NSTimer对objc的强持有解除,objc也跟着释放。
- 在
封装自定义Timer
创建LGTimerWapper,实现自定义Timer的封装。
打开LGTimerWapper.h文件,写入以下代码:
#import <Foundation/Foundation.h>
@interface LGTimerWapper : NSObject
- (instancetype)lg_initWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo;
- (void)lg_invalidate;
@end
打开LGTimerWapper.m文件,写入以下代码:
#import "LGTimerWapper.h"
#import <objc/message.h>
@interface LGTimerWapper()
@property (nonatomic, weak) id target;
@property (nonatomic, assign) SEL aSelector;
@property (nonatomic, strong) NSTimer *timer;
@end
@implementation LGTimerWapper
- (instancetype)lg_initWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo {
if (self == [super init]) {
self.target = aTarget;
self.aSelector = aSelector;
if ([self.target respondsToSelector:self.aSelector]) {
Method method = class_getInstanceMethod([self.target class], aSelector);
const char *type = method_getTypeEncoding(method);
class_addMethod([self class], aSelector, (IMP)fireHomeWapper, type);
self.timer = [NSTimer scheduledTimerWithTimeInterval:ti target:self selector:aSelector userInfo:userInfo repeats:yesOrNo];
}
}
return self;
}
void fireHomeWapper(LGTimerWapper *warpper){
if (warpper.target) {
void (*lg_msgSend)(void *,SEL, id) = (void *)objc_msgSend;
lg_msgSend((__bridge void *)(warpper.target), warpper.aSelector,warpper.timer);
} else {
[warpper lg_invalidate];
}
}
- (void)lg_invalidate {
[self.timer invalidate];
self.timer = nil;
}
@end
LGTimerWapper的调用代码:
- (void)viewDidLoad {
[super viewDidLoad];
self.timerWapper = [[LGTimerWapper alloc] lg_initWithTimeInterval:1 target:self selector:@selector(fireHome) userInfo:nil repeats:YES];
}
- (void)fireHome {
num++;
}
-
在
LGTimerWapper中,定义初始化lg_initWithTimeInterval方法和lg_invalidate释放方法; -
初始化方法:
-
控件内部的
target使用weak修饰,对ViewController进行弱持有; -
检测
target中是否存在该selector。如果存在,对当前类使用Runtime添加同名方法编号,指向自身内部fireHomeWapper的函数地址; -
创建真正的
NSTimer定时器,将控件自身的实例对象传入target,避免NSTimer对ViewController强持有。
-
-
当
NSTimer回调时,会进入fireHomeWapper函数:- 函数内部不负责业务处理,如果
target存在,使用objc_msgSend,将消息发送给target自身下的selector方法。
- 函数内部不负责业务处理,如果
-
当页面退出时,
ViewController可以正常释放。但LGTimerWapper和NSTimer相互持有,双方都无法释放; -
由于双方都无法释放,
NSTimer的回调会继续调用:-
当进入
fireHomeWapper函数,发现target已经不存在了,调用LGTimerWapper的lg_invalidate方法,内部对NSTimer进行释放; -
当
NSTimer释放后,对LGTimerWapper的强持有解除,LGTimerWapper也跟着释放。
-
NSProxy虚基类
NSProxy的作用:
-
OC不支持多继承,但是它基于运行时机制,可以通过NSProxy来实现伪多继承; -
NSProxy和NSObject属于同一级别的类,也可以说是一个虚拟类,只实现了NSObject的协议部分; -
NSProxy本质是一个消息转发封装的抽象类,类似一个代理人。
可以通过继承NSProxy,并重写以下两个方法实现消息转发:
- (void)forwardInvocation:(NSInvocation *)invocation;
- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel;
NSProxy除了可以用于多继承,也可以作为切断强持有的中间人。
打开LGProxy.h文件,写入以下代码:
#import <Foundation/Foundation.h>
@interface LGProxy : NSProxy
+ (instancetype)proxyWithTransformObject:(id)object;
@end
打开LGProxy.m文件,写入以下代码:
#import "LGProxy.h"
@interface LGProxy()
@property (nonatomic, weak) id object;
@end
@implementation LGProxy
+ (instancetype)proxyWithTransformObject:(id)object {
LGProxy *proxy = [LGProxy alloc];
proxy.object = object;
return proxy;
}
- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel {
if (!self.object) {
NSLog(@"异常收集-stack");
return nil;
}
return [self.object methodSignatureForSelector:sel];
}
- (void)forwardInvocation:(NSInvocation *)invocation {
if (!self.object) {
NSLog(@"异常收集-stack");
return;
}
[invocation invokeWithTarget:self.object];
}
@end
LGProxy的调用代码:
- (void)viewDidLoad {
[super viewDidLoad];
self.proxy = [LGProxy proxyWithTransformObject:self];
self.timer = [NSTimer scheduledTimerWithTimeInterval:1 target:self.proxy selector:@selector(fireHome) userInfo:nil repeats:YES];
}
- (void)fireHome {
num++;
NSLog(@"hello word - %d",num);
}
- (void)dealloc {
[self.timer invalidate];
self.timer = nil;
}
-
LGProxy初始化方法,将传入的object赋值给弱引用对象; -
在
UIViewController中,创建LGProxy对象proxy。创建NSTimer对象,将proxy传入target,避免NSTimer对ViewController强持有; -
当
NSTimer回调时,触发LGProxy的消息转发方法:-
methodSignatureForSelector:设置方法签名; -
forwardInvocation:自身不做业务处理,将消息转发给object。
-
-
当页面退出时,
ViewController可以正常释放:- 在
dealloc中,对NSTimer进行释放。此时NSTimer对proxy的强持有解除,proxy也跟着释放。
- 在