NSTimer,NSRunLoop,autoreleasepool,多線程的愛恨情仇

引言

NSTimer內(nèi)存泄漏真的是因?yàn)関c與timer循環(huán)引用嗎?不是跌宛!

小伙伴們都知道,循環(huán)引用會(huì)造成內(nèi)存泄漏积仗,所謂循環(huán)引用無非就是強(qiáng)指針連成一個(gè)圈疆拘。但是,沒連成圈的強(qiáng)指針引用同樣可能造成內(nèi)存泄漏寂曹,如NSTimer
注意:timer內(nèi)存泄漏哎迄,部分童鞋認(rèn)為是vc與timer循環(huán)引用造成的,這種說法是錯(cuò)誤的隆圆!

正文

  • 內(nèi)存泄漏

NSTimer內(nèi)存泄漏的坑很多人都遇到過漱挚,為避免內(nèi)存泄漏,部分童鞋是這么做的:

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

更有甚者是這么做的:

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

然而并沒有什么...用渺氧!

通常會(huì)這么寫:

@interface NSTimerExample ()
@property (nonatomic, weak) NSTimer *timer0;
@property (nonatomic, weak) NSTimer *timer1;
@end

- (void)viewDidLoad {
    [super viewDidLoad];

    {
        NSTimer *one = [NSTimer scheduledTimerWithTimeInterval:1.f target:self selector:@selector(tick:) userInfo:@"timer0" repeats:YES];
        _timer0 = one;
    }
}

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

這句代碼timer會(huì)強(qiáng)引用target旨涝,即timer強(qiáng)引用vc,然而vc并沒有強(qiáng)引用timer侣背,哪來的vc與timer循環(huán)引用白华?但是,如果vc沒有強(qiáng)引用timer秃踩,timer是如何存活的衬鱼?
其實(shí),上句代碼默認(rèn)將timer加入到currentRunLoop中憔杨,currentRunLoop會(huì)強(qiáng)引用timer鸟赫,而currentRunLoop就是mainRunLoop,mainRunLoop一直存活消别,所以timer可以存活

如抛蚤,我們還會(huì)顯式的這么寫(這種runloop模式,在scrollview滑動(dòng)時(shí)同樣可以工作):

- (void)viewDidLoad {
    [super viewDidLoad];

    {
        NSTimer *one = [NSTimer timerWithTimeInterval:1.f target:self selector:@selector(tick:) userInfo:@"timer1" repeats:YES];
        [[NSRunLoop currentRunLoop] addTimer:one forMode:NSRunLoopCommonModes];
        _timer1 = one;  
    }
}

回到問題寻狂,為啥vc的dealloc方法進(jìn)不去岁经?

關(guān)系圖.png

從以上關(guān)系圖可見,只要runLoop存活蛇券,vc必然存活缀壤,所以vc的dealloc方法自然就不會(huì)執(zhí)行。因此纠亚,將timer的銷毀方法放在dealloc中必然造成內(nèi)存泄漏塘慕!

基于這種關(guān)系鏈,只要銷毀兩條線中的任意一條蒂胞,就不會(huì)出現(xiàn)內(nèi)存泄漏

所以出現(xiàn)了這種方案:

- (void)viewWillAppear:(BOOL)animated {
    [super viewWillAppear:animated];
    
    NSTimer *one = [NSTimer scheduledTimerWithTimeInterval:1.f target:self selector:@selector(tick:) userInfo:@"timer0" repeats:YES];
    _timer0 = one;
}

- (void)viewWillDisappear:(BOOL)animated {
    [super viewWillDisappear:animated];
    
    [_timer0 invalidate];
}

這樣做簡單粗暴图呢,直接將兩條線都銷毀,確實(shí)沒有內(nèi)存泄漏,可以滿足部分場景蛤织。但是如果在這個(gè)vc基礎(chǔ)上push一個(gè)新vc赴叹,而原vc的定時(shí)器還要繼續(xù)工作,這種方案顯然無法滿足需求指蚜。
雖然NSRunLoop提供了addTimer接口乞巧,但是并沒有提供removeTimer接口,顯然姚炕,runLoop與timer這條線無法直接銷毀摊欠,所以只能從vc與timer持有關(guān)系入手。
(CFRunLoopRef有這種接口)

CF_EXPORT void CFRunLoopAddTimer(CFRunLoopRef rl, CFRunLoopTimerRef timer, CFRunLoopMode mode);
CF_EXPORT void CFRunLoopRemoveTimer(CFRunLoopRef rl, CFRunLoopTimerRef timer, CFRunLoopMode mode);

