淺談NSTimer內存泄漏問題

今天在做項目的時候,遇到一個播放語音的需求,由于需要顯示播放時長,于是用到了NSTimer那么問題來了,當控制器釋放的時候,音頻仍然在播放.利用XCode 8 新添加的內存泄漏查看工具Debug Memory Graph,我試著找到是否存在內存泄漏.

圖中所指為內存查看工具

在沒有播放時,JSQAudioMediaItem只被Controller引用,因此是沒有問題的


但是如果在播放的途中,將Controller給釋放了,按道理來說AudioItem也應該被釋放才對,觀察dealloc方法,發現Audio并沒有被釋放,因為audioItem在播放音頻之后,他的內存引用是這樣的:

audioItem還被AVAudioPlayer的delegate給引用了,當然,這不是事實的真相,嘗試在音頻播放的時候查看內存狀態,于是真相浮出水面了:

實際上AVAudioPlayer的delegate是assign的,所以不會有強引用問題,問題出在NSTimer
身上.

NSTimer和NSRunLoop

我們都知道NSTimer要結合NSRunloop來使用,其實NSRunloop對NSTimer是強引用的
所以在使用的時候建議對NSTimer弱引用,但這并不解決問題,問題在于NSTimer的target:
類似于這樣的語句:

self.progressTimer = [NSTimer scheduledTimerWithTimeInterval:0.1
                                                          target:self
                                                        selector:@selector(updateProgressTimer:)
                                                        userInfo:nil
                                                         repeats:YES];

其實,NSTiemr會對它Target的對象有強引用,所以,self并不會被釋放(因為定時器被MainRunloop引用,不會被釋放,定時器再通過target強引用self,self也不會被釋放)
所以,能不能把self變成一個weakSelf呢?

__weak typeof(self) weakSelf = self;
self.progressTimer = [NSTimer scheduledTimerWithTimeInterval:0.1
                                                          target:weakSelf
                                                        selector:@selector(updateProgressTimer:)
                                                        userInfo:nil
                                                         repeats:YES];

但是這并沒用.........釋放NSTimer只有一個方法,那就是- invalidate
于是去查閱蘋果文檔:

Removes the object from all runloop modes (releasing the receiver if it has been implicitly retained) and releases the 'target' object.

據官方介紹可知,- invalidate做了兩件事,首先是把本身(定時器)從NSRunLoop中移除,然后就是釋放對‘target’對象的強引用。可是,我們現在就是要在定時器的target釋放的時候(dealloc)去調用- invalidate,而因為定時器強引用了target(也就是self),導致永遠都不會調用self的dealloc方法,也就不會調用- invalidate了. 于是我們的定時器永遠都會被Runloop持有,而我們的音頻播放也不會在我們釋放了控制器之后就停止播放了...(因為定時器強引用了self,也就是audioItem)

那么解決方案是什么呢,一共有三個解決方案:

  • 利用NSNotificationCenter ,也就是注冊一個通知,當控制器釋放的時候,通知AudioItem,讓它去調用 - invalidate方法
  • 如果你的定時器是被控制器本身引用的,那好了,你可以在- viewDidDisapper方法中調用- invalidate方法

但是,如果你不想用通知(這東西一個項目里總是要用的,所以不想太泛濫)呢,而且你的定時器并不是被控制器引用,而是被某個繼承于NSObject的類引用(就像我這種情況,AudioItem不是控制器),那么你就無法通過- viewDidDisapper來調用了,那么其實你可以重寫一個NSTimer類,當然,這是假的Timer.

思路就是,既然定時器的target會強引用self,那么為self創建一個接住這一波強引用的東東不就行了嗎?
于是:

@interface CRSafeTimer ()

@property (nonatomic, assign) SEL selector;

@property (nonatomic, weak) NSTimer *timer;

@property (nonatomic, weak) id target;

@end
@implementation CRSafeTimer

- (void)run:(NSTimer *)timer {
    if (!self.target) {
        [self.timer invalidate];
    }else {
        [self.target performSelector:self.selector withObject:timer.userInfo];
    }
    
}

并且自己構造一個假的+ scheduledTimerWithTimeInterval 方法:

+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)interval
                                     target:(id)target
                                   selector:(SEL)aSelector
                                   userInfo:(id)userInfo
                                   repeats :(BOOL)repeats {
    CRSafeTimer *crTimer = [[CRSafeTimer alloc] init];
    crTimer.target = target;
    crTimer.selector = aSelector;
    crTimer.timer = [NSTimer scheduledTimerWithTimeInterval:interval
                                                     target:crTimer
                                                   selector:@selector(run:)
                                                   userInfo:userInfo
                                                    repeats:repeats];
    return crTimer.timer;
}

這就ok了,我把CRSafeTimer上傳了,歡迎大家使用.

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容