泄漏原因
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)的方式就是要么手動置空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:
根據(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ù)制的去原博:
畫張圖方便理解:
雖然圖中_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)多重繼承的功能芯勘。
截一段代碼:
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