NSTimer內(nèi)存泄漏锅必、解決及MSWeakTimer

泄漏原因

NSTimer對象會強(qiáng)引用它的target對象。具體造成引用循環(huán)的原因惕艳,可以先看下以下代碼:

#import "ViewController.h"

@interface ViewController (){
    NSTimer *_timer;
}

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    [self startPolling];
}

- (void)startPolling {
    _timer = [NSTimer scheduledTimerWithTimeInterval:5.0
                                              target:self
                                            selector:@selector(doPoll)
                                            userInfo:nil repeats:YES];
}

- (void)stopPolling {
    [_timer invalidate];
    _timer = nil;
}

- (void)doPoll {
    //Do Something
}

- (void)dealloc {
    [_timer invalidate];
}
@end

我們的ViewController對象強(qiáng)引用一個實(shí)例變量_timer,與此同時(shí)_timer的target又是self(當(dāng)前ViewController對象)搞隐,前文提到過NSTimer會強(qiáng)引用它的target,此時(shí)就產(chǎn)生了一個引用循環(huán)远搪。

引用循環(huán)示例圖

目前打破這個循環(huán)的方式就是要么手動置空viewController劣纲,要么調(diào)用stopPolling方法置空_timer。
雖然看上去打破這個循環(huán)不難谁鳍,但是如果需要手動去調(diào)用一個方法來避免內(nèi)存泄漏其實(shí)是有點(diǎn)不太合理的癞季。
如果想用過在dealloc方法中調(diào)用stopPolling方法去打破循環(huán)會帶來一個雞生蛋的問題:該視圖控制器是無法被釋放的劫瞳,它的引用計(jì)數(shù)器因?yàn)開timer的原因永遠(yuǎn)不會降到0,也就不會觸發(fā)dealloc方法绷柒。

解決

Block法

思路就是使用block的形式替換掉原先的“target-selector”方式柠新,打斷_timer對于其他對象的引用。
官方已經(jīng)在iOS10之后加入了新的api辉巡,從而支持了block形式創(chuàng)建timer:

NSTimer新api

根據(jù)翻譯恨憎,加入block形式就是為了避免引用循環(huán)。
但是其實(shí)在項(xiàng)目中郊楣,為了向下兼容憔恳,這個api估計(jì)也是暫時(shí)用不到了。

根據(jù)《Effective Objective-C 2.0》一書的做法其實(shí)也是類似于官方的净蚤,不過基于更低版本的api钥组,適配起來會方便很多,可以參考一下:

#import <Foundation/Foundation.h>

@interface NSTimer (EOCBlockSupport)

+ (NSTimer *)eoc_scheduledTimerWithTimeInterval:(NSTimeInterval)interval
                                        repeats:(BOOL)repeats
                                          block:(void (^)(NSTimer *timer))block;
@end
#import "NSTimer+EOCBlockSupport.h"

@implementation NSTimer (EOCBlockSupport)

+ (NSTimer *)eoc_scheduledTimerWithTimeInterval:(NSTimeInterval)interval
                                        repeats:(BOOL)repeats
                                          block:(void (^)(NSTimer *))block {
    return [self scheduledTimerWithTimeInterval:interval
                                         target:self
                                       selector:@selector(eoc_blockInvoke:)
                                       userInfo:[block copy]
                                        repeats:repeats];
    
}

#pragma mark - Private Method
- (void)eoc_blockInvoke:(NSTimer *)timer {
    void(^block)(NSTimer *timer) = timer.userInfo;
    if (block) {
        block(timer);
    }
}
@end

簡單來說就是使用userInfo這個參數(shù)去傳遞block給selector去進(jìn)行執(zhí)行今瀑,target是timer自己程梦,不會造成引用循環(huán)。還有一個需要注意的地方就是規(guī)避block的引用循環(huán)橘荠,為什么之類的詳細(xì)解釋不在這說了屿附。

構(gòu)造第三方target法

@GGGHub對于該方法比較有研究:

利用RunTime解決由NSTimer導(dǎo)致的內(nèi)存泄 漏
利用NSProxy解決NSTimer內(nèi)存泄漏問題