思路很簡單柱宦,以NSProxy為基類些椒,創(chuàng)建一個(gè)weak proxy類,弱引用target即可掸刊,關(guān)系圖如下:


新關(guān)系圖.png

proxy弱引用vc免糕,所以vc可以釋放,當(dāng)vc執(zhí)行dealloc忧侧,在dealloc內(nèi)部銷毀timer即可

proxy可以這么寫:

NS_ASSUME_NONNULL_BEGIN

@interface JKWeakProxy : NSProxy

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

+ (instancetype)proxyWithTarget:(id)target;

@end

NS_ASSUME_NONNULL_END
@implementation JKWeakProxy

- (instancetype)initWithTarget:(id)target {
    _target = target;
    return self;
}

+ (instancetype)proxyWithTarget:(id)target {
    return [[self alloc] initWithTarget:target];
}

- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel {
    return [_target methodSignatureForSelector:sel];
}

- (void)forwardInvocation:(NSInvocation *)invocation {
    if ([_target respondsToSelector:invocation.selector]) {
        [invocation invokeWithTarget:_target];
    }
}

- (NSUInteger)hash {
    return [_target hash];
}

- (Class)superclass {
    return [_target superclass];
}

- (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;
}

- (NSString *)description {
    return [_target description];
}

- (NSString *)debugDescription {
    return [_target debugDescription];
}

@end

此時(shí)代碼只要這樣寫就可以了:

- (void)viewDidLoad {
    [super viewDidLoad];

    {
        JKWeakProxy *proxy = [JKWeakProxy proxyWithTarget:self];
        NSTimer *one = [NSTimer scheduledTimerWithTimeInterval:1.f target:proxy selector:@selector(tick:) userInfo:@"proxyTimer0" repeats:YES];
        _proxyTimer0 = one;
    }
}

- (void)dealloc {
    [_proxyTimer0 invalidate];
}

到這里石窑,timer內(nèi)存泄漏的坑已經(jīng)完美解決。

再簡單說說timer與runloop的組合使用

  • NSTimer && NSRunLoop && autoreleasepool && 多線程

前面已經(jīng)說過如何在scrollview滑動(dòng)時(shí)蚓炬,讓timer繼續(xù)工作

  NSTimer *one = [NSTimer timerWithTimeInterval:1.f target:self selector:@selector(tick:) userInfo:@"timer1" repeats:YES];
  [[NSRunLoop currentRunLoop] addTimer:one forMode:NSRunLoopCommonModes];
  _timer1 = one; 

簡單說下NSRunLoopCommonModes松逊,NSRunLoopCommonModes是一種偽模式,它表示一組runLoopMode的集合肯夏,具體包含:NSDefaultRunLoopMode经宏、NSTaskDeathCheckMode、UITrackingRunLoopMode驯击。由于含有UITrackingRunLoopMode烁兰,所以可以在滑動(dòng)時(shí)繼續(xù)工作。

多線程中又是如何使用timer的呢徊都?

    {
        dispatch_async(dispatch_get_global_queue(0, 0), ^{
            JKWeakProxy *proxy = [JKWeakProxy proxyWithTarget:self];
            NSTimer *one = [NSTimer scheduledTimerWithTimeInterval:1.f target:proxy selector:@selector(tick:) userInfo:@"dispatchTimer0" repeats:YES];
            [[NSRunLoop currentRunLoop] run];
            _dispatchTimer0 = one;
        });
    }

多線程中需要顯示啟動(dòng)runloop沪斟,為啥?

我們無法主動(dòng)創(chuàng)建runloop暇矫,只能通過[NSRunLoop currentRunLoop]主之、CFRunLoopGetCurrent()[NSRunLoop mainRunLoop]李根、CFRunLoopGetMain()來獲取runloop槽奕。除主線程外,子線程創(chuàng)建后并不存在runloop朱巨,主動(dòng)獲取后才有runloop。當(dāng)子線程銷毀枉长,runloop也隨之銷毀冀续。

上句代碼為琼讽,獲取子線程的runloop,并開啟runloop洪唐,所以timer才可以正常運(yùn)行钻蹬。而在主線程中,mainRunLoop已經(jīng)在程序運(yùn)行時(shí)默認(rèn)開啟凭需,所以不需要顯示啟動(dòng)runloop问欠。

