前言
我們?cè)陂_發(fā)中玛臂,經(jīng)常會(huì)用到NSTimer
這個(gè)類的+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo
方法橡疼,但是NSTimer會(huì)對(duì)傳入的target
對(duì)象進(jìn)行強(qiáng)引用,如果target
又對(duì)timer
進(jìn)行了強(qiáng)引用矮男,那么就會(huì)出現(xiàn)循環(huán)引用,今天我們就研究一下如何解決這個(gè)問題桐玻。
例如:
@interface ViewController ()
@property (strong, nonatomic) NSTimer *timer;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0f target:self selector:@selector(timerTest) userInfo:nil repeats:YES];
}
- (void)timerTest {
NSLog(@"%s", __func__);
}
- (void)dealloc {
NSLog(@"%s", __func__);
//類銷毀的時(shí)候調(diào)用`timer`的`invalidate`方法進(jìn)行停止
[_timer invalidate];
}
@end
當(dāng)控制器pop
出去的時(shí)候简识,會(huì)執(zhí)行dealloc
方法,看上去好像沒什么問題忿危,但是我們實(shí)際的運(yùn)行結(jié)果是這樣的:
2019-08-14 20:26:39.636581+0800 定時(shí)器-01[16487:12091593] -[ViewController timerTest]
2019-08-14 20:26:40.636660+0800 定時(shí)器-01[16487:12091593] -[ViewController timerTest]
2019-08-14 20:26:41.636513+0800 定時(shí)器-01[16487:12091593] -[ViewController timerTest]
2019-08-14 20:26:42.636270+0800 定時(shí)器-01[16487:12091593] -[ViewController timerTest]
當(dāng)我們的控制器pop
消失以后控制臺(tái)還在一直打印timerTest
的方法达箍,dealloc
方法也沒有執(zhí)行。這是為什么铺厨?原因就是控制器
對(duì)timer
進(jìn)行了強(qiáng)引用缎玫,而timer
又會(huì)對(duì)傳入的target
也就是控制器
進(jìn)行了強(qiáng)引用,這就造成了循環(huán)引用解滓。
這時(shí)候我們就會(huì)想碘梢,將self
通過__weak
修飾一下不就可以了,那么我們來試一下伐蒂,代碼經(jīng)過改造后如下:
@interface ViewController ()
@property (strong, nonatomic) NSTimer *timer;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
__weak typeof(self) weakSelf = self;
self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0f target:weakSelf selector:@selector(timerTest) userInfo:nil repeats:YES];
}
- (void)timerTest {
NSLog(@"%s", __func__);
}
- (void)dealloc {
NSLog(@"%s", __func__);
//類銷毀的時(shí)候調(diào)用`timer`的`invalidate`方法進(jìn)行停止
[_timer invalidate];
}
@end
這時(shí)候我們?cè)俅芜\(yùn)行代碼煞躬,結(jié)果還是一樣的,控制器消失了,但是控制臺(tái)還在一直打印timerTest
方法恩沛,這是為什么呢在扰?
其實(shí)原因還是上面所說的,timer
對(duì)傳入的terget
也就是self
進(jìn)行了強(qiáng)引用雷客,雖然我們?cè)谕獠總魅氲氖且粋€(gè)弱引用的weakSelf
芒珠,但是timer
內(nèi)部還是有一個(gè)強(qiáng)指針指向了self
的內(nèi)存地址。
那么我們應(yīng)該怎么解決這個(gè)問題呢搅裙?在這里我提供了三種解決方式:
-
方案1:在
- (void)viewWillDisappear:(BOOL)animated
方法中調(diào)執(zhí)行[_timer invalidate]
方法
改造后的代碼如下:
@interface ViewController ()
@property (strong, nonatomic) NSTimer *timer;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0f target:self selector:@selector(timerTest) userInfo:nil repeats:YES];
}
- (void)timerTest {
NSLog(@"%s", __func__);
}
- (void)viewWillDisappear:(BOOL)animated {
[super viewWillDisappear:animated];
//在此處調(diào)用皱卓,可以避免循環(huán)引用導(dǎo)致的內(nèi)存問題
[_timer invalidate];
}
- (void)dealloc {
NSLog(@"%s", __func__);
}
@end
運(yùn)行結(jié)果如下:
2019-08-14 20:54:36.662354+0800 定時(shí)器-01[16508:12095299] -[ViewController timerTest]
2019-08-14 20:54:37.662384+0800 定時(shí)器-01[16508:12095299] -[ViewController timerTest]
2019-08-14 20:54:38.662300+0800 定時(shí)器-01[16508:12095299] -[ViewController timerTest]
2019-08-14 20:54:39.662305+0800 定時(shí)器-01[16508:12095299] -[ViewController timerTest]
2019-08-14 20:54:40.210602+0800 定時(shí)器-01[16508:12095299] -[ViewController dealloc]
我們看到,當(dāng) 控制器
消失的時(shí)候部逮,執(zhí)行了dealloc
方法娜汁,為什么呢?原因是在[_timer invalidate]
執(zhí)行以后兄朋,釋放了對(duì)target
也就是self
的強(qiáng)引用掐禁,循環(huán)引用被破壞,因此控制器
可以被釋放颅和。
這種方式僅限于控制器中傅事,當(dāng)我們的傳入的target
不是一個(gè)控制器,而是一個(gè)普通的繼承自NSObject
的對(duì)象的時(shí)候峡扩,就不能用這種方式蹭越,因此我們來看下面的方法
-
方案2:使用
+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block
方法
改造后的代碼如下:
@interface ViewController ()
@property (strong, nonatomic) NSTimer *timer;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
__weak typeof(self) weakSelf = self;
if (@available(iOS 10.0, *)) {
self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0f repeats:YES block:^(NSTimer * _Nonnull timer) {
[weakSelf timerTest];
}];
} else {
// Fallback on earlier versions
}
}
- (void)timerTest {
NSLog(@"%s", __func__);
}
- (void)dealloc {
NSLog(@"%s", __func__);
//類銷毀的時(shí)候調(diào)用`timer`的`invalidate`方法進(jìn)行停止
[_timer invalidate];
}
執(zhí)行結(jié)果如下:
2019-08-14 20:44:26.588273+0800 定時(shí)器-01[16505:12094230] -[ViewController timerTest]
2019-08-14 20:44:27.588241+0800 定時(shí)器-01[16505:12094230] -[ViewController timerTest]
2019-08-14 20:44:28.588314+0800 定時(shí)器-01[16505:12094230] -[ViewController timerTest]
2019-08-14 20:44:29.588270+0800 定時(shí)器-01[16505:12094230] -[ViewController timerTest]
2019-08-14 20:44:29.611601+0800 定時(shí)器-01[16505:12094230] -[ViewController dealloc]
這時(shí)候,當(dāng)控制器
消失的時(shí)候教届,執(zhí)行了dealloc
方法般又,原因是雖然timer
對(duì)block
是強(qiáng)引用,但是block
內(nèi)部對(duì)于self
是弱引用的巍佑,這個(gè)時(shí)候茴迁,破壞了循環(huán)引用,控制器
沒有被timer
強(qiáng)引用萤衰,所以可以被釋放堕义。但是我們發(fā)現(xiàn),這個(gè)方法只有在iOS10
以后才有的脆栋,所以倦卖,如果我們的app
的兼容版本是iOS 10
以后的,那么我們可以這么寫椿争,如果要兼容更低版本怕膛,那么我們只能載想其他的辦法。
-
方案3:使用
代理對(duì)象
首先我們創(chuàng)建一個(gè)代理對(duì)象
秦踪,代碼如下:
#import <Foundation/Foundation.h>
@interface TimerProxy : NSObject
@property (weak, nonatomic) id target;
+ (instancetype)proxyWithTarget:(id)target;
@end
#import "TimerProxy.h"
@implementation TimerProxy
+ (instancetype)proxyWithTarget:(id)target {
TimerProxy *proxy = [[TimerProxy alloc] init];
proxy.target = target;
return proxy;
}
//利用消息轉(zhuǎn)發(fā)機(jī)制褐捻,將方法轉(zhuǎn)發(fā)給target處理
- (id)forwardingTargetForSelector:(SEL)aSelector {
return self.target;
}
@end
然后改造原有的代碼:
#import "TimerProxy.h"
@interface ViewController ()
@property (strong, nonatomic) NSTimer *timer;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
//此時(shí)掸茅,傳入的target就不是self,而是TimerProxy對(duì)象
self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0f target:[TimerProxy proxyWithTarget:self] selector:@selector(timerTest) userInfo:nil repeats:YES];
}
- (void)timerTest {
NSLog(@"%s", __func__);
}
- (void)dealloc {
NSLog(@"%s", __func__);
//類銷毀的時(shí)候調(diào)用`timer`的`invalidate`方法進(jìn)行停止
[_timer invalidate];
}
執(zhí)行結(jié)果如下:
2019-08-14 21:17:47.476647+0800 定時(shí)器-01[16527:12097738] -[ViewController timerTest]
2019-08-14 21:17:48.475937+0800 定時(shí)器-01[16527:12097738] -[ViewController timerTest]
2019-08-14 21:17:49.476636+0800 定時(shí)器-01[16527:12097738] -[ViewController timerTest]
2019-08-14 21:17:50.476643+0800 定時(shí)器-01[16527:12097738] -[ViewController timerTest]
2019-08-14 21:17:51.120013+0800 定時(shí)器-01[16527:12097738] -[ViewController dealloc]
這種方案柠逞,我們采用的是代理對(duì)象
的方式昧狮,控制器
對(duì)于timer
是強(qiáng)引用,timer
對(duì)于代理對(duì)象
是強(qiáng)引用板壮,而代理對(duì)象
對(duì)于控制器
是弱引用逗鸣,這樣我們就破壞了循環(huán)引用,最終調(diào)用了dealloc
方绰精,從而解決問題撒璧。
-
方案4:使用
NSProxy
(推薦方案)
首先,我們自定義一個(gè)繼承自NSProxy
的類
#import <Foundation/Foundation.h>
@interface TimerProxy2 : NSProxy
@property (weak, nonatomic) id target;
+ (instancetype)proxyWithTarget:(id)target;
@end
#import "TimerProxy2.h"
@implementation TimerProxy2
+ (instancetype)proxyWithTarget:(id)target {
// 這句代碼會(huì)報(bào)錯(cuò)笨使,因?yàn)镹SProxy無init方法卿樱,直接alloc即可
// TimerProxy2 *proxy = [[TimerProxy2 alloc] init];
TimerProxy2 *proxy = [TimerProxy2 alloc];
proxy.target = target;
return proxy;
}
- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel {
return [self.target methodSignatureForSelector:sel];
}
//消息轉(zhuǎn)發(fā)
- (void)forwardInvocation:(NSInvocation *)invocation {
invocation.target = self.target;
[invocation invokeWithTarget:self.target];
}
@end
改造原有代碼
#import "TimerProxy2.h"
@interface ViewController ()
@property (strong, nonatomic) NSTimer *timer;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
//此時(shí),傳入的target就不是self阱表,而是TimerProxy對(duì)象
self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0f target:[TimerProxy2 proxyWithTarget:self] selector:@selector(timerTest) userInfo:nil repeats:YES];
}
- (void)timerTest {
NSLog(@"%s", __func__);
}
- (void)dealloc {
NSLog(@"%s", __func__);
//類銷毀的時(shí)候調(diào)用`timer`的`invalidate`方法進(jìn)行停止
[_timer invalidate];
}
執(zhí)行結(jié)果如下:
2019-08-14 21:17:46.476786+0800 定時(shí)器-01[16527:12097738] -[ViewController timerTest]
2019-08-14 21:17:47.476647+0800 定時(shí)器-01[16527:12097738] -[ViewController timerTest]
2019-08-14 21:17:48.475937+0800 定時(shí)器-01[16527:12097738] -[ViewController timerTest]
2019-08-14 21:17:49.476636+0800 定時(shí)器-01[16527:12097738] -[ViewController timerTest]
2019-08-14 21:17:50.476643+0800 定時(shí)器-01[16527:12097738] -[ViewController timerTest]
2019-08-14 21:17:51.120013+0800 定時(shí)器-01[16527:12097738] -[ViewController dealloc]
這種方案也可以解決循環(huán)引用的問題殿如,但是大家可能覺得贡珊,這種寫法相比方案3更麻煩一些最爬,代碼寫的要更多,但是為什么更推薦這種實(shí)現(xiàn)方式呢门岔?原因還要從oc
方法調(diào)用分析爱致。
oc
的方法調(diào)用最終會(huì)轉(zhuǎn)換為objc_msgSend
函數(shù)進(jìn)行調(diào)用,objc_msgSend
的執(zhí)行流程分為三大階段
- 消息發(fā)送
- 動(dòng)態(tài)解析
- 消息轉(zhuǎn)發(fā)
其中消息發(fā)送
階段寒随,就會(huì)進(jìn)行多次方法查找糠悯。首先從自身的方法緩存中進(jìn)行查找,找不到會(huì)在本類的方法列表進(jìn)行查找妻往,找不到再去父類的緩存中進(jìn)行查找互艾,找不到再去父類的方法列表中進(jìn)行查找...一直超找到根類,如果還沒有找到的話讯泣,就會(huì)進(jìn)行動(dòng)態(tài)方法解析
纫普,沒有實(shí)現(xiàn)動(dòng)態(tài)方法解析
的話,最后就會(huì)進(jìn)入消息轉(zhuǎn)發(fā)
階段好渠。
而使用NSProxy
會(huì)先從自身方法列表進(jìn)行查找昨稼,找不到的話,會(huì)直接進(jìn)入消息轉(zhuǎn)發(fā)
階段拳锚,省去了去父類一層層查找和動(dòng)態(tài)方法解析
這些操作假栓,因此效率更高。
自此霍掺,我們關(guān)于NSTimer
引起的循環(huán)引用問題就算是徹底解決了匾荆,其實(shí)方法4也是給我們提供了一種思路拌蜘,如果以后開發(fā)中遇到了類似于NSTimer
這種的循環(huán)引用問題,我們完全可以通過代理對(duì)象
這種方式來解決這種問題棋凳。