iOS中解決NSTimer循環(huán)引用問題

??NSTimer使用不當(dāng)就會造成內(nèi)存泄漏,比如常見的使用方法:

//定義
@property (nonatomic, strong) NSTimer *timer;

//實(shí)現(xiàn)
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對象曼氛,所以形成循環(huán)引用喉前,dealloc 永遠(yuǎn)不會被執(zhí)行,timer 也永遠(yuǎn)不會被釋放轻专,造成內(nèi)存泄漏忆矛。

嘗試解決辦法:

1、把timer改成弱引用

@property (nonatomic, weak) NSTimer *timer;

??雖然selftimer是弱引用请垛,但是控制的delloc方法的執(zhí)行依賴于timerinvalidate催训,timerinvalidate又依賴于控制器的delloc方法,這是一個雞生蛋還是蛋生雞的問題宗收,依舊是循環(huán)引用漫拭;

2、使用__weak
??那換個思路能不能讓NSTimer弱引用target

 __weak typeof(self) weakSelf = self;
 self.timer = [NSTimer scheduledTimerWithTimeInterval:1 target:weakSelf selector:@selector(showMsg) userInfo:nil repeats:YES];

??weak關(guān)鍵字適用于block混稽,當(dāng)block引用了塊外的變量時采驻,會根據(jù)修飾變量的關(guān)鍵字來決定是強(qiáng)引用還是弱引用,如果變量使用weak關(guān)鍵字修飾匈勋,那block會對變量進(jìn)行弱引用礼旅,如果沒有__weak關(guān)鍵字,那就是強(qiáng)引用洽洁。
??但是NSTimerscheduledTimerWithTimeInterval:target方法內(nèi)部不會判斷修飾target的關(guān)鍵字痘系,所以這里傳selfweakSelf是沒區(qū)別的,其內(nèi)部會對target進(jìn)行強(qiáng)引用诡挂,還是會產(chǎn)生循環(huán)引用碎浇。

3、 選擇合適的時機(jī)手動釋放timer
采用下面的方法解決循環(huán)引用:

- (void) viewWillDisappear:(BOOL)animated {
    [super viewWillDisappear:animated];
    
    if (self.timer) {
        [self.timer invalidate];
        self.timer = nil;
    }
}

??在某些情況下璃俗,這種做法是可以解決問題的奴璃,但是有時卻會引起其他問題,比如控制器push到下一個控制器城豁,viewDidDisappear執(zhí)行后苟穆,timer被釋放,此時再pop回來唱星,timer已經(jīng)不復(fù)存在了雳旅。

所以,這種"方案"并不是合理的间聊。

??優(yōu)化上面的方法這個時候可以采用配對使用在 viewWillAppeartimer啟攒盈,在 viewWillDisappear 關(guān)閉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;
    }
}

上面的方法只是維護(hù)起來比較麻煩

最終解決辦法

1、自定義categoryblock解決

??網(wǎng)上有一些封裝的比較好的block的解決方案哎榴,思路無外乎是封裝一個NSTimercategory型豁,提供block形式的接口:

#import <Foundation/Foundation.h>

@interface NSTimer (TimerBlock)

/**
 分類解決NSTimer在使用時造成的循環(huán)引用的問題

 @param interval 間隔時間
 @param block    回調(diào)
 @param repeats  用于設(shè)置定時器是否重復(fù)觸發(fā)

 @return 返回NSTimer實(shí)體
 */
