iOS之NSTimer循環(huán)引用的解決方案

前言

在使用NSTimer痰滋,如果使用不得當(dāng)特別會(huì)引起循環(huán)引用予权,造成內(nèi)存泄露些楣。所以怎么避免循環(huán)引用問題脂凶,下面我提出幾種解決NSTimer的幾種循環(huán)引用。

原因

當(dāng)你在ViewController(簡(jiǎn)稱VC)中使用timer屬性愁茁,由于VC強(qiáng)引用timer蚕钦,timer的target又是VC造成循環(huán)引用。當(dāng)你在VC的dealloc方法中銷毀timer鹅很,
發(fā)現(xiàn)VC被pop嘶居,VC的dealloc方法沒走,VC在等timer釋放才走dealloc促煮,timer釋放在dealloc中邮屁,所以引起循環(huán)引用。

解決方案

  • 在ViewController執(zhí)行dealloc前釋放timer(不推薦)
  • 對(duì)定時(shí)器NSTimer封裝
  • 蘋果API接口解決方案(iOS 10.0以上可用)
  • 使用block進(jìn)行解決
  • 使用NSProxy進(jìn)行解決

一菠齿、在ViewController執(zhí)行dealloc前釋放timer(不推薦)

  • 可以在viewWillAppear中創(chuàng)建timer
  • 可以在viewWillDisappear中銷毀timer

二佑吝、對(duì)定時(shí)器NSTimer封裝到PFTimer中

代碼如下:

//PFTimer.h文件
#import <Foundation/Foundation.h>
@interface PFTimer : NSObject

//開啟定時(shí)器
- (void)startTimer;

//暫停定時(shí)器
- (void)stopTimer;
@end

復(fù)制代碼

在PFTimer.m文件中代碼如下:

#import "PFTimer.h"

@implementation PFTimer {

    NSTimer *_timer;
}

- (void)stopTimer{

    if (_timer == nil) {
        return;
    }
    [_timer invalidate];
    _timer = nil;
}

- (void)startTimer{

    _timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(work) userInfo:nil repeats:YES];
}

- (void)work{

    NSLog(@"正在計(jì)時(shí)中。绳匀。芋忿。。襟士。盗飒。");
}

- (void)dealloc{

   NSLog(@"%s",__func__);
    [_timer invalidate];
    _timer = nil;
}

@end

復(fù)制代碼

在ViewController中使用代碼如下:

#import "ViewController1.h"
#import "PFTimer.h"

@interface ViewController1 ()

@property (nonatomic, strong) PFTimer *timer;

@end

@implementation ViewController1

- (void)viewWillDisappear:(BOOL)animated {

    [super viewWillDisappear:animated];
}

- (void)viewDidLoad {
    [super viewDidLoad];
    self.title = @"VC1";
    self.view.backgroundColor = [UIColor whiteColor];

    //自定義timer
    PFTimer *timer = [[PFTimer alloc] init];
    self.timer = timer;
    [timer startTimer];
}

- (void)dealloc {

    [self.timer stopTimer];
    NSLog(@"%s",__func__);
}
復(fù)制代碼

運(yùn)行打印結(jié)果:

-[ViewController1 dealloc]
-[PFTimer dealloc]

復(fù)制代碼

這個(gè)方式主要就是讓PFTimer強(qiáng)引用NSTimer,NSTimer強(qiáng)引用PFTimer,避免讓NSTimer強(qiáng)引用ViewController陋桂,這樣就不會(huì)引起循環(huán)引用逆趣,然后在dealloc方法中執(zhí)行NSTimer的銷毀,相對(duì)的PFTimer也會(huì)進(jìn)行銷毀了嗜历。

三宣渗、蘋果系統(tǒng)API可以解決(iOS10以上)

在iOS 10.0以后,蘋果官方新增了關(guān)于NSTimer的三個(gè)API:

+ (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)interval repeats:
(BOOL)repeats block:(void (^)(NSTimer *timer))block 
API_AVAILABLE(macosx(10.12), ios(10.0), watchos(3.0), tvos(10.0));

