iOS NSTimer使用注意事項(xiàng)

NSTimer 是系統(tǒng)提供的定時(shí)器廊宪,系統(tǒng)提供的api也比較簡單,使用很方便女轿,項(xiàng)目開發(fā)中會(huì)經(jīng)常用到箭启。然而,在使用NSTimer時(shí)蛉迹,如果不注意傅寡,非常容易引起內(nèi)存泄露的問題。本文總結(jié)了下NSTimer 引起內(nèi)存泄露問題的原因,以及解決方案荐操。

NSTimer的使用

通常情況下大猛,NSTimer 是作為controller或者view的一個(gè)屬性來使用:

gif播放的定時(shí)器

*/@property (nonatomic, strong) NSTimer *gifPlayTimer;

timer初始化:

NSTimer * timer = [NSTimer scheduledTimerWithTimeInterval:0.01 target:self selector:@selector(refreshPlayTime) userInfo:nil repeats:YES];

? ? self.gifPlayTimer = timer;

? ? [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];

該timer的作用是每隔0.01秒會(huì)執(zhí)行一次self 的 refreshPlayTime 方法。

這樣使用是沒有問題的淀零,每0.01秒確實(shí)會(huì)執(zhí)行一次 refreshPlayTime方法挽绩。

然而當(dāng)該控制器退出之后,會(huì)發(fā)現(xiàn)timer仍舊在執(zhí)行驾中,每隔0.01秒還是會(huì)調(diào)用 refreshPlayTime方法唉堪,而且,控制器退出了肩民,但是該控制器的 dealloc 方法并沒有被調(diào)用唠亚,也就是該控制器沒有被釋放,有內(nèi)存泄露的問題持痰。

既然timer沒有停止灶搜,那么手動(dòng)調(diào)用timer的 invalidate方法試一下。

通常情況下工窍,我們希望在控制器釋放的時(shí)候結(jié)束timer割卖,也就是在 dealloc 方法中將timer給停掉。代碼如下:

- (void)dealloc{

? ? [self.gifPlayTimer invalidate];

}

然而患雏,并沒有什么用鹏溯,在退出控制器之后,timer仍舊生效淹仑。原因上面其實(shí)也說了丙挽,因?yàn)榭刂破鞯?dealloc方法根本沒有被調(diào)用。為什么控制器不會(huì)被釋放匀借?以及如何解決颜阐?

NSTimer對(duì)target的強(qiáng)引用

首先看一下NSTimer初始化方法的官方文檔介紹:

+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo;

注意target參數(shù)的描述:

The object to which to send the message specified by aSelector when the timer fires. The timer maintains a strong reference to target until it (the timer) is invalidated.

注意:文檔中寫的很清楚,timer對(duì)target會(huì)有一個(gè)強(qiáng)引用吓肋,直到timer is invalidated凳怨。也就是說,在timer調(diào)用 invalidate方法之前蓬坡,timer對(duì)target一直都有一個(gè)強(qiáng)引用猿棉。這也是為什么控制器的dealloc 方法不會(huì)被調(diào)用的原因。

由于timer對(duì)target強(qiáng)引用的特性屑咳,如果要避免控制器不釋放的問題萨赁,需要在特定的時(shí)機(jī)調(diào)用timer 的 invalidate方法,也就是提前結(jié)束timer兆龙。在通常情況下杖爽,這種方式是可以解決問題的敲董,雖然需要警惕頁面退出之前有沒有結(jié)束timer,但畢竟解決了問題不是慰安。但是腋寨,日常項(xiàng)目中通常是多人協(xié)作,如果該timer是一個(gè)view的屬性化焕,而這個(gè)view又需要讓別人使用萄窜,那timer什么時(shí)候結(jié)束呢?讓調(diào)用者來管理timer的結(jié)束顯然是不合理的撒桨。更好的方式還是應(yīng)該在dealloc 方法中結(jié)束timer查刻,這樣調(diào)用者根本無須關(guān)注timer。

那么如何解決呢凤类?

timer修飾符改為weak