+ (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

??上述創(chuàng)建方式調(diào)用者是NSTImer自己僵蛛,只是NSTimer捕獲了參數(shù)block。這樣我們在使用timer時迎变,由于target的改變充尉,就不再有循環(huán)引用了。

__weak typeof(self) weakSelf = self;    //避免 block 強(qiáng)引用 self
self.timer = [NSTimer block_TimerWithTimeInterval:3 block:^{
//    [weakSelf dosomething];
} repeats:YES];

??iOS10中衣形,定時器的API新增了block方法驼侠,實(shí)現(xiàn)原理與此類似,這里采用分類為NSTimer添加了帶有block參數(shù)的方法谆吴,而系統(tǒng)是在原始類中直接添加方法倒源,最終的行為是一致的。

+ (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 強(qiáng)引用NSTimer突诬,NSTimer強(qiáng)引用 proxyproxy 弱引用 self芜繁,這樣通過弱引用來解決了相互引用旺隙,此時不會形成環(huán)。

打破環(huán)形引用

??定義一個繼承自NSObject的中間代理對象FFProxy骏令,ViewController不持有timer蔬捷,而是持有FFProxy實(shí)例,讓FFProxy實(shí)例來弱引用ViewController榔袋,timer強(qiáng)引用FFProxy實(shí)例周拐,直接看代碼:

//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類型的屬性還不夠,為了保證中間件能夠響應(yīng)外部self的事件凰兑,需要通過消息轉(zhuǎn)發(fā)機(jī)制妥粟,讓實(shí)際的響應(yīng)target還是外部self,這一步至關(guān)重要吏够,主要涉及到runtime的消息機(jī)制勾给。
-(id)forwardingTargetForSelector:(SEL)aSelector
{
    return self.target;
}
@end
//ViewController.m

FFProxy *proxy = [FFProxy proxyWithTarget:self];
//將timer的target設(shè)置為proxy,proxy又弱引用了控制器锅知,其實(shí)最終還是調(diào)用了控制器的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是什么?
??消息轉(zhuǎn)發(fā)售睹,簡單來說就是如果當(dāng)前對象沒有實(shí)現(xiàn)這個方法桩警,系統(tǒng)會到這個方法里來找實(shí)現(xiàn)對象。
??本文中由于當(dāng)前targetFFProxy昌妹,但是FFProxy沒有實(shí)現(xiàn)showMsg方法(當(dāng)然也不需要它實(shí)現(xiàn))捶枢,讓系統(tǒng)去找target實(shí)例的方法實(shí)現(xiàn)沉噩,也就是去找ViewController中的方法實(shí)現(xiàn)。

3柱蟀、使用NSProxy

使用iOSNSProxy類,NSProxy就是專門用來做消息轉(zhuǎn)發(fā)的蚜厉。

//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實(shí)例方法為alloc
    FFWeakProxy *proxy = [FFWeakProxy alloc];
    proxy.target = target;
    return proxy;
}

/**
 這個函數(shù)讓重載方有機(jī)會拋出一個函數(shù)的簽名长已,再由后面的forwardInvocation:去執(zhí)行
    為給定消息提供參數(shù)類型信息
 */
- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel {
    return [self.target methodSignatureForSelector:sel];
}

/**
 *  NSInvocation封裝了NSMethodSignature,通過invokeWithTarget方法將消息轉(zhuǎn)發(fā)給其他對象昼牛。這里轉(zhuǎn)發(fā)給控制器執(zhí)行术瓮。
 */
- (void)forwardInvocation:(NSInvocation *)invocation {
    [invocation invokeWithTarget:self.target];
}
@end

Controller里代碼如下:

- (void)viewDidLoad {
    [super viewDidLoad];
    // 這里的target又發(fā)生了變化
    self.timer = [NSTimer scheduledTimerWithTimeInterval:1 target:[FFWeakProxy proxyWithTarget:self] selector:@selector(showMsg) userInfo:nil repeats:YES];
}

//銷毀
-(void)dealloc
{
    [self.timer invalidate];
    self.timer = nil;
}
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市贰健,隨后出現(xiàn)的幾起案子胞四,更是在濱河造成了極大的恐慌,老刑警劉巖伶椿,帶你破解...
    沈念sama閱讀 216,591評論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件辜伟,死亡現(xiàn)場離奇詭異,居然都是意外死亡脊另,警方通過查閱死者的電腦和手機(jī)导狡,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,448評論 3 392
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來偎痛,“玉大人旱捧,你說我怎么就攤上這事〔嚷螅” “怎么了枚赡?”我有些...
    開封第一講書人閱讀 162,823評論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長谓谦。 經(jīng)常有香客問我贫橙,道長,這世上最難降的妖魔是什么反粥? 我笑而不...
    開封第一講書人閱讀 58,204評論 1 292
  • 正文 為了忘掉前任料皇,我火速辦了婚禮,結(jié)果婚禮上星压,老公的妹妹穿的比我還像新娘践剂。我一直安慰自己,他們只是感情好娜膘,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,228評論 6 388
  • 文/花漫 我一把揭開白布逊脯。 她就那樣靜靜地躺著,像睡著了一般竣贪。 火紅的嫁衣襯著肌膚如雪军洼。 梳的紋絲不亂的頭發(fā)上巩螃,一...
    開封第一講書人閱讀 51,190評論 1 299
  • 那天,我揣著相機(jī)與錄音匕争,去河邊找鬼避乏。 笑死,一個胖子當(dāng)著我的面吹牛甘桑,可吹牛的內(nèi)容都是我干的拍皮。 我是一名探鬼主播,決...
    沈念sama閱讀 40,078評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼跑杭,長吁一口氣:“原來是場噩夢啊……” “哼铆帽!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起德谅,我...
    開封第一講書人閱讀 38,923評論 0 274
  • 序言:老撾萬榮一對情侶失蹤爹橱,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后窄做,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體愧驱,經(jīng)...
    沈念sama閱讀 45,334評論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,550評論 2 333
  • 正文 我和宋清朗相戀三年椭盏,在試婚紗的時候發(fā)現(xiàn)自己被綠了冯键。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,727評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡庸汗,死狀恐怖惫确,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情蚯舱,我是刑警寧澤改化,帶...
    沈念sama閱讀 35,428評論 5 343
  • 正文 年R本政府宣布,位于F島的核電站枉昏,受9級特大地震影響陈肛,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜兄裂,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,022評論 3 326
  • 文/蒙蒙 一句旱、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧晰奖,春花似錦谈撒、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,672評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春溯乒,著一層夾襖步出監(jiān)牢的瞬間夹厌,已是汗流浹背寓娩。 一陣腳步聲響...
    開封第一講書人閱讀 32,826評論 1 269
  • 我被黑心中介騙來泰國打工挽拔, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人霜医。 一個月前我還...
    沈念sama閱讀 47,734評論 2 368
  • 正文 我出身青樓光稼,卻偏偏與公主長得像或南,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子钟哥,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,619評論 2 354

推薦閱讀更多精彩內(nèi)容