- (instancetype)initWithFireDate:(NSDate *)date interval:
(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block 
API_AVAILABLE(macosx(10.12), ios(10.0), watchos(3.0), tvos(10.0));

復(fù)制代碼

這三個(gè)方法都有一個(gè)Block的回調(diào)方法梨州。關(guān)于block參數(shù)痕囱,官方文檔有說(shuō)明:

the timer itself is passed as the parameter to this block when executed 
to aid in avoiding cyclical references。

復(fù)制代碼

翻譯過來(lái)就是說(shuō)暴匠,定時(shí)器在執(zhí)行時(shí)鞍恢,將自身作為參數(shù)傳遞給block,來(lái)幫助避免循環(huán)引用。使用很簡(jiǎn)單帮掉,但是要注意兩點(diǎn):

1.避免block的循環(huán)引用弦悉,使用__weak和__strong來(lái)避免

2.在持用NSTimer對(duì)象的類的方法中-(void)dealloc調(diào)用NSTimer 的- (void)invalidate方法;

四蟆炊、使用block來(lái)解決

通過創(chuàng)建一個(gè)NSTimer的category名字為PFSafeTimer稽莉,在NSTimer+PFSafeTimer.h代碼如下:

#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

@interface NSTimer (PFSafeTimer)

+ (NSTimer *)PF_ScheduledTimerWithTimeInterval:(NSTimeInterval)timeInterval block:
(void(^)(void))block repeats:(BOOL)repeats;

@end

NS_ASSUME_NONNULL_END

復(fù)制代碼

在NSTimer+PFSafeTimer.m中的代碼如下:

#import "NSTimer+PFSafeTimer.h"

@implementation NSTimer (PFSafeTimer)

+ (NSTimer *)PF_ScheduledTimerWithTimeInterval:(NSTimeInterval)timeInterval block:(void(^)(void))block repeats:(BOOL)repeats {

    return [NSTimer scheduledTimerWithTimeInterval:timeInterval target:self selector:@selector(handle:) userInfo:[block copy] repeats:repeats];
}

+ (void)handle:(NSTimer *)timer {

    void(^block)(void) = timer.userInfo;
    if (block) {
        block();
    }
}
@end

復(fù)制代碼

該方案主要要點(diǎn):

  • 將計(jì)時(shí)器所應(yīng)執(zhí)行的任務(wù)封裝成"Block",在調(diào)用計(jì)時(shí)器函數(shù)時(shí)涩搓,把block作為userInfo參數(shù)傳進(jìn)去污秆。

  • userInfo參數(shù)用來(lái)存放"不透明值",只要計(jì)時(shí)器有效昧甘,就會(huì)一直保留它良拼。

  • 在傳入?yún)?shù)時(shí)要通過copy方法,將block拷貝到"堆區(qū)"疾层,否則等到稍后要執(zhí)行它的時(shí)候将饺,該blcok可能已經(jīng)無(wú)效了。

  • 計(jì)時(shí)器現(xiàn)在的target是NSTimer類對(duì)象痛黎,這是個(gè)單例,因此計(jì)時(shí)器是否會(huì)保留它刮吧,其實(shí)都無(wú)所謂湖饱。此處依然有保留環(huán),然而因?yàn)轭悓?duì)象(class object)無(wú)需回收杀捻,所以不用擔(dān)心井厌。

再調(diào)用如下:

#import "ViewController1.h"
#import "PFTimer.h"
#import "NSTimer+PFSafeTimer.h"

@interface ViewController1 ()

//使用category
@property (nonatomic, strong) NSTimer *timer1;

@end

@implementation ViewController1

- (void)viewWillDisappear:(BOOL)animated {

    [super viewWillDisappear:animated];
}

- (void)viewDidLoad {
    [super viewDidLoad];
    self.title = @"VC1";
    self.view.backgroundColor = [UIColor whiteColor];

    __weak typeof(self) weakSelf = self;
    self.timer1 = [NSTimer PF_ScheduledTimerWithTimeInterval:1.0 block:^{

        __strong typeof(self) strongSelf = weakSelf;
        [strongSelf timerHandle];

    } repeats:YES];
}

//定時(shí)觸發(fā)的事件
- (void)timerHandle {

     NSLog(@"正在計(jì)時(shí)中。致讥。仅仆。。垢袱。墓拜。");
}

- (void)dealloc {

//    [self.timer stopTimer];
    NSLog(@"%s",__func__);
}

復(fù)制代碼

如果在block里面直接調(diào)用self,還是會(huì)保留環(huán)的请契。因?yàn)閎lock對(duì)self強(qiáng)引用咳榜,self對(duì)timer強(qiáng)引用,timer又通過userInfo參數(shù)保留block(強(qiáng)引用block)爽锥,這樣就構(gòu)成一個(gè)環(huán)block->self->timer->userinfo->block,所以要打破這個(gè)環(huán)的話要在block里面弱引用self涌韩。

使用NSProxy來(lái)解決循環(huán)引用

原理如下圖:

image

NSProxy解決循環(huán)引用原理.png

代碼如下:

//PFProxy.h
#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

@interface PFProxy : NSProxy

//通過創(chuàng)建對(duì)象
- (instancetype)initWithObjc:(id)object;

//通過類方法創(chuàng)建創(chuàng)建
+ (instancetype)proxyWithObjc:(id)object;

@end

NS_ASSUME_NONNULL_END

復(fù)制代碼

在PFProxy.m文件中寫代碼

#import "PFProxy.h"

@interface PFProxy()

@property (nonatomic, weak) id object;

