iOS中解决NSTimer循环引用问题
NSTimer
使用不当就会造成内存泄漏,比如常见的使用方法:
//定义
@property (nonatomic, strong) NSTimer *timer;
//实现
self.timer = [NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(showMsg) userInfo:nil repeats:YES];
//销毁
-(void)dealloc
{
[self.timer invalidate];
self.timer = nil;
}
由于NSTimer
会引用住self
,而 self
又持有NSTimer
对象,所以形成循环引用,dealloc
永远不会被执行,timer
也永远不会被释放,造成内存泄漏。
尝试解决办法:
1、把timer
改成弱引用
@property (nonatomic, weak) NSTimer *timer;
虽然
self
对timer
是弱引用,但是控制的delloc
方法的执行依赖于timer
的invalidate
,timer
的invalidate
又依赖于控制器的delloc
方法,这是一个鸡生蛋还是蛋生鸡的问题,依旧是循环引用;
2、使用__weak
那换个思路能不能让NSTimer
弱引用target
:
__weak typeof(self) weakSelf = self;
self.timer = [NSTimer scheduledTimerWithTimeInterval:1 target:weakSelf selector:@selector(showMsg) userInfo:nil repeats:YES];
weak
关键字适用于block
,当block
引用了块外的变量时,会根据修饰变量的关键字来决定是强引用还是弱引用,如果变量使用weak
关键字修饰,那block
会对变量进行弱引用,如果没有__weak
关键字,那就是强引用。
但是NSTimer
的scheduledTimerWithTimeInterval:target
方法内部不会判断修饰target
的关键字,所以这里传self
和weakSelf
是没区别的,其内部会对target
进行强引用,还是会产生循环引用。
3、 选择合适的时机手动释放timer
采用下面的方法解决循环引用:
- (void) viewWillDisappear:(BOOL)animated {
[super viewWillDisappear:animated];
if (self.timer) {
[self.timer invalidate];
self.timer = nil;
}
}
在某些情况下,这种做法是可以解决问题的,但是有时却会引起其他问题,比如控制器
push
到下一个控制器,viewDidDisappear
执行后,timer
被释放,此时再pop
回来,timer
已经不复存在了。所以,这种"方案"并不是合理的。
优化上面的方法这个时候可以采用配对使用在 viewWillAppear
开timer
启,在 viewWillDisappear
关闭timer
:
-(void)viewWillAppear:(BOOL)animated{
[super viewWillAppear:animated];
if (!self.timer) {
self.timer = [NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(showMsg) userInfo:nil repeats:YES];
}
}
-(void)viewWillDisappear:(BOOL)animated{
[super viewWillDisappear:animated];
if (self.timer) {
[self.timer invalidate];
self.timer = nil;
}
}
上面的方法只是维护起来比较麻烦
最终解决办法
1、自定义category
用block
解决
网上有一些封装的比较好的block
的解决方案,思路无外乎是封装一个NSTimer
的category
,提供block
形式的接口:
#import <Foundation/Foundation.h>
@interface NSTimer (TimerBlock)
/**
分类解决NSTimer在使用时造成的循环引用的问题
@param interval 间隔时间
@param block 回调
@param repeats 用于设置定时器是否重复触发
@return 返回NSTimer实体
*/
+ (NSTimer *)block_TimerWithTimeInterval:(NSTimeInterval)interval block:(void(^)())block repeats:(BOOL)repeats;
@end
#import "NSTimer+TimerBlock.h"
@implementation NSTimer (TimerBlock)
+ (NSTimer *)block_TimerWithTimeInterval:(NSTimeInterval)interval block:(void (^)())block repeats:(BOOL)reqeats{
return [self timerWithTimeInterval:interval target:self selector:@selector(blockSelector:) userInfo:[block copy] repeats:reqeats];
}
+ (void) blockSelector:(NSTimer *)timer{
void (^block)() = timer.userInfo;
if (block) {
block();
}
}
@end
上述创建方式调用者是NSTImer
自己,只是NSTimer
捕获了参数block
。这样我们在使用timer
时,由于target
的改变,就不再有循环引用了。
__weak typeof(self) weakSelf = self; //避免 block 强引用 self
self.timer = [NSTimer block_TimerWithTimeInterval:3 block:^{
// [weakSelf dosomething];
} repeats:YES];
iOS10中,定时器的API新增了
block
方法,实现原理与此类似,这里采用分类为NSTimer
添加了带有block
参数的方法,而系统是在原始类中直接添加方法,最终的行为是一致的。
+ (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));
2、给self
添加中间件proxy
引入一个对象proxy
,proxy
弱引用 self
,然后 proxy
传入NSTimer
。即self
强引用NSTimer
,NSTimer
强引用 proxy
,proxy
弱引用 self
,这样通过弱引用来解决了相互引用,此时不会形成环。
定义一个继承自NSObject
的中间代理对象FFProxy
,ViewController
不持有timer
,而是持有FFProxy
实例,让FFProxy
实例来弱引用ViewController
,timer
强引用FFProxy
实例,直接看代码:
//FFProxy.h
@interface FFProxy : NSObject
+(instancetype)proxyWithTarget:(id)target;
@end
//FFProxy.m
#import "FFProxy.h"
@interface FFProxy()
@property (nonatomic ,weak) id target;
@end
@implementation FFProxy
+(instancetype)proxyWithTarget:(id)target
{
FFProxy *proxy = [[FFProxy alloc] init];
proxy.target = target;
return proxy;
}
//仅仅添加了weak类型的属性还不够,为了保证中间件能够响应外部self的事件,需要通过消息转发机制,让实际的响应target还是外部self,这一步至关重要,主要涉及到runtime的消息机制。
-(id)forwardingTargetForSelector:(SEL)aSelector
{
return self.target;
}
@end
//ViewController.m
FFProxy *proxy = [FFProxy proxyWithTarget:self];
//将timer的target设置为proxy,proxy又弱引用了控制器,其实最终还是调用了控制器的showMsg方法。
self.timer = [NSTimer scheduledTimerWithTimeInterval:1 target:proxy selector:@selector(showMsg) userInfo:nil repeats:YES];
//销毁
-(void)dealloc
{
[self.timer invalidate];
self.timer = nil;
}
- (id)forwardingTargetForSelector:(SEL)aSelector
是什么?
消息转发,简单来说就是如果当前对象没有实现这个方法,系统会到这个方法里来找实现对象。
本文中由于当前target
是FFProxy
,但是FFProxy
没有实现showMsg
方法(当然也不需要它实现),让系统去找target
实例的方法实现,也就是去找ViewController
中的方法实现。
3、使用NSProxy
类
使用iOS
的NSProxy
类,NSProxy
就是专门用来做消息转发的。
//FFWeakProxy.h
@interface FFWeakProxy : NSProxy
+ (instancetype)proxyWithTarget:(id)target;
@end
//FFWeakProxy.m
@interface FFWeakProxy()
@property (nonatomic ,weak)id target;
@end
@implementation FFWeakProxy
+ (instancetype)proxyWithTarget:(id)target {
//NSProxy实例方法为alloc
FFWeakProxy *proxy = [FFWeakProxy alloc];
proxy.target = target;
return proxy;
}
/**
这个函数让重载方有机会抛出一个函数的签名,再由后面的forwardInvocation:去执行
为给定消息提供参数类型信息
*/
- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel {
return [self.target methodSignatureForSelector:sel];
}
/**
* NSInvocation封装了NSMethodSignature,通过invokeWithTarget方法将消息转发给其他对象。这里转发给控制器执行。
*/
- (void)forwardInvocation:(NSInvocation *)invocation {
[invocation invokeWithTarget:self.target];
}
@end
Controller
里代码如下:
- (void)viewDidLoad {
[super viewDidLoad];
// 这里的target又发生了变化
self.timer = [NSTimer scheduledTimerWithTimeInterval:1 target:[FFWeakProxy proxyWithTarget:self] selector:@selector(showMsg) userInfo:nil repeats:YES];
}
//销毁
-(void)dealloc
{
[self.timer invalidate];
self.timer = nil;
}