以下內(nèi)容也是基于他給出的方法進(jìn)行展開。
首先講一下runtime的方法哥童,關(guān)鍵思路還是打破viewController的引用計(jì)數(shù)不能降為0挺份,從而使它可以調(diào)用dealloc方法,從而再打斷viewController和timer的強(qiáng)引用贮懈,代碼如下匀泊,需要復(fù)制的去原博:

runtime法

畫張圖方便理解:

原理圖

雖然圖中_targetObject和_timer之間好像有循環(huán)引用,但是由于self的干預(yù)可以直接置空_timer從而打破循環(huán)朵你。

至于NSPorxy方法其實(shí)原理也是一樣的各聘,也是運(yùn)用runtime,不過使用了消息轉(zhuǎn)發(fā)的機(jī)制抡医,使用NSProxy的原因如下(引用):

實(shí)際上本篇用了消息轉(zhuǎn)發(fā)的機(jī)制來避免NSTimer內(nèi)存泄漏的問題躲因,無論NSProxy
與NSObject的派生類在Objective-C
運(yùn)行時(shí)找不到消息都會執(zhí)行消息轉(zhuǎn)發(fā)。所以這個解決方案用NSProxy與NSObject
的子類都能實(shí)現(xiàn)魂拦,不過NSProxy從類名來看是代理類專門負(fù)責(zé)代理對象轉(zhuǎn)發(fā)消息的毛仪。相比NSObject類來說NSProxy更輕量級,通過NSProxy可以幫助Objective-C
間接的實(shí)現(xiàn)多重繼承的功能芯勘。

截一段代碼:

使用NSProxy
MSWeakTimer

描述
MSWeakTimer是由mindsnacks寫的一個輕量級的定時(shí)器庫,使用GCD來實(shí)現(xiàn)腺逛,沒有引用循環(huán)的問題并且線程安全荷愕。

先來解決一個問題,線程安全是什么鬼?
蘋果在NSTimer文檔的invalidate方法中寫到:

Special Considerations
You must send this message from the thread on which the timer was installed. If you send this message from another thread, the input source associated with the timer may not be removed from its run loop, which could prevent the thread from exiting properly.

大概就是NSTimer的啟動和失效必須都是在同一個線程調(diào)用,否則可能沒用安疗。

所以對于匿名的GCD線程抛杨,我們最好不要在里面用NSTimer了,而使用GCD自帶的定時(shí)線程荐类,于是MSWeakTimer誕生了怖现。值得一提的是這個庫是蘋果工程師認(rèn)證過的。

初始化

- (id)initWithTimeInterval:(NSTimeInterval)timeInterval
                    target:(id)target
                  selector:(SEL)selector
                  userInfo:(id)userInfo
                   repeats:(BOOL)repeats
             dispatchQueue:(dispatch_queue_t)dispatchQueue
{
    NSParameterAssert(target);
    NSParameterAssert(selector);
    NSParameterAssert(dispatchQueue);

    if ((self = [super init]))
    {
        self.timeInterval = timeInterval;
        self.target = target;
        self.selector = selector;
        self.userInfo = userInfo;
        self.repeats = repeats;

        NSString *privateQueueName = [NSString stringWithFormat:@"com.mindsnacks.msweaktimer.%p", self];
        //創(chuàng)建一個私有的串行隊(duì)列
        self.privateSerialQueue = dispatch_queue_create([privateQueueName cStringUsingEncoding:NSASCIIStringEncoding], DISPATCH_QUEUE_SERIAL);
        //保證私有的串行隊(duì)列任務(wù)在目標(biāo)隊(duì)列上串行執(zhí)行(先進(jìn)先執(zhí)行)玉罐。
        dispatch_set_target_queue(self.privateSerialQueue, dispatchQueue);
        //創(chuàng)建timer事件
        self.timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER,
                                            0,
                                            0,
                                            self.privateSerialQueue);
    }

    return self;
}

tolerance
由于系統(tǒng)底層的調(diào)度優(yōu)化關(guān)系屈嗤,當(dāng)我們使用定時(shí)器調(diào)用fired的時(shí)候并不能立馬就能運(yùn)行的〉跏洌可能馬上運(yùn)行饶号,也可能需要等一段時(shí)間(如果當(dāng)前CPU忙著做別的事情)。當(dāng)時(shí)我們可以設(shè)置一個最大等待時(shí)間季蚂。
看設(shè)置時(shí)間時(shí)候的源代碼:

