NSTimer 循環(huán)引用的原因和解決方案
造成循環(huán)引用的原因就是兩個對象之間因為強(qiáng)引用無法釋放。本文將通過NSTimer
來剖析強(qiáng)引用黄锤,以及解決方法搪缨。
1. 強(qiáng)引用
舉個例子,比如我們有兩個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__);
}
當(dāng)我們從B
界面pop
到A
時齿桃,timer
并不會停,那是為什么呢煮盼?顯然是沒有執(zhí)行B
界面的dealloc
方法短纵,導(dǎo)致B
界面沒有被釋放。
既然沒釋放肯定是有循環(huán)引用僵控,那么這個循環(huán)引用產(chǎn)生的在哪里呢香到?乍一看,我們的BViewController
強(qiáng)引用了timer
报破,那么如果說造成循環(huán)引用就是timer
強(qiáng)引用了self
悠就,但是這里面沒有block
怎么產(chǎn)生的循環(huán)引用呢?這里面在初始化timer
的時候有個target
充易,我們查看一下這個初始化方法shift+command+0
梗脾,搜索一下timerWithTimeInterval:target:selector:userInfo:repeats:
關(guān)于target
的描述如下:
可以看到timer
對target
保持強(qiáng)引用,直到timer
失效盹靴。
所以說循環(huán)引用就產(chǎn)生了炸茧,B
強(qiáng)引用著timer
,timer
強(qiáng)引用著target
也就是self
稿静,在這里self
就是B
的實例對象梭冠。此時就是:
self -> timer -> self
構(gòu)成的循環(huán)引用。
我們在iOS Objective-C Block簡介這篇文章中介紹了使用weakSelf
來解決循環(huán)引用改备,既然是這樣控漠,那么我們用weakSelf
是否可以解決這層循環(huán)引用呢?
將代碼修改為如下:
- (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];
}
運行悬钳,依舊沒有打破循環(huán)引用润脸,timer
在pop
后依舊運行柬脸。那么這是為什么呢?毙驯,在block
中我們可以使用weakSelf
來打破循環(huán)引用,那么在這里為什么不行呢灾测?
此時我們使用__weak
雖然打破了self -> timer -> self
這個循環(huán)引用爆价,使其變成了self -> timer -> weakSelf -> self
。
但是這里我們分析的并不全面媳搪,因為我們的timer
需要加入到Runloop
铭段,Runloop
對timer
是一個強(qiáng)持有,Runloop
的生命周期比B
界面更長秦爆,所以這才是導(dǎo)致timer
無法釋放的真正原因序愚,timer
無法釋放,自然self
也就無法釋放等限。所以這個引用鏈最初應(yīng)該是這樣的:
self -> timer -> self
runloop -> timer -> self
畫個圖:
加上weakSelf
之后爸吮,變成了這樣:
self -> timer -> weakSelf -> self
runloop -> timer -> weakSelf -> self
那么雖然是這樣weakSelf
也是弱引用啊,為什么不能打破循環(huán)引用呢望门?在block
中我們可以通過self -> block -> weakSelf -> self
打破循環(huán)引用形娇?為什么這里就不可以了呢?
這里我們就要稍微研究一下這行代碼了:
__weak typeof(self) weakSelf = self;
我們想知道weakSelf
和self
有什么區(qū)別筹误,其實主要是這三點:
-
weakSelf
會對self
的引用計數(shù)+1
嗎桐早? -
weakSelf
和self
的指針地址相同嗎? -
weakSelf
和self
是指向同一片內(nèi)存空間嗎厨剪?
下面我們驗證一下哄酝,添加這樣一段代碼:
NSLog(@"%ld",CFGetRetainCount((__bridge CFTypeRef)self));
__weak typeof(self) weakSelf = self;
NSLog(@"%ld",CFGetRetainCount((__bridge CFTypeRef)self));
運行并通過lldb
調(diào)試得到如下結(jié)果:
我們可以看到:
-
weakSelf
并沒有增加self
的引用計數(shù) -
weakSelf
和self
指向同一內(nèi)存區(qū)域 -
weakSelf
和self
的指針地址是不同的
其實分析完這里我們也看不出什么,這里的引用關(guān)系還是這幅圖:
下面我們在看看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();
}
調(diào)用test1
陶衅,通過lldb
調(diào)試:
此時就很清晰了,block
中的weakSelf
與外面的weakSelf
根本不是同一個對象钾唬,雖然他們指向的都是同一片內(nèi)存區(qū)域万哪,在這里就是<LGTimerViewController: 0x7fc275604b10>
,下面我們在看看libclosure
中的_Block_object_assign
函數(shù)抡秆。
在這里我們看到都是取的對象的地址**
奕巍,或者是通過_Block_copy
拷貝一份,也就是說在block
中都是臨時變量儒士,一份新的變量的止,所以說在block
中其引用鏈并不存在對weakSelf
持有,而是持有的weakSelf
的指針地址着撩,也就是*weakSelf
诅福,跟self
沒有任何關(guān)系匾委。
然而在timer
這里,timer
對weakSelf
也就是target
是強(qiáng)持有氓润,所以不能打破循環(huán)引用赂乐。
所以對于block
和timer
兩個模型之間循環(huán)引用的區(qū)別如下:
timer:self -> timer -> weakSelf -> self
block:self -> block -> *weakSelf
2. 解決Timer強(qiáng)引用
2.1 不使用帶target的Timer
因為timer
通過target
強(qiáng)持有了self
,那么我們不使用含有target
的API不就就可以了咖气,修改代碼為如下:
self.timer = [NSTimer scheduledTimerWithTimeInterval:1 repeats:YES block:^(NSTimer * _Nonnull timer) {
NSLog(@"hello word - %d",num);
}];
2.2 提前銷毀timer
因為timer
通過target
強(qiáng)持有了self
挨措,當(dāng)我們需要pop
的時候,提前銷毀timer
就可以打破這層循環(huán)引用崩溪,所以我們可以通過didMoveToParentViewController
浅役,但是無論是pop還是push都會調(diào)用該方法,所以我們加一層判斷伶唯,代碼如下:
- (void)didMoveToParentViewController:(UIViewController *)parent{
// 無論push 進(jìn)來 還是 pop 出去 正常跑
// 就算繼續(xù)push 到下一層 pop 回去還是繼續(xù)
if (parent == nil) {
[self.timer invalidate];
self.timer = nil;
NSLog(@"timer 走了");
}
}
此時當(dāng)我們pop
的時候就可以正常銷毀timer
了觉既。
2.3 中介者模式
在這里我們關(guān)系的是fireHome
能執(zhí)行,并不關(guān)心timer
捕獲的target
是誰乳幸,所以為了避免循環(huán)引用瞪讼,我們可以把target
換成其他對象,將fireHome
交給target
執(zhí)行反惕。所以修改代碼為如下:
#import <objc/runtime.h>// 導(dǎo)入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__);
}
這里因為不在強(qiáng)引用self
尝艘,self
就可以正常dealloc
,也就可以停掉timer
姿染。從而解除對target
的強(qiáng)引用背亥。
2.4 自定義封裝timer
上面的解決方式其實需要考慮的方面比較多,需要定義target
對象悬赏,添加方法狡汉,停掉和置空timer
,步驟還是蠻多的闽颇,稍不注意就可能出錯盾戴,所以我們自己封裝一個timer,作為中間層兵多,來解決調(diào)用者這些復(fù)雜的操作尖啡,來使調(diào)用顯得簡單、方便、安全。
首先我們提供兩個方法勘伺,分別是初始化方法和銷毀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
下面是初始化方法的實現(xiàn):
- 首先我們存儲了
target
和aSelector
- 然后判斷
target
能響應(yīng)aSelector
的時候- 為中介添加方法奠涌,這里面的中介就是當(dāng)前類
- 并把
imp
指向當(dāng)前類的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]) {
// 將中介的處理添加到這里宪巨,不去外面再次添加,這里面的中介就是當(dāng)前類型
// 通過Runtime 獲取到方法
Method method = class_getInstanceMethod([self.target class], aSelector);
// 獲取方法的type
const char *type = method_getTypeEncoding(method);
// 為當(dāng)前類添加這個方法
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
方法的實現(xiàn)溜畅,這里是重點也是難點:
- 首先判斷
target
屬性是否有值捏卓,因為這個屬性是weak
的,如果有值說明能響應(yīng)- 這里通過
objc_msgSend
來調(diào)用存儲的aSelector
- 這里通過
- 如果不存在慈格,說明不能響應(yīng)了天吓,停掉
timer
并置空就好了
關(guān)于lg_invalidate
方法的實現(xiàn)就更簡單了,在本示例中沒有用到該方法峦椰,但是如果想要主動銷毀可以調(diào)用,代碼如下:
- (void)lg_invalidate{
[self.timer invalidate];
self.timer = nil;
}
這樣編寫后調(diào)用的時候就非常簡單了汰规,減少了很多需要處理的地方:
#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虛基類的子類
上面的代碼雖然使用起來比較簡單汤功,但是代碼寫起來少多了些,有時候也存在維護(hù)問題溜哮,對于調(diào)用者沒有真正的去調(diào)用invalidate
和置空timer
滔金,總是有些別扭的,其實解決timer
循環(huán)引用的最好的方式還是使用NSProxy
茂嗓。下面我們來看看怎么實現(xiàn):
首先我們定義一個NSProxy
的子類餐茵,這個類里面通過一個weak
屬性,持有著target
中需要強(qiáng)引用的實例對象述吸。代碼如下:
#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
響應(yīng)消息,畢竟LGProxy
不能真正響應(yīng)timer
中的消息蝌矛。
/*
僅僅添加了weak類型的屬性還不夠道批,為了保證中間件能夠響應(yīng)外部self的事件
需要通過消息轉(zhuǎn)發(fā)機(jī)制,讓實際的響應(yīng)target還是外部self入撒,
這一步至關(guān)重要隆豹,主要涉及到runtime的消息機(jī)制。
*/
-(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
的強(qiáng)引用的修改成了Proxy
璃赡。