NSTimer循環(huán)引用的問題

前言

我們?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ì)象這種方式來解決這種問題棋凳。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末拦坠,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子剩岳,更是在濱河造成了極大的恐慌贞滨,老刑警劉巖,帶你破解...
    沈念sama閱讀 217,277評(píng)論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件拍棕,死亡現(xiàn)場(chǎng)離奇詭異晓铆,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)绰播,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,689評(píng)論 3 393
  • 文/潘曉璐 我一進(jìn)店門骄噪,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人蠢箩,你說我怎么就攤上這事链蕊。” “怎么了谬泌?”我有些...
    開封第一講書人閱讀 163,624評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵滔韵,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我掌实,道長(zhǎng)陪蜻,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,356評(píng)論 1 293
  • 正文 為了忘掉前任贱鼻,我火速辦了婚禮宴卖,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘邻悬。我一直安慰自己症昏,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,402評(píng)論 6 392
  • 文/花漫 我一把揭開白布父丰。 她就那樣靜靜地躺著肝谭,像睡著了一般。 火紅的嫁衣襯著肌膚如雪础米。 梳的紋絲不亂的頭發(fā)上分苇,一...
    開封第一講書人閱讀 51,292評(píng)論 1 301
  • 那天,我揣著相機(jī)與錄音屁桑,去河邊找鬼医寿。 笑死,一個(gè)胖子當(dāng)著我的面吹牛蘑斧,可吹牛的內(nèi)容都是我干的靖秩。 我是一名探鬼主播须眷,決...
    沈念sama閱讀 40,135評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼沟突!你這毒婦竟也來了花颗?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 38,992評(píng)論 0 275
  • 序言:老撾萬榮一對(duì)情侶失蹤惠拭,失蹤者是張志新(化名)和其女友劉穎扩劝,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體职辅,經(jīng)...
    沈念sama閱讀 45,429評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡棒呛,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,636評(píng)論 3 334
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了域携。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片簇秒。...
    茶點(diǎn)故事閱讀 39,785評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖秀鞭,靈堂內(nèi)的尸體忽然破棺而出趋观,到底是詐尸還是另有隱情,我是刑警寧澤锋边,帶...
    沈念sama閱讀 35,492評(píng)論 5 345
  • 正文 年R本政府宣布皱坛,位于F島的核電站,受9級(jí)特大地震影響宠默,放射性物質(zhì)發(fā)生泄漏麸恍。R本人自食惡果不足惜灵巧,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,092評(píng)論 3 328
  • 文/蒙蒙 一搀矫、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧刻肄,春花似錦瓤球、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,723評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至麦到,卻和暖如春绿饵,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背瓶颠。 一陣腳步聲響...
    開封第一講書人閱讀 32,858評(píng)論 1 269
  • 我被黑心中介騙來泰國(guó)打工拟赊, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人粹淋。 一個(gè)月前我還...
    沈念sama閱讀 47,891評(píng)論 2 370
  • 正文 我出身青樓吸祟,卻偏偏與公主長(zhǎng)得像瑟慈,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子屋匕,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,713評(píng)論 2 354