- (void)resetTimerProperties
{
    int64_t intervalInNanoseconds = (int64_t)(self.timeInterval * NSEC_PER_SEC);
    int64_t toleranceInNanoseconds = (int64_t)(self.tolerance * NSEC_PER_SEC);

    dispatch_source_set_timer(self.timer,
                              dispatch_time(DISPATCH_TIME_NOW, intervalInNanoseconds),
                              (uint64_t)intervalInNanoseconds,
                              //這里設(shè)置了等待時(shí)間
                              toleranceInNanoseconds
                              );
}

再看看官方對于這個參數(shù)的詳細(xì)解釋吧:

Any fire of the timer may be delayed by the system in order to improve power consumption and system performance. The upper limit to the allowable delay
may be configured with the 'leeway' argument, the lower limit is under the
control of the system.
For the initial timer fire at 'start', the upper limit to the allowable delay is set to 'leeway' nanoseconds. For the subsequent timer fires at 'start' + N * 'interval', the upper limit is MIN('leeway','interval'/2).
The lower limit to the allowable delay may vary with process state such as visibility of application UI. If the specified timer source was created with a mask of DISPATCH_TIMER_STRICT, the system will make a best effort to strictly observe the provided 'leeway' value even if it is smaller than the current lower limit. Note that a minimal amount of delay is to be expected even if this flag is specified.

對于剛創(chuàng)建的timer第一次在start時(shí)間點(diǎn)fire茫船,那么這個fire的時(shí)間上限為'leeway',即第一次fire不會晚于'start' + 'leeway' 。
對于重復(fù)了N次的fire扭屁,那么這個時(shí)間上限就是 MIN('leeway','interval'/2)算谈。
如果我們使用了參數(shù)DISPATCH_TIMER_STRICT,那么系統(tǒng)將盡最大可能去"盡早
"啟動定時(shí)器料滥,即使DISPATCH_TIMER_STRICT比當(dāng)前的發(fā)射延遲下限還低濒生。注意就算這樣,還是會有微量的延遲幔欧。

MSWeakTimer中對于這個參數(shù)就是重新包裝一下罪治,名字叫tolerance,更好理解一點(diǎn)礁蔗。

OSAtomicTestAndSetBarrier

先看代碼:

- (void)invalidate
{
    // We check with an atomic operation if it has already been invalidated. Ideally we would synchronize this on the private queue,
    // but since we can't know the context from which this method will be called, dispatch_sync might cause a deadlock.
    if (!OSAtomicTestAndSetBarrier(7, &_timerFlags.timerIsInvalidated))
    {
        dispatch_source_t timer = self.timer;
        dispatch_async(self.privateSerialQueue, ^{
            dispatch_source_cancel(timer);
            ms_release_gcd_object(timer);
        });
    }
}

- (void)timerFired
{
    // Checking attomatically if the timer has already been invalidated.
    if (OSAtomicAnd32OrigBarrier(1, &_timerFlags.timerIsInvalidated))
    {
        return;
    }

    // We're not worried about this warning because the selector we're calling doesn't return a +1 object.
    #pragma clang diagnostic push
    #pragma clang diagnostic ignored "-Warc-performSelector-leaks"
        [self.target performSelector:self.selector withObject:self];
    #pragma clang diagnostic pop

    if (!self.repeats)
    {
        [self invalidate];
    }
}

在invalidate方法中使用了異步方法去取消定時(shí)器觉义,因?yàn)橛猛降脑捒赡軒砭€程死鎖。
于是這里引入了一個比較優(yōu)雅的OSAtomicTestAndSetBarrier方法去判斷和更改timer的invalidate狀態(tài)浴井。
這個函數(shù)的作用就是原子性得去檢測并設(shè)置屏障

  • 好處一:原子操作
  • 好處二:檢測和改變變量一步到位
  • 好處三:高大上
    后面的OSAtomicAnd32OrigBarrier也是差不多意思晒骇。(水平不高,就不敢亂說話了)磺浙。

