NSTimer 循环引用的原因和解决方案
造成循环引用的原因就是两个对象之间因为强引用无法释放。本文将通过NSTimer
来剖析强引用,以及解决方法。
1. 强引用
举个例子,比如我们有两个ViewController
,分别为A
和B
,从A
可以push
到B
,从B
可以pop
回A
,B
中代码如下:
static int num = 0;
@property (nonatomic, strong) NSTimer *timer;
- (void)viewDidLoad {
[super viewDidLoad];
self.timer = [NSTimer timerWithTimeInterval:1 target:self selector:@selector(fireHome) userInfo:nil repeats:YES];
// 加runloop
[[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSDefaultRunLoopMode];
}
- (void)fireHome{
num++;
NSLog(@"hello word - %d",num);
}
- (void)dealloc{
[self.timer invalidate];
self.timer = nil;
NSLog(@"%s",__func__);
}
当我们从B
界面pop
到A
时,timer
并不会停,那是为什么呢?显然是没有执行B
界面的dealloc
方法,导致B
界面没有被释放。
既然没释放肯定是有循环引用,那么这个循环引用产生的在哪里呢?乍一看,我们的BViewController
强引用了timer
,那么如果说造成循环引用就是timer
强引用了self
,但是这里面没有block
怎么产生的循环引用呢?这里面在初始化timer
的时候有个target
,我们查看一下这个初始化方法shift+command+0
,搜索一下timerWithTimeInterval:target:selector:userInfo:repeats:
关于target
的描述如下:
可以看到timer
对target
保持强引用,直到timer
失效。
所以说循环引用就产生了,B
强引用着timer
,timer
强引用着target
也就是self
,在这里self
就是B
的实例对象。此时就是:
self -> timer -> self
构成的循环引用。
我们在iOS Objective-C Block简介这篇文章中介绍了使用weakSelf
来解决循环引用,既然是这样,那么我们用weakSelf
是否可以解决这层循环引用呢?
将代码修改为如下:
- (void)viewDidLoad {
[super viewDidLoad];
__weak typeof(self) weakSelf = self;
self.timer = [NSTimer timerWithTimeInterval:1 target:weakSelf selector:@selector(fireHome) userInfo:nil repeats:YES];
// 加runloop
[[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSDefaultRunLoopMode];
}
运行,依旧没有打破循环引用,timer
在pop
后依旧运行。那么这是为什么呢?,在block
中我们可以使用weakSelf
来打破循环引用,那么在这里为什么不行呢?
此时我们使用__weak
虽然打破了self -> timer -> self
这个循环引用,使其变成了self -> timer -> weakSelf -> self
。
但是这里我们分析的并不全面,因为我们的timer
需要加入到Runloop
,Runloop
对timer
是一个强持有,Runloop
的生命周期比B
界面更长,所以这才是导致timer
无法释放的真正原因,timer
无法释放,自然self
也就无法释放。所以这个引用链最初应该是这样的:
self -> timer -> self
runloop -> timer -> self
画个图:
image加上weakSelf
之后,变成了这样:
self -> timer -> weakSelf -> self
runloop -> timer -> weakSelf -> self
那么虽然是这样weakSelf
也是弱引用啊,为什么不能打破循环引用呢?在block
中我们可以通过self -> block -> weakSelf -> self
打破循环引用?为什么这里就不可以了呢?
这里我们就要稍微研究一下这行代码了:
__weak typeof(self) weakSelf = self;
我们想知道weakSelf
和self
有什么区别,其实主要是这三点:
-
weakSelf
会对self
的引用计数+1
吗? -
weakSelf
和self
的指针地址相同吗? -
weakSelf
和self
是指向同一片内存空间吗?
下面我们验证一下,添加这样一段代码:
NSLog(@"%ld",CFGetRetainCount((__bridge CFTypeRef)self));
__weak typeof(self) weakSelf = self;
NSLog(@"%ld",CFGetRetainCount((__bridge CFTypeRef)self));
运行并通过lldb
调试得到如下结果:
我们可以看到:
-
weakSelf
并没有增加self
的引用计数 -
weakSelf
和self
指向同一内存区域 -
weakSelf
和self
的指针地址是不同的
其实分析完这里我们也看不出什么,这里的引用关系还是这幅图:
image下面我们在看看block中的weakSelf
,添加如下代码:
@property (nonatomic, copy) void(^myBlock)(void);
@property (nonatomic, copy) NSString *name;
- (void)test1 {
__weak typeof(self) weakSelf = self;
self.name = @"test1";
self.myBlock = ^{
NSLog(@"%@",weakSelf.name);
};
self.myBlock();
}
调用test1
,通过lldb
调试:
此时就很清晰了,block
中的weakSelf
与外面的weakSelf
根本不是同一个对象,虽然他们指向的都是同一片内存区域,在这里就是<LGTimerViewController: 0x7fc275604b10>
,下面我们在看看libclosure
中的_Block_object_assign
函数。
在这里我们看到都是取的对象的地址**
,或者是通过_Block_copy
拷贝一份,也就是说在block
中都是临时变量,一份新的变量,所以说在block
中其引用链并不存在对weakSelf
持有,而是持有的weakSelf
的指针地址,也就是*weakSelf
,跟self
没有任何关系。
然而在timer
这里,timer
对weakSelf
也就是target
是强持有,所以不能打破循环引用。
所以对于block
和timer
两个模型之间循环引用的区别如下:
timer:self -> timer -> weakSelf -> self
block:self -> block -> *weakSelf
2. 解决Timer强引用
2.1 不使用带target的Timer
因为timer
通过target
强持有了self
,那么我们不使用含有target
的API不就就可以了,修改代码为如下:
self.timer = [NSTimer scheduledTimerWithTimeInterval:1 repeats:YES block:^(NSTimer * _Nonnull timer) {
NSLog(@"hello word - %d",num);
}];
2.2 提前销毁timer
因为timer
通过target
强持有了self
,当我们需要pop
的时候,提前销毁timer
就可以打破这层循环引用,所以我们可以通过didMoveToParentViewController
,但是无论是pop还是push都会调用该方法,所以我们加一层判断,代码如下:
- (void)didMoveToParentViewController:(UIViewController *)parent{
// 无论push 进来 还是 pop 出去 正常跑
// 就算继续push 到下一层 pop 回去还是继续
if (parent == nil) {
[self.timer invalidate];
self.timer = nil;
NSLog(@"timer 走了");
}
}
此时当我们pop
的时候就可以正常销毁timer
了。
2.3 中介者模式
在这里我们关系的是fireHome
能执行,并不关心timer
捕获的target
是谁,所以为了避免循环引用,我们可以把target
换成其他对象,将fireHome
交给target
执行。所以修改代码为如下:
#import <objc/runtime.h>// 导入runtime
//* 定义一个id类型的对象属性 */
@property (nonatomic, strong) id target;
- (void)viewDidLoad {
[super viewDidLoad];
// 初始化target
self.target = [[NSObject alloc] init];
// 给NSObject添加方法
class_addMethod([NSObject class], @selector(fireHome), (IMP)fireHomeObjc, "v@:");
// 初始化timer
self.timer = [NSTimer scheduledTimerWithTimeInterval:1 target:self.target selector:@selector(fireHome) userInfo:nil repeats:YES];
}
void fireHomeObjc(id obj){
num++;
NSLog(@"hello word - %d",num);
}
- (void)dealloc{
[self.timer invalidate];
self.timer = nil;
NSLog(@"%s",__func__);
}
这里因为不在强引用self
,self
就可以正常dealloc
,也就可以停掉timer
。从而解除对target
的强引用。
2.4 自定义封装timer
上面的解决方式其实需要考虑的方面比较多,需要定义target
对象,添加方法,停掉和置空timer
,步骤还是蛮多的,稍不注意就可能出错,所以我们自己封装一个timer,作为中间层,来解决调用者这些复杂的操作,来使调用显得简单、方便、安全。
首先我们提供两个方法,分别是初始化方法和销毁timer
的方法,代码如下:
@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
然后我们提供了三个属性,分别用于存储target
、selector
以及自定义timer
中的timer
属性,代码如下:
#import <objc/message.h>
@interface LGTimerWapper()
// 定义一个target 用于存储传入的target 注意这里使用的是weak
@property (nonatomic, weak) id target;
// 存储 sel
@property (nonatomic, assign) SEL aSelector;
// timer
@property (nonatomic, strong) NSTimer *timer;
@end
下面是初始化方法的实现:
- 首先我们存储了
target
和aSelector
- 然后判断
target
能响应aSelector
的时候- 为中介添加方法,这里面的中介就是当前类
- 并把
imp
指向当前类的fireHomeWapper
方法 - 初始化timer
- return self
- (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; // vc
self.aSelector = aSelector; // 方法 -- vc 释放
if ([self.target respondsToSelector:self.aSelector]) {
// 将中介的处理添加到这里,不去外面再次添加,这里面的中介就是当前类型
// 通过Runtime 获取到方法
Method method = class_getInstanceMethod([self.target class], aSelector);
// 获取方法的type
const char *type = method_getTypeEncoding(method);
// 为当前类添加这个方法
class_addMethod([self class], aSelector, (IMP)fireHomeWapper, type);
// runloop&self -> timer -> lgtimerwarpper
self.timer = [NSTimer scheduledTimerWithTimeInterval:ti target:self selector:aSelector userInfo:userInfo repeats:yesOrNo];
}
}
return self;
}
下面我们看看fireHomeWapper
方法的实现,这里是重点也是难点:
- 首先判断
target
属性是否有值,因为这个属性是weak
的,如果有值说明能响应- 这里通过
objc_msgSend
来调用存储的aSelector
- 这里通过
- 如果不存在,说明不能响应了,停掉
timer
并置空就好了
关于lg_invalidate
方法的实现就更简单了,在本示例中没有用到该方法,但是如果想要主动销毁可以调用,代码如下:
- (void)lg_invalidate{
[self.timer invalidate];
self.timer = nil;
}
这样编写后调用的时候就非常简单了,减少了很多需要处理的地方:
#import "LGTimerWapper.h"
@property (nonatomic, strong) LGTimerWapper *timerWapper;
//* 定义一个id类型的对象属性 */
- (void)viewDidLoad {
[super viewDidLoad];
self.timerWapper = [[LGTimerWapper alloc] lg_initWithTimeInterval:1 target:self selector:@selector(fireHome) userInfo:nil repeats:YES];
}
2.5 使用NSProxy虚基类的子类
上面的代码虽然使用起来比较简单,但是代码写起来少多了些,有时候也存在维护问题,对于调用者没有真正的去调用invalidate
和置空timer
,总是有些别扭的,其实解决timer
循环引用的最好的方式还是使用NSProxy
。下面我们来看看怎么实现:
首先我们定义一个NSProxy
的子类,这个类里面通过一个weak
属性,持有着target
中需要强引用的实例对象。代码如下:
#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;
}
但是仅仅是这样还是不行的,还需要让实际的target
响应消息,毕竟LGProxy
不能真正响应timer
中的消息。
/*
仅仅添加了weak类型的属性还不够,为了保证中间件能够响应外部self的事件
需要通过消息转发机制,让实际的响应target还是外部self,
这一步至关重要,主要涉及到runtime的消息机制。
*/
-(id)forwardingTargetForSelector:(SEL)aSelector {
return self.object;
}
下面我们看看怎么使用:
#import "LGProxy.h"
@property (nonatomic, strong) LGProxy *proxy;
- (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;
NSLog(@"%s",__func__);
}
此时使用起来还是直接使用NSTimer
,只是对target
的强引用的修改成了Proxy
。
网友评论