NSTimer產(chǎn)生循環(huán)引用的原因
我們首先看下NSTimer的初始化方法
+ (NSTimer *)timerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo;
+ (NSTimer *)timerWithTimeInterval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block API_AVAILABLE(macosx(10.12), ios(10.0), watchos(3.0), tvos(10.0));
+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo;
+ (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));
timerWithTimeInterval
創(chuàng)建出來的timer無法立刻使用,需要添加到NSRunloop中才可以正常工作
「After creating it肪笋, you must add the timer to a run loop manually by calling the addTimer:forMode: method of the corresponding NSRunLoop object图张●常」
scheduledTimerWithTimeInterval
創(chuàng)建出來的runloop已經(jīng)被添加到當前線程的currentRunloop
中來了崔梗。
「Schedules it on the current run loop in the default mode佛嬉÷甙模」
NSTimer與runloop的關(guān)系暫時不在本文詳談。我們先關(guān)注下timer為何會容易產(chǎn)生循環(huán)引用暖呕。 NSTimer會強引用target斜做,等到自身'失效'時再釋放此對象。
我們先假設(shè)開發(fā)中一個最常見的場景
#import "ViewController.h"
@interface ViewController ()
@property (nonatomic, strong) NSTimer *timer;
@end
@implementation ViewController
- (void)viewDidLoad
{
self.timer = [NSTimer timerWithTimeInterval:1.0 target:self selector:@selector(doSomething) userInfo:nil repeats:YES];
[[NSRunLoop mainRunLoop] addTimer:self.timer forMode:NSDefaultRunLoopMode];
}
- (void)doSomething
{
NSLog(@"%s", __func__);
}
- (void)dealloc
{
[self.timer invalidate];
self.timer = nil;
}
控制器中有一個timer屬性湾揽,且timer的target是該控制器瓤逼。如圖兩者之間會形成一個retain cycle。
在不主動釋放timer的前提下库物,那么控制器會一直強引用著timer霸旗,timer內(nèi)部的target也強引用著控制器,控制器的引用計數(shù)永遠不會為0戚揭。這種內(nèi)存泄漏問題尤其嚴重诱告,因為timer還在不斷的執(zhí)行著輪詢?nèi)蝿?wù),很容易導(dǎo)致其它的內(nèi)存泄漏問題民晒。
幾種不太好的解決方案
1.在dealloc中對timer進行釋放
很多人都會在控制器的dealloc
方法中寫如下代碼
- (void)dealloc
{
[self.timer invalidate];
self.timer = nil;
}
認為在控制器銷毀的時候順便銷毀timer
精居,這樣一來就萬無一失了,殊不知因為循環(huán)引用dealloc方法根本沒有執(zhí)行镀虐。
2.在- (void)viewDidDisappear:(BOOL)animated
中對timer進行釋放
這種方式在平時開發(fā)中是比較是比較常見的且有效的箱蟆,但是我認為有幾點不好
- 如果當前控制器是在導(dǎo)航控制器的棧中,那么無論push/pop都會調(diào)用- (void)viewDidDisappear:(BOOL)animated 需要在該方法中判斷
- (void)viewDidDisappear:(BOOL)animated
{
if (self.navigationController == nil) {
[self.timer invalidate];
self.timer = nil;
}
}
- 如果當前類的跳轉(zhuǎn)方式是modal呢刮便?或者說當前類并不是ViewController的子類,那么該如何判斷呢空猜?
最好的辦法是讓timer跟當前類的生命周期綁定在一起,自動化的進行釋放恨旱,減少非必要的代碼書寫辈毯。
3.使用weakSelf
很多人回想如果把傳入target的引用改為弱引用,這樣一來引用線在timer指向當前類就斷掉了搜贤,引用換就無法形成閉環(huán),那么就不會形成循環(huán)引用了谆沃。
__weak typeof(self) weakSelf = self;
self.timer = [NSTimer timerWithTimeInterval:1.0 target:weakSelf selector:@selector(doSomething) userInfo:nil repeats:YES];
其實這是一個非常容易出錯的想法,傳參跟使用block是兩個完全不同的概念!!
weakSelf最多使用的場景是在block內(nèi)部中使用仪芒,block內(nèi)部的機制會根據(jù)捕獲的對象變量的指針類型(__weak, __strong)進行強引用或弱引用.
但是參數(shù)傳遞的本質(zhì)是將參數(shù)的地址傳過去唁影。無論是self或者是weakSelf耕陷,本質(zhì)都是一個地址。所以該方法無效据沈。
4.不使用屬性
你可能覺得不使用屬性或成員變量可切斷當前類對timer的強引用哟沫,但是當前類仍然會一直在內(nèi)存中。原因如圖主線程的runloop在程序運行期間是不會銷毀的锌介, runloop引用著timer,timer就不會自動銷毀嗜诀。timer引用著target,target也不會銷毀。
解決方案
1.使用中間代理方法
既然循環(huán)引用的原因是因為timer和控制器之間的強引用,那么是否可以使用一個中間代理得接觸這個閉環(huán)呢孔祸?答案是可以的隆敢。整體構(gòu)思如下圖
可以在timer與控制器之間使用一個proxy來解除兩者之間的相互強引用。
首先聲明一個.h文件
#import <Foundation/Foundation.h>
@interface LLTimerProxy1 : NSObject
+ (instancetype)proxyWithTarget:(id)target withSelector:(SEL)selector;
- (void)__execute;
@end
實現(xiàn).m文件
#import "LLTimerProxy1.h"
@interface LLTimerProxy1()
/** target */
@property (nonatomic, weak) id target;
/** SEL */
@property (nonatomic, assign) SEL selector;
@end
@implementation LLTimerProxy1
+ (instancetype)proxyWithTarget:(id)target withSelector:(SEL)selector
{
LLTimerProxy1 *proxy = [[LLTimerProxy1 alloc] init];
proxy.target = target;
proxy.selector = selector;
return proxy;
}
- (void)__execute
{
if (_target && _selector) {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
[_target performSelector:_selector withObject:nil];
#pragma clang diagnostic pop
}
}
@end
使用方式
- (void)viewDidLoad
{
LLTimerProxy1 *proxy = [LLTimerProxy1 proxyWithTarget:self withSelector:@selector(doSomething)];
self.timer = [NSTimer timerWithTimeInterval:1.0 target:proxy selector:@selector(__execute) userInfo:nil repeats:YES];
[[NSRunLoop mainRunLoop] addTimer:self.timer forMode:NSDefaultRunLoopMode];
}
- (void)doSomething
{
NSLog(@"%s", __func__);
}
- (void)dealloc
{
[self.timer invalidate];
self.timer = nil;
NSLog(@"%s", __func__);
}
首先梳理一下引用流程
NSTimer target -> 強引用著proxy
proxy的target -> 弱引用著控制器
這樣一來當控制器的引用計數(shù)為0的時候崔慧,會調(diào)用dealloc方法拂蝎,在dealloc方法中對timer進行釋放,timer釋放的時候也會對proxy進行釋放尊浪。這樣一來就可以讓timer的聲明周期與控制器同步了
補充
- 在proxy的
__execute
方法中匣屡,我做了一個if判斷,是因為有可能在target的dealloc方法中并沒有對timer進行釋放拇涤。這樣就會導(dǎo)致timer仍然runloop中運行捣作,不斷的調(diào)用__execute
方法.此時的target因為釋放了,所以target為nil鹅士。像空指針發(fā)送消息并不會引起崩潰券躁,但是最好還是在該方法里添加一個判斷target是否為空的if語句來告訴開發(fā)人員某個類已經(jīng)釋放掉了,但是該類的timer沒有被釋放。
2.使用NSProxy
NSProxy是一個基類掉盅,是蘋果創(chuàng)建出來專門做代理轉(zhuǎn)發(fā)事件的基類也拜,負責(zé)將消息轉(zhuǎn)發(fā)到真正target的類
An abstract superclass defining an API for objects that act as stand-ins for other objects or for objects that don’t exist yet.
該類有兩個方法,有runtime儲備的同學(xué)應(yīng)該會對這兩個方法比較熟悉
- (void)forwardInvocation:(NSInvocation *)invocation;
- (nullable NSMethodSignature *)methodSignatureForSelector:(SEL)sel
NSProxy收到消息之后會在自己的方法列表中查找,如果沒有則直接會進入消息轉(zhuǎn)發(fā)趾痘。比NSObject類少了在父類的方法列表和動態(tài)解析的步驟慢哈,性能會更好。
因為Proxy可以實現(xiàn)消息轉(zhuǎn)發(fā)永票,那么本身也不用持有選擇子卵贱,這樣代碼會寫的會更明確。
.h文件
#import <Foundation/Foundation.h>
@interface LLTimerProxy : NSProxy
+ (instancetype)proxyWithTarget:(id)target;
@end
.m實現(xiàn)文件
#import "LLTimerProxy.h"
@interface LLTimerProxy ()
/** tatget */
@property (nonatomic, weak) id target;
@end
@implementation LLTimerProxy
+ (instancetype)proxyWithTarget:(id)target
{
LLTimerProxy *proxy = [LLTimerProxy alloc];
proxy.target = target;
return proxy;
}
- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel
{
if (!self.target || ![self.target respondsToSelector:sel]) {
return [NSMethodSignature signatureWithObjCTypes:"v:@"];
}
return [self.target methodSignatureForSelector:sel];
}
- (void)forwardInvocation:(NSInvocation *)invocation
{
if (!self.target) {
NSLog(@"target已經(jīng)從內(nèi)存中死掉了");
return;
}
[invocation invokeWithTarget:self.target];
}
@end
使用方式
- (void)viewDidLoad
{
LLTimerProxy *proxy = [LLTimerProxy proxyWithTarget:self];
self.timer = [NSTimer timerWithTimeInterval:1.0 target:proxy selector:@selector(doSomething) userInfo:nil repeats:YES];
[[NSRunLoop mainRunLoop] addTimer:self.timer forMode:NSDefaultRunLoopMode];
}
- (void)doSomething
{
NSLog(@"%s", __func__);
}
- (void)dealloc
{
[self.timer invalidate];
self.timer = nil;
NSLog(@"%s", __func__);
}
可以發(fā)現(xiàn)Proxy類并沒有引用著selector侣集,因為proxy類并沒有doSomething
方法键俱,所有直接進入了消息轉(zhuǎn)發(fā)步驟.在消息轉(zhuǎn)發(fā)中將消息接受者
轉(zhuǎn)發(fā)給自身持有的target,這樣就可以完成調(diào)用了。
補充
- 使用繼承NSProxy的類的優(yōu)勢是在代碼書寫上比使用繼承自NSObject的類更加直觀世分,因為NSTimer的選擇子可以直接填寫本類的方法,而不用寫
__execute
方法 - 但是使用NSProxy會‘更’容易造成崩潰编振,當然這種崩潰的原因是因為開發(fā)者沒有規(guī)范的處理timer的聲明周期。設(shè)想一種這樣的場景臭埋,類已經(jīng)釋放掉了踪央,但是timer仍然不斷的調(diào)用方法臀玄,那么在
methodSignatureForSelector
方法中,[self.target methodSignatureForSelector:sel];
因為self.target已經(jīng)是nil了,就會導(dǎo)致return nil.methodSignatureForSelector
方法中返回為空代表消息轉(zhuǎn)發(fā)失敗,會導(dǎo)致[NSProxy doesNotRecognizeSelector:doSomething
崩潰.當然崩潰不一定是壞事杯瞻,容錯性是雙刃劍镐牺。有時候別人犯錯了就應(yīng)該讓它崩潰炫掐,讓別人發(fā)現(xiàn)問題魁莉,必須去解決。如果不提醒它募胃,那這個問題就越來越嚴重旗唁,比如內(nèi)存泄露問題。各位在開發(fā)中靈活選擇使用痹束。
3.使用block+weakSelf
NSTimer在iOS10開放了兩個API
/// Creates and returns a new NSTimer object initialized with the specified block object. This timer needs to be scheduled on a run loop (via -[NSRunLoop addTimer:]) before it will fire.
/// - parameter: timeInterval The number of seconds between firings of the timer. If seconds is less than or equal to 0.0, this method chooses the nonnegative value of 0.1 milliseconds instead
/// - parameter: repeats If YES, the timer will repeatedly reschedule itself until invalidated. If NO, the timer will be invalidated after it fires.
/// - parameter: block The execution body of the timer; the timer itself is passed as the parameter to this block when executed to aid in avoiding cyclical references
+ (NSTimer *)timerWithTimeInterval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block API_AVAILABLE(macosx(10.12), ios(10.0), watchos(3.0), tvos(10.0));
/// Creates and returns a new NSTimer object initialized with the specified block object and schedules it on the current run loop in the default mode.
/// - parameter: ti The number of seconds between firings of the timer. If seconds is less than or equal to 0.0, this method chooses the nonnegative value of 0.1 milliseconds instead
/// - parameter: repeats If YES, the timer will repeatedly reschedule itself until invalidated. If NO, the timer will be invalidated after it fires.
/// - parameter: block The execution body of the timer; the timer itself is passed as the parameter to this block when executed to aid in avoiding cyclical references
+ (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));
使用方式
- (void)viewDidLoad
{
__weak typeof(self) weakSelf = self;
self.timer = [NSTimer scheduledTimerWithTimeInterval:1 repeats:YES block:^(NSTimer * _Nonnull timer) {
[weakSelf doSomething];
}];
}
- (void)doSomething
{
NSLog(@"%s", __func__);
}
該方法比較簡單检疫,就不多贅述了。