問題又來了,runloop開啟后粒蜈,timer會(huì)一直在子線程中運(yùn)行顺献,所以子線程不會(huì)銷毀,因此runloop無法自動(dòng)停止枯怖,這似乎又是個(gè)死循環(huán)注整。既然無法自動(dòng)停止,可以選擇其他方式停止runloop

  1. runUntilDate
    {
        dispatch_async(dispatch_get_global_queue(0, 0), ^{
            JKWeakProxy *proxy = [JKWeakProxy proxyWithTarget:self];
            NSTimer *one = [NSTimer scheduledTimerWithTimeInterval:1.f target:proxy selector:@selector(tick:) userInfo:@"dispatchTimer0" repeats:YES];
//            [[NSRunLoop currentRunLoop] run];
            
            NSDate *date = [NSDate dateWithTimeIntervalSinceNow:5.f];
            [[NSRunLoop currentRunLoop] runUntilDate:date];
            _dispatchTimer0 = one;
        });
    }

這樣可以讓runloop在5s后銷毀度硝,但是如果銷毀時(shí)間不確定怎么辦肿轨?

這涉及到runloop的運(yùn)行機(jī)制:
runloop在指定mode模式下運(yùn)行,每種mode至少要有一個(gè)mode item蕊程,如果runloop在某種mode模式下不含mode item椒袍,那么runloop直接退出。mode item有3種藻茂,分別為: Source/Timer/Observer驹暑,它們對(duì)應(yīng)CFRunLoopSourceRef/CFRunLoopTimerRef/CFRunLoopObserverRef。Source為事件源捌治,Timer為時(shí)間觸發(fā)器岗钩,Observer為觀察者。

OK肖油,了解這些之后現(xiàn)在可以銷毀子線程了

  1. invalidate

如果runloop在某種特停情況下要退出兼吓,由于上述代碼runloop的mode item只有Timer,所以只要銷毀timer森枪,runloop就會(huì)退出

[_timer invalidate];
  • NSTimer并非真實(shí)的時(shí)間機(jī)制
    什么意思视搏?即NSTimer并非基于我們現(xiàn)實(shí)生活中的物理時(shí)間。似乎還不是太好懂县袱,可以從以下幾點(diǎn)理解:
  1. timer需要在某種特定的runLoopMode下運(yùn)行浑娜,如果當(dāng)前mode為非指定mode,timer不會(huì)被觸發(fā)式散,直到mode變成指定mode筋遭,timer開始運(yùn)行
  2. 如果在指定mode下運(yùn)行,但timer觸發(fā)事件的時(shí)間點(diǎn)runloop剛好在處理其他事件,timer對(duì)應(yīng)的事件不會(huì)被觸發(fā)漓滔,直到下一次runloop循環(huán)
  3. 如果timer設(shè)置精度過高编饺,由于runloop可能存在大量mode item,timer精度過高極有可能timer對(duì)應(yīng)處理事件時(shí)間點(diǎn)出現(xiàn)誤差(精度最好不超過0.1s)

Tip: runloop的內(nèi)部是通過dispatch_source_t和mach_absolute_time()控制時(shí)間的响驴,因此要實(shí)現(xiàn)精確的定時(shí)器透且,應(yīng)使用dispatch_source_t或者mach_absolute_time()。CADisplayLink在某種程度上也可以當(dāng)做定時(shí)器豁鲤,但與NSTimer一樣秽誊,并不準(zhǔn)確。由于默認(rèn)刷新頻率與屏幕相同琳骡,因此可以用來檢測FPS

  • autoreleasepool
    既然說到runloop锅论,簡單說下autoreleasepool。runloop會(huì)默認(rèn)創(chuàng)建autoreleasepool日熬,在runloop睡眠前或者退出前會(huì)執(zhí)行pop操作

autoreleasepool的簡單應(yīng)用

    NSLog(@"begin");
    for (NSUInteger i = 0; i < 10000; i++) {
        NSString *str = [NSString stringWithFormat:@"hello %zd", i];
        NSLog(@"%@", str);
    }
    NSLog(@"end");

before.gif

可以看到棍厌,內(nèi)存一直在增加,并且for循環(huán)結(jié)束后竖席,內(nèi)存仍然不會(huì)釋放

可以將代碼加上autoreleasepool:

   NSLog(@"begin");
    for (NSUInteger i = 0; i < 10000; i++) {
        @autoreleasepool {
            NSString *str = [NSString stringWithFormat:@"hello %zd", i];
            NSLog(@"%@", str);
        }
    }
    NSLog(@"end");
