今天在做項目的時候,遇到一個播放語音的需求,由于需要顯示播放時長,于是用到了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上傳了,歡迎大家使用.