這一塊還是需要專門花時(shí)間去研讀一下:Threading Programming Guide

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末洪囤,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子撕氧,更是在濱河造成了極大的恐慌瘤缩,老刑警劉巖,帶你破解...
    沈念sama閱讀 219,539評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件伦泥,死亡現(xiàn)場離奇詭異剥啤,居然都是意外死亡锦溪,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,594評論 3 396
  • 文/潘曉璐 我一進(jìn)店門府怯,熙熙樓的掌柜王于貴愁眉苦臉地迎上來刻诊,“玉大人,你說我怎么就攤上這事牺丙≡蜓模” “怎么了?”我有些...
    開封第一講書人閱讀 165,871評論 0 356
  • 文/不壞的土叔 我叫張陵冲簿,是天一觀的道長粟判。 經(jīng)常有香客問我,道長民假,這世上最難降的妖魔是什么浮入? 我笑而不...
    開封第一講書人閱讀 58,963評論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮羊异,結(jié)果婚禮上事秀,老公的妹妹穿的比我還像新娘。我一直安慰自己野舶,他們只是感情好易迹,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,984評論 6 393
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著平道,像睡著了一般睹欲。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上一屋,一...
    開封第一講書人閱讀 51,763評論 1 307
  • 那天窘疮,我揣著相機(jī)與錄音,去河邊找鬼冀墨。 笑死闸衫,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的诽嘉。 我是一名探鬼主播蔚出,決...
    沈念sama閱讀 40,468評論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼虫腋!你這毒婦竟也來了骄酗?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,357評論 0 276
  • 序言:老撾萬榮一對情侶失蹤悦冀,失蹤者是張志新(化名)和其女友劉穎趋翻,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體雏门,經(jīng)...
    沈念sama閱讀 45,850評論 1 317
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡嘿歌,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,002評論 3 338
  • 正文 我和宋清朗相戀三年掸掏,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了茁影。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片宙帝。...
    茶點(diǎn)故事閱讀 40,144評論 1 351
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖募闲,靈堂內(nèi)的尸體忽然破棺而出步脓,到底是詐尸還是另有隱情,我是刑警寧澤浩螺,帶...
    沈念sama閱讀 35,823評論 5 346
  • 正文 年R本政府宣布靴患,位于F島的核電站,受9級特大地震影響要出,放射性物質(zhì)發(fā)生泄漏鸳君。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,483評論 3 331
  • 文/蒙蒙 一患蹂、第九天 我趴在偏房一處隱蔽的房頂上張望或颊。 院中可真熱鬧,春花似錦传于、人聲如沸囱挑。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,026評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽平挑。三九已至,卻和暖如春系草,著一層夾襖步出監(jiān)牢的瞬間通熄,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,150評論 1 272
  • 我被黑心中介騙來泰國打工找都, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留唇辨,地道東北人。 一個月前我還...
    沈念sama閱讀 48,415評論 3 373
  • 正文 我出身青樓檐嚣,卻偏偏與公主長得像助泽,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子嚎京,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,092評論 2 355

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

  • 我們常用NSTimer的方式 如下代碼所示,是我們最常見的使用timer的方式 當(dāng)使用NSTimer的schedu...
    yohunl閱讀 1,677評論 1 17
  • 首先介紹NSTimer的幾種創(chuàng)建方式 常用方法 三種方法的區(qū)別是: scheduledTimerWithTimeI...
    不吃雞爪閱讀 842評論 0 3
  • 偶得前言 NSRunLoop與定時(shí)器 [- invalidate的作用](#- invalidate的作用) 我們...
    tingxins閱讀 898評論 0 11
  • CSS的樣式1.內(nèi)置樣式嗡贺,就是html文件在標(biāo)簽上的默認(rèn)的樣式2.外部樣式引入 3.內(nèi)部樣式表 各種選擇器 4.內(nèi)...
    種諤閱讀 284評論 0 0
  • 同學(xué)選修成功掛掉,在班群抱怨說老師平時(shí)還總點(diǎn)名鞍帝,分給的少诫睬,選修而已,居然還掛了他帕涌,十分氣憤摄凡,奉勸我們別選他的課续徽。看...
    oO0啦啦閱讀 156評論 0 0