after.gif

可以看到耘纱,內(nèi)存幾乎沒有變化

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市毕荐,隨后出現(xiàn)的幾起案子束析,更是在濱河造成了極大的恐慌,老刑警劉巖憎亚,帶你破解...
    沈念sama閱讀 216,692評(píng)論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件员寇,死亡現(xiàn)場離奇詭異,居然都是意外死亡第美,警方通過查閱死者的電腦和手機(jī)蝶锋,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,482評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來什往,“玉大人扳缕,你說我怎么就攤上這事”鹜” “怎么了躯舔?”我有些...
    開封第一講書人閱讀 162,995評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長省古。 經(jīng)常有香客問我粥庄,道長,這世上最難降的妖魔是什么豺妓? 我笑而不...
    開封第一講書人閱讀 58,223評(píng)論 1 292
  • 正文 為了忘掉前任惜互,我火速辦了婚禮布讹,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘训堆。我一直安慰自己炒事,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,245評(píng)論 6 388
  • 文/花漫 我一把揭開白布蔫慧。 她就那樣靜靜地躺著,像睡著了一般权薯。 火紅的嫁衣襯著肌膚如雪姑躲。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,208評(píng)論 1 299
  • 那天盟蚣,我揣著相機(jī)與錄音黍析,去河邊找鬼。 笑死屎开,一個(gè)胖子當(dāng)著我的面吹牛阐枣,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播奄抽,決...
    沈念sama閱讀 40,091評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼蔼两,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了逞度?” 一聲冷哼從身側(cè)響起额划,我...
    開封第一講書人閱讀 38,929評(píng)論 0 274
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎档泽,沒想到半個(gè)月后俊戳,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,346評(píng)論 1 311
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡馆匿,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,570評(píng)論 2 333
  • 正文 我和宋清朗相戀三年抑胎,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片渐北。...
    茶點(diǎn)故事閱讀 39,739評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡阿逃,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出腔稀,到底是詐尸還是另有隱情盆昙,我是刑警寧澤,帶...
    沈念sama閱讀 35,437評(píng)論 5 344
  • 正文 年R本政府宣布焊虏,位于F島的核電站淡喜,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏诵闭。R本人自食惡果不足惜炼团,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,037評(píng)論 3 326
  • 文/蒙蒙 一澎嚣、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧瘟芝,春花似錦易桃、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,677評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至贸宏,卻和暖如春造寝,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背吭练。 一陣腳步聲響...
    開封第一講書人閱讀 32,833評(píng)論 1 269
  • 我被黑心中介騙來泰國打工诫龙, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人鲫咽。 一個(gè)月前我還...
    沈念sama閱讀 47,760評(píng)論 2 369
  • 正文 我出身青樓签赃,卻偏偏與公主長得像,于是被迫代替她去往敵國和親分尸。 傳聞我的和親對(duì)象是個(gè)殘疾皇子锦聊,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,647評(píng)論 2 354

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

  • 一、什么是runloop 字面意思是“消息循環(huán)箩绍、運(yùn)行循環(huán)”括丁。它不是線程,但它和線程息息相關(guān)伶选。一般來講史飞,一個(gè)線程一次...
    WeiHing閱讀 8,126評(píng)論 11 111
  • 轉(zhuǎn)自http://blog.ibireme.com/2015/05/18/runloop 深入理解RunLoop ...
    飄金閱讀 983評(píng)論 0 4
  • Runloop實(shí)現(xiàn)了線程內(nèi)部的事件循環(huán)。一個(gè)線程通常一次只能執(zhí)行一個(gè)任務(wù)仰税,任務(wù)執(zhí)行完成線程就會(huì)退出构资,但是有時(shí)候,我...
    大猿媛閱讀 229評(píng)論 0 1
  • RunLoop 是 iOS 和 OS X 開發(fā)中非吃纱兀基礎(chǔ)的一個(gè)概念吐绵,這篇文章將從 CFRunLoop 的源碼入手,...
    iOS_Alex閱讀 905評(píng)論 0 10
  • Runloop是iOS和OSX開發(fā)中非澈诱溃基礎(chǔ)的一個(gè)概念己单,從概念開始學(xué)習(xí)。 RunLoop的概念 -般說耙饰,一個(gè)線程一...
    小貓仔閱讀 993評(píng)論 0 1