@end
@implementation PFProxy

- (instancetype)initWithObjc:(id)object {

    self.object = object;
    return self;
}

+ (instancetype)proxyWithObjc:(id)object {

    return [[self alloc] initWithObjc:object];
}

- (void)forwardInvocation:(NSInvocation *)invocation {

    if ([self.object respondsToSelector:invocation.selector]) {

        [invocation invokeWithTarget:self.object];
    }
}

- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel {

    return [self.object methodSignatureForSelector:sel];
}
@end

復(fù)制代碼

在使用的時(shí)候如下代碼:

#import "ViewController1.h"
#import "PFProxy.h"

@interface ViewController1 ()

//使用NSProxy
@property (nonatomic, strong) NSTimer *timer2;

@end

@implementation ViewController1

- (void)viewWillDisappear:(BOOL)animated {

    [super viewWillDisappear:animated];
}

- (void)viewDidLoad {

    [super viewDidLoad];
    self.title = @"VC1";
    self.view.backgroundColor = [UIColor whiteColor];

    PFProxy *proxy = [[PFProxy alloc] initWithObjc:self];
    self.timer2 = [NSTimer scheduledTimerWithTimeInterval:1.0 target:proxy selector:@selector(timerHandle) userInfo:nil repeats:YES];
}

//定時(shí)觸發(fā)的事件
- (void)timerHandle {

     NSLog(@"正在計(jì)時(shí)中。氯夷。臣樱。。。雇毫。");
}

- (void)dealloc {

    [self.timer2 invalidate];
    self.timer2 = nil;
    NSLog(@"%s",__func__);
}

@end

復(fù)制代碼

當(dāng)pop當(dāng)前viewController時(shí)候玄捕,打印結(jié)果:

-[ViewController1 dealloc]
復(fù)制代碼

通過PFProxy這個(gè)偽基類(相當(dāng)于ViewController1的復(fù)制類),避免直接讓timer和viewController造成循環(huán)嘴拢。

原文:https://juejin.cn/post/6844903968250789896

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末桩盲,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子席吴,更是在濱河造成了極大的恐慌,老刑警劉巖孝冒,帶你破解...
    沈念sama閱讀 219,039評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件柬姚,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡庄涡,警方通過查閱死者的電腦和手機(jī)量承,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,426評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)穴店,“玉大人撕捍,你說(shuō)我怎么就攤上這事∑矗” “怎么了忧风?”我有些...
    開封第一講書人閱讀 165,417評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)球凰。 經(jīng)常有香客問我狮腿,道長(zhǎng),這世上最難降的妖魔是什么呕诉? 我笑而不...
    開封第一講書人閱讀 58,868評(píng)論 1 295
  • 正文 為了忘掉前任缘厢,我火速辦了婚禮,結(jié)果婚禮上甩挫,老公的妹妹穿的比我還像新娘贴硫。我一直安慰自己,他們只是感情好捶闸,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,892評(píng)論 6 392
  • 文/花漫 我一把揭開白布夜畴。 她就那樣靜靜地躺著,像睡著了一般删壮。 火紅的嫁衣襯著肌膚如雪贪绘。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,692評(píng)論 1 305
  • 那天央碟,我揣著相機(jī)與錄音税灌,去河邊找鬼均函。 笑死,一個(gè)胖子當(dāng)著我的面吹牛菱涤,可吹牛的內(nèi)容都是我干的苞也。 我是一名探鬼主播,決...
    沈念sama閱讀 40,416評(píng)論 3 419
  • 文/蒼蘭香墨 我猛地睜開眼粘秆,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼如迟!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起攻走,我...
    開封第一講書人閱讀 39,326評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤殷勘,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后昔搂,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體玲销,經(jīng)...
    沈念sama閱讀 45,782評(píng)論 1 316
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,957評(píng)論 3 337
  • 正文 我和宋清朗相戀三年摘符,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了贤斜。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,102評(píng)論 1 350
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡逛裤,死狀恐怖瘩绒,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情带族,我是刑警寧澤草讶,帶...
    沈念sama閱讀 35,790評(píng)論 5 346
  • 正文 年R本政府宣布,位于F島的核電站炉菲,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏坤溃。R本人自食惡果不足惜拍霜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,442評(píng)論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望薪介。 院中可真熱鬧祠饺,春花似錦、人聲如沸汁政。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,996評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)记劈。三九已至勺鸦,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間目木,已是汗流浹背换途。 一陣腳步聲響...
    開封第一講書人閱讀 33,113評(píng)論 1 272
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人军拟。 一個(gè)月前我還...
    沈念sama閱讀 48,332評(píng)論 3 373
  • 正文 我出身青樓剃执,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親懈息。 傳聞我的和親對(duì)象是個(gè)殘疾皇子肾档,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,044評(píng)論 2 355

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