上述代碼中穗泵,self強(qiáng)引用了timer,timer又強(qiáng)引用了self谜疤,導(dǎo)致timer不能釋放佃延,self也一直不能釋放,那么如果timer的修飾符是weak夷磕,能解決這個(gè)問題嘛履肃?

@property (nonatomic, weak) NSTimer *gifPlayTimer;

經(jīng)過驗(yàn)證,使用weak修飾timer并不能解決問題企锌。Why?

看一下

- (void)addTimer:(NSTimer *)timer forMode:(NSRunLoopMode)mode;

方法的文檔介紹:

The receiver retains aTimer. To remove a timer from all run loop modes on which it is installed, send an invalidate message to the timer.

也就是說榆浓,runLoop會(huì)對(duì)timer有強(qiáng)引用于未,因此撕攒,timer修飾符是weak,timer還是不能釋放烘浦,timer的target也就不能釋放抖坪。

target用weak來修飾

既然timer強(qiáng)引用了target,導(dǎo)致target一直不能釋放闷叉,如果target用weak來修飾擦俐,能解決這個(gè)問題嘛?

__weak typeof(self) weakSelf = self;

NSTimer * timer = [NSTimer scheduledTimerWithTimeInterval:0.01 target:weakSelf selector:@selector(refreshPlayTime) userInfo:nil repeats:YES];

self.gifPlayTimer = timer;

[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];

經(jīng)過驗(yàn)證握侧,并沒有解決問題蚯瞧。Why?

實(shí)際上,上面的寫法和直接使用self的并沒有太大的區(qū)別品擎,唯一的區(qū)別是這種寫法timer的target有可能是nil埋合,不過這種可能性太低了。

使用中間target的方式

這種方法的思路是:新建一個(gè)中間對(duì)象Object萄传,該中間對(duì)象對(duì)timer真正的target有一個(gè)弱引用甚颂,寫代碼時(shí),timer的target 是Object。timer觸發(fā)的方法仍舊是真正target中的方法振诬。

部分代碼如下:

@interface _YYImageWeakProxy : NSProxy

// 對(duì)target有一個(gè)弱引用

@property (nonatomic, weak, readonly) id target;

- (instancetype)initWithTarget:(id)target;

+ (instancetype)proxyWithTarget:(id)target;

timer的初始化方法:

NSTimer * timer = [NSTimer scheduledTimerWithTimeInterval:0.01 target:[_YYImageWeakProxy proxyWithTarget:self] selector:@selector(refreshPlayTime) userInfo:nil repeats:YES];

self.gifPlayTimer = timer;

[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];

使用這種方式驗(yàn)證一下蹭睡,是可以解決問題的,target的dealloc方法會(huì)被調(diào)用赶么。

這種方式避免了timer直接引用target肩豁,因此self的dealloc方法會(huì)調(diào)用,在dealloc方法中移除timer即可辫呻。

使用這種方式timer的方法如何執(zhí)行呢蓖救?其實(shí)通過上面的代碼也可以看出,借助了NSProxy類印屁。NSProxy類是和NSObject平級(jí)的類循捺,該類的作用可以簡單的理解為一個(gè)代理,將消息轉(zhuǎn)發(fā)給另一個(gè)對(duì)象雄人。?

看一下_YYImageWeakProxy中的代碼:

@implementation _YYImageWeakProxy

- (id)forwardingTargetForSelector:(SEL)selector {

? ? return _target;

}

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

? ? void *null = NULL;

? ? [invocation setReturnValue:&null];

}

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

? ? return [NSObject instanceMethodSignatureForSelector:@selector(init)];

}

- (BOOL)respondsToSelector:(SEL)aSelector {

? ? return [_target respondsToSelector:aSelector];

}

- (Class)class {

? ? return [_target class];

}

- (BOOL)isKindOfClass:(Class)aClass {

? ? return [_target isKindOfClass:aClass];

}

- (BOOL)isMemberOfClass:(Class)aClass {

? ? return [_target isMemberOfClass:aClass];

}

- (BOOL)conformsToProtocol:(Protocol *)aProtocol {

? ? return [_target conformsToProtocol:aProtocol];

}

- (BOOL)isProxy {

? ? return YES;

}

@end

