總結(jié)一下平時(shí)使用NSTimer碰到的幾個(gè)小問題:
- 作為定時(shí)器使用時(shí)不準(zhǔn)確的問題
- ScrollView滾動(dòng)時(shí)timer不執(zhí)行的問題
- 內(nèi)存泄漏的問題
NSTimer準(zhǔn)確性不高的原因
以上幾個(gè)問題都跟RunLoop
有關(guān)系,一個(gè) NSTimer
注冊(cè)到RunLoop
后,RunLoop
會(huì)為其重復(fù)的時(shí)間點(diǎn)注冊(cè)好事件。例如 10:00, 10:10, 10:20 這幾個(gè)時(shí)間點(diǎn)。RunLoop
為了節(jié)省資源,并不會(huì)在非常準(zhǔn)確的時(shí)間點(diǎn)回調(diào)這個(gè)Timer。Timer 有個(gè)屬性叫做 Tolerance (寬容度)
,標(biāo)示了當(dāng)時(shí)間點(diǎn)到后,容許有多少最大誤差。
如果某個(gè)時(shí)間點(diǎn)被錯(cuò)過了,例如執(zhí)行了一個(gè)很長(zhǎng)的任務(wù),則那個(gè)時(shí)間點(diǎn)的回調(diào)也會(huì)跳過去,不會(huì)延后執(zhí)行。就比如等公交,如果 10:10 時(shí)我忙著玩手機(jī)錯(cuò)過了那個(gè)點(diǎn)的公交,那我只能等 10:20 這一趟了。
ScrollView滾動(dòng)時(shí)timer不執(zhí)行的原因
這里有個(gè)概念叫 CommonModes
:一個(gè) Mode
可以將自己標(biāo)記為Common
屬性(通過將其 ModeName
添加到 RunLoop
的 commonModes
中)。每當(dāng) RunLoop
的內(nèi)容發(fā)生變化時(shí),RunLoop
都會(huì)自動(dòng)將 _commonModeItems
里的 Source/Observer/Timer
同步到具有 Common
標(biāo)記的所有Mode里。
應(yīng)用場(chǎng)景舉例:主線程的 RunLoop
里有兩個(gè)預(yù)置的 Mode:kCFRunLoopDefaultMode
和 UITrackingRunLoopMode
。這兩個(gè) Mode 都已經(jīng)被標(biāo)記為Common
屬性。DefaultMode
是 App 平時(shí)所處的狀態(tài),TrackingRunLoopMode
是追蹤 ScrollView 滑動(dòng)時(shí)的狀態(tài)。當(dāng)你創(chuàng)建一個(gè) Timer 并加到 DefaultMode
時(shí),Timer 會(huì)得到重復(fù)回調(diào),但此時(shí)滑動(dòng)一個(gè)TableView時(shí),RunLoop 會(huì)將 mode 切換為 TrackingRunLoopMode
,這時(shí) Timer 就不會(huì)被回調(diào),并且也不會(huì)影響到滑動(dòng)操作。
有時(shí)你需要一個(gè) Timer,在兩個(gè) Mode 中都能得到回調(diào),一種辦法就是將這個(gè) Timer 分別加入這兩個(gè) Mode。還有一種方式,就是將 Timer 加入到頂層的 RunLoop 的 commonModeItems
中。commonModeItems
被 RunLoop 自動(dòng)更新到所有具有Common
屬性的 Mode 里去。
NSTimer導(dǎo)致內(nèi)存泄漏的原因及解決方法
原因
-
NSTimer
作為局部變量使用,在添加到RunLoop
中的時(shí)會(huì)被RunLoop
強(qiáng)引用,導(dǎo)致Timer
本身不能被釋放。 - NSTimer作為屬性使用時(shí)只能聲明成強(qiáng)引用(使用弱引用添加到RunLoop中會(huì)導(dǎo)致奔潰),在創(chuàng)建
timer
的過程中,timer
會(huì)強(qiáng)引用當(dāng)前添加的target
,若target
是當(dāng)前timer
所屬的對(duì)象時(shí),會(huì)造成循環(huán)引用的問題 - 當(dāng)
timer
釋放時(shí)所在線程跟timer
添加到的RunLoop
對(duì)應(yīng)的線程不一致時(shí)釋放會(huì)有問題
解決方法
在合適的時(shí)機(jī)調(diào)用invalidate
方法。蘋果官方的描述如下:
This method is the only way to remove a timer from an NSRunLoop object. The NSRunLoop object removes its strong reference to the timer, either just before the invalidate method returns or at some later point.
If it was configured with target and user info objects, the receiver removes its strong references to those objects as well.
invaliate
是唯一一個(gè)將timer
從NSRunLoop
對(duì)象移除的方法,如果我們是使用target
跟userinfo
對(duì)象創(chuàng)建的,那么同樣會(huì)移除對(duì)這些對(duì)象強(qiáng)引用。
#import "TimerViewController.h"
@interface TimerViewController ()
@property (nonatomic, strong) NSTimer *timer;
@end
@implementation TimerViewController
- (void)dealloc {
NSLog(@"%@", NSStringFromSelector(_cmd));
}
- (void)viewDidLoad {
[super viewDidLoad];
[self propertyTimer];
}
- (void)viewDidDisappear:(BOOL)animated {
[super viewDidDisappear:animated];
if ([self.timer isValid]) {
[self.timer invalidate];
}
}
#pragma mark - private method
- (void)propertyTimer {
self.timer = [NSTimer timerWithTimeInterval:5
target:self
selector:@selector(propertyTimerMethod)
userInfo:nil
repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:self.timer
forMode:NSRunLoopCommonModes];
[self.timer fire];
}
- (void)propertyTimerMethod {
NSLog(@"%@", NSStringFromSelector(_cmd));
}
- (void)localTimer {
//局部變量 無法釋放
NSTimer *timer = [NSTimer timerWithTimeInterval:5
target:self
selector:@selector(localTimerMethod)
userInfo:nil
repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:timer
forMode:NSRunLoopCommonModes];
[timer fire];
}
- (void)localTimerMethod {
NSLog(@"%@", NSStringFromSelector(_cmd));
}