主要完成了消息轉(zhuǎn)發(fā)的功能从橘,將其接收到的消息,轉(zhuǎn)發(fā)給target础钠,這樣timer就能正確觸發(fā)對(duì)應(yīng)的方法

NSTimer+YYAdd

YYKit框架中提供了NSTimer的一個(gè)分類恰力,NSTimer+YYAdd,使用該分類中的方法旗吁,能夠解決問題踩萎,target的dealloc方法會(huì)被調(diào)用『艿觯看一下使用方法:

NSTimer * timer = [NSTimer timerWithTimeInterval:3.0f block:^(NSTimer * _Nonnull timer) {

? ? ? ? ? ? [weakSelf hideControlViewWithAnimation];

? ? ? ? } repeats:YES];

_hiddenTimer = timer;

[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];

+ (NSTimer *)timerWithTimeInterval:(NSTimeInterval)seconds block:(void (^)(NSTimer *timer))block repeats:(BOOL)repeats;

該方法是NSTimer+YYAdd 提供的香府。那么該Category是如何解決timer強(qiáng)引用target的問題呢?看一下其內(nèi)部實(shí)現(xiàn)

+ (NSTimer *)timerWithTimeInterval:(NSTimeInterval)seconds block:(void (^)(NSTimer *timer))block repeats:(BOOL)repeats {

? ? return [NSTimer timerWithTimeInterval:seconds target:self selector:@selector(_yy_ExecBlock:) userInfo:[block copy] repeats:repeats];

}

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

? ? if ([timer userInfo]) {

? ? ? ? void (^block)(NSTimer *timer) = (void (^)(NSTimer *timer))[timer userInfo];

? ? ? ? block(timer);

? ? }

}

注意:timer的target變成了self码倦,也就是timer企孩,而_yy_ExecBlock方法實(shí)際上就是執(zhí)行timer的回調(diào)。

也就是說袁稽,NSTimer+YYAdd中的解決方式也是使用中間類的方式勿璃,只不過這里的中間類正好是NSTimer對(duì)象,寫起來更簡單一些推汽。具體引用關(guān)系如下:?

invalidate方法注意事項(xiàng)

看一下invalidate方法的介紹:

兩點(diǎn):

(1)invalidate方法是唯一能從runloop中移除timer的方式补疑,調(diào)用invalidate方法后,runloop會(huì)移除對(duì)timer的強(qiáng)引用歹撒。

(2)timer的添加和timer的移除(invalidate)需要在同一個(gè)線程中莲组,否則timer可能不能正確的移除,線程不能正確退出栈妆。

在之前自己就是這樣解決循環(huán)引用的:

控制器中

- (void)viewDidDisappear:(BOOL)animated {

? ? [super viewDidDisappear:animated];

? ? [self.timer invalidate];

? ? self.timer = nil;

}

view中

- (void)removeFromSuperview {

? ? [super removeFromSuperview];

? ? [self.timer invalidate];

? ? self.timer = nil;

}

在某些情況下胁编,這種做法是可以解決問題的厢钧,但是有時(shí)卻會(huì)引起其他問題,比如控制器push到下一個(gè)控制器嬉橙,viewDidDisappear后早直,timer被釋放,此時(shí)再回來市框,timer已經(jīng)不復(fù)存在了霞扬。

2. 給self添加中間件proxy

考慮到循環(huán)引用的原因,改方案就是需要打破這些相互引用關(guān)系枫振,因此添加一個(gè)中間件喻圃,弱引用self,同時(shí)timer引用了中間件粪滤,這樣通過弱引用來解決了相互引用斧拍,如圖:

接下來看看怎么實(shí)現(xiàn)這個(gè)中間件,直接上代碼:

@interface ZYWeakObject()

@property (weak, nonatomic) id weakObject;

@end

@implementation ZYWeakObject

- (instancetype)initWithWeakObject:(id)obj {

? ? _weakObject = obj;

? ? return self;

}

+ (instancetype)proxyWithWeakObject:(id)obj {

? ? return [[ZYWeakObject alloc] initWithWeakObject:obj];

}

僅僅添加了weak類型的屬性還不夠杖小,為了保證中間件能夠響應(yīng)外部self的事件肆汹,需要通過消息轉(zhuǎn)發(fā)機(jī)制,讓實(shí)際的響應(yīng)target還是外部self予权,這一步至關(guān)重要昂勉,主要涉及到runtime的消息機(jī)制。

/**

* 消息轉(zhuǎn)發(fā)扫腺,讓_weakObject響應(yīng)事件

*/

- (id)forwardingTargetForSelector:(SEL)aSelector {

? ? return _weakObject;

}

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

? ? void *null = NULL;

? ? [invocation setReturnValue:&null];

}

- (BOOL)respondsToSelector:(SEL)aSelector {

? ? return [_weakObject respondsToSelector:aSelector];

}

接下來就可以這樣使用中間件了:

// target要設(shè)置成weakObj岗照,實(shí)際響應(yīng)事件的是self

ZYWeakObject *weakObj = [ZYWeakObject proxyWithWeakObject:self];

self.timer = [NSTimer scheduledTimerWithTimeInterval:1 target:weakObj selector:@selector(changeText) userInfo:nil repeats:YES];

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市笆环,隨后出現(xiàn)的幾起案子攒至,更是在濱河造成了極大的恐慌,老刑警劉巖咧织,帶你破解...
    沈念sama閱讀 217,542評(píng)論 6 504
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件嗓袱,死亡現(xiàn)場離奇詭異,居然都是意外死亡习绢,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,822評(píng)論 3 394
  • 文/潘曉璐 我一進(jìn)店門蝙昙,熙熙樓的掌柜王于貴愁眉苦臉地迎上來闪萄,“玉大人,你說我怎么就攤上這事奇颠“苋ィ” “怎么了?”我有些...
    開封第一講書人閱讀 163,912評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵烈拒,是天一觀的道長圆裕。 經(jīng)常有香客問我广鳍,道長,這世上最難降的妖魔是什么吓妆? 我笑而不...
    開封第一講書人閱讀 58,449評(píng)論 1 293
  • 正文 為了忘掉前任赊时,我火速辦了婚禮,結(jié)果婚禮上行拢,老公的妹妹穿的比我還像新娘祖秒。我一直安慰自己,他們只是感情好舟奠,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,500評(píng)論 6 392
  • 文/花漫 我一把揭開白布竭缝。 她就那樣靜靜地躺著,像睡著了一般沼瘫。 火紅的嫁衣襯著肌膚如雪抬纸。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,370評(píng)論 1 302
  • 那天耿戚,我揣著相機(jī)與錄音松却,去河邊找鬼。 笑死溅话,一個(gè)胖子當(dāng)著我的面吹牛晓锻,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播飞几,決...
    沈念sama閱讀 40,193評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼砚哆,長吁一口氣:“原來是場噩夢(mèng)啊……” “哼!你這毒婦竟也來了屑墨?” 一聲冷哼從身側(cè)響起躁锁,我...
    開封第一講書人閱讀 39,074評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎卵史,沒想到半個(gè)月后战转,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,505評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡以躯,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,722評(píng)論 3 335
  • 正文 我和宋清朗相戀三年槐秧,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片忧设。...
    茶點(diǎn)故事閱讀 39,841評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡刁标,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出址晕,到底是詐尸還是另有隱情膀懈,我是刑警寧澤,帶...
    沈念sama閱讀 35,569評(píng)論 5 345
  • 正文 年R本政府宣布谨垃,位于F島的核電站启搂,受9級(jí)特大地震影響硼控,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜胳赌,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,168評(píng)論 3 328
  • 文/蒙蒙 一牢撼、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧匈织,春花似錦浪默、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,783評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至乡小,卻和暖如春阔加,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背满钟。 一陣腳步聲響...
    開封第一講書人閱讀 32,918評(píng)論 1 269
  • 我被黑心中介騙來泰國打工胜榔, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人湃番。 一個(gè)月前我還...
    沈念sama閱讀 47,962評(píng)論 2 370
  • 正文 我出身青樓夭织,卻偏偏與公主長得像,于是被迫代替她去往敵國和親吠撮。 傳聞我的和親對(duì)象是個(gè)殘疾皇子尊惰,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,781評(píng)論 2 354

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