定時器:
- 需要被添加到
Runloop
,否則不會運行,當然添加的Runloop
不存在也不會運行 - 還要指定添加到的
Runloop
的哪個模式,而且還可以指定添加到Runloop
的多個模式,模式不對也是不會運行的 -
Runloop
會對timer有強引用,timer會對目標對象進行強引用(是否隱約的感覺到坑了。。。) - timer的執(zhí)行時間并不準確,系統(tǒng)繁忙的話,還會被跳過去
-
invalidate
調(diào)用后,timer停止運行后,就一定能從Runloop
中消除嗎?
定時器的一般用法
- (void)viewDidLoad {
NSTimer *timer = [[NSTimer alloc] initWithFireDate:[NSDate date] interval:1 target:self selector:@selector(timerFire) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
self.timer = timer;
}
- (void)timerFire {
NSLog(@"timer fire");
}
上面的代碼就是我們使用定時器最常用的方式,可以總結(jié)為2個步驟:創(chuàng)建,添加到runloop
-
scheduled
開頭的方法會自動將timer添加到當前Runloop
的default mode
并馬上啟動 -
timer
開頭的方法需要自己添加到Runloop
并手動開啟
方法參數(shù):
-
ti(interval)
:定時器觸發(fā)間隔時間,單位為秒,可以是小數(shù)。如果值小于等于0.0的話,系統(tǒng)會默認賦值0.1毫秒 -
invocation
:這種形式用的比較少,大部分都是block和aSelector的形式 -
yesOrNo(rep)
:是否重復(fù),如果是YES則重復(fù)觸發(fā),直到調(diào)用invalidate
方法;如果是NO,則只觸發(fā)一次就自動調(diào)用invalidate
方法 -
aTarget(t)
:發(fā)送消息的目標,timer會強引用aTarget,直到調(diào)用invalidate
方法 -
aSelector(s)
:將要發(fā)送給aTarget
的消息,如果帶有參數(shù)則應(yīng):- (void)timerFireMethod:(NSTimer *)timer
聲明 -
userInfo(ui)
:傳遞的用戶信息。使用的話,首先aSelector
須帶有參數(shù)的聲明,然后可以通過[timer userInfo]
獲取,也可以為nil,那么[timer userInfo]
就為空 -
date
:觸發(fā)的時間,一般情況下我們都寫[NSDate date]
,這樣的話定時器會立馬觸發(fā)一次,并且以此時間為基準。如果沒有此參數(shù)的方法,則都是以當前時間為基準,第一次觸發(fā)時間是當前時間加上時間間隔ti -
block
:timer觸發(fā)的時候會執(zhí)行這個操作,帶有一個參數(shù),無返回值
添加到runloop,參數(shù)timer是不能為空的,否則拋出異常
- (void)addTimer:(NSTimer *)timer forMode:(NSRunLoopMode)mode
另外,系統(tǒng)提供了一個- (void)fire
方法,調(diào)用它可以觸發(fā)一次:
NSTimer添加到NSRunLoop
timer必須添加到Runloop
才有效,很明顯要保證兩件事情,一是Runloop
存在(運行),另一個才是添加。確保這兩個前提后,還有Runloop
模式的問題。
一個timer可以被添加到Runloop
的多個模式,比如在主線程中runloop一般處于NSDefaultRunLoopMode
,而當滑動屏幕的時候,比如UIScrollView
或者它的子類UITableView、UICollectionView
等滑動時Runloop
處于UITrackingRunLoopMode
模式下,因此如果你想讓timer在滑動的時候也能夠觸發(fā),就可以分別添加到這兩個模式下。或者直接用NSRunLoopCommonModes
。
但是一個timer只能添加到一個Runloop
(Runloop
與線程一一對應(yīng)關(guān)系,也就是說一個timer只能添加到一個線程)。如果你非要添加到多個Runloop
,則只有一個有效
關(guān)于強引用的問題
一般我們使用NSTimer
如下圖所示
- (void)viewDidLoad {
// 代碼1
NSTimer *timer = [[NSTimer alloc] initWithFireDate:[NSDate date] interval:1 target:self selector:@selector(timerFire) userInfo:nil repeats:YES];
// 代碼2 上文中提到有些初始化方法會自動添加Runloop 這里是主動調(diào)用 效果都一樣
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
// 代碼3
self.timer = timer;
}
- (void)timerFire {
NSLog(@"timer fire");
}
假設(shè)代碼中self(viewController)
由UINavgationController
管理 并且timer與self
之間為強引用
如圖所示 分析一下4根線的由來
- L1:nav push 控制器的時候會強引用,即在push的時候產(chǎn)生;
- L2:是在代碼3的位置產(chǎn)生
- L3:是在代碼1的位置產(chǎn)生,至此L2與L3已經(jīng)產(chǎn)生了循環(huán)引用,雖然timer還沒有添加到
Runloop
- L4:是在代碼2的位置產(chǎn)生
我們常說的NSTimer
會產(chǎn)生循環(huán)引用 其實就是由于timer會對self進行強引用造成的。
打破循環(huán)引用
解決循環(huán)引用,首先想到的方法就是讓self對timer為弱引用weak
或者time對target
如self替換為weakSelf
然而這真的有用嗎?
例:
@interface TimerViewController ()
@property (nonatomic, weak) NSTimer *timer;
@end
@implementation TimerViewController
- (void)dealloc {
[self.timer invalidate];
NSLog(@"dealloc");
}
- (void)viewDidLoad {
[super viewDidLoad];
NSTimer *timer = [NSTimer timerWithTimeInterval:1.0f target:self selector:@selector(count) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
self.timer = timer;
}
- (void)count {
NSLog(@"count");
}
@end
從上一個vc push
進來之后, 定時器開始啟動 控制臺打印count, 然后pop回去, TimerViewController
并沒有走dealloc
方法 控制臺依然打印count。
將self改成weakSelf
效果依然一樣
- (void)viewDidLoad {
[super viewDidLoad];
__weak typeof(self) weakSelf = self;
NSTimer *timer = [NSTimer timerWithTimeInterval:1.0f target:weakSelf selector:@selector(count) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
self.timer = timer;
}
分析:
設(shè)置timer為weak
我們想通過self對timer的弱引用, 在self中的dealloc
方法中讓timer失效來達到相互釋放的目的。但是, timer內(nèi)部本身對于self有一個強引用。并且timer如果不調(diào)用invalidate
方法,就會一直存在,所以就導(dǎo)致了self根本釋放不了, 進而我們想通過在dealloc
中設(shè)置timer失效來釋放timer的方法也就行不通了。
設(shè)置self為weakSelf
用__weak
修飾self為weakSelf
, weakSelf
作為一個局部變量在一般情況下,這時候就可以打破循環(huán)引用。 但是timer
還是對weakSelf
有強引用,所以weakSelf
一直都在,即還是釋放不了。self和weakSelf
在這里的區(qū)別僅僅在于,如果self
釋放了,weakSelf
會置空。
(別人對于weak
和strong
的解釋 我覺得挺好的 多讀幾遍會有更多了解)
1.(
weak
與strong
)不同的是:當一個對象不再有strong
類型的指針指向它的時候,它就會被釋放,即使該對象還有_weak
類型的指針指向它;
2.一旦最后一個指向該對象的strong
類型的指針離開,這個對象將被釋放,如果這個時候還有weak
指針指向該對象,則會清除掉所有剩余的weak
指針
解決辦法
總結(jié): 要想解決循環(huán)引用的問題, 關(guān)鍵在于讓timer失效即調(diào)用[timer invalidate]
方法而不是各種weak。
- 外部調(diào)用方法觸發(fā)
invalidate
- (void)stopTimer {
[self.timer invalidate];
}
- 如果在是在view中的定時器, 可以重寫
removeFromSuperview
- (void)removeFromSuperview {
[super removeFromSuperview];
[self.timer invalidate];
}
- 將timer的target設(shè)為一個中間類
@interface BreakTimeLoop ()
// 這里必須用weak 不然釋放不了
@property (nonatomic, weak) id owner;
@end
@implementation BreakTimeLoop
- (void)dealloc {
NSLog(@"BreakTimeLoop dealloc");
}
- (instancetype)initWithOwner:(id)owner {
if (self = [super init]) {
self.owner = owner;
}
return self;
}
- (void)doSomething:(NSTimer *)timer {
// 需要參數(shù)可以通過 timer.userInfo 獲取
[self.owner performSelector:@selector(count)];
}
在vc中
@interface TimerViewController ()
/** timer在這種情況下也可用strong **/
@property (nonatomic, strong) NSTimer *timer;
/** 中間類 這里用strong和weak無所謂 主要是breaker中對owner的引用為weak就行 **/
@property (nonatomic, strong) BreakTimeLoop *breaker;
@end
@implementation TimerViewController
- (void)dealloc {
[self.timer invalidate];
NSLog(@"TimerViewController dealloc");
}
- (void)viewDidLoad {
[super viewDidLoad];
self.breaker = [[BreakTimeLoop alloc] initWithOwner:self];
// 這里的doSomething: 如果在.h中沒有聲明會報警告 不過不影響實現(xiàn)
self.timer = [NSTimer timerWithTimeInterval:1.0f target:self.breaker selector:@selector(doSomething:) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSRunLoopCommonModes];
}
- (void)count {
NSLog(@"count");
}
@end
控制臺打印:
- 給
NSTimer
寫一個分類
@interface NSTimer (SLBreakTimer)
+ (NSTimer *)sl_scheduledTimerWithTimeInterval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)())block;
@end
@implementation NSTimer (SLBreakTimer)
+ (NSTimer *)sl_scheduledTimerWithTimeInterval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)())block {
// copy將block放入堆上防止提前釋放
return [NSTimer scheduledTimerWithTimeInterval:interval target:self selector:@selector(sl_block:) userInfo:[block copy] repeats:repeats];
}
+ (void)sl_block:(NSTimer *)timer {
void (^block)() = timer.userInfo;
if (block) {
block();
}
}
@end
在vc中
- (void)viewDidLoad {
[super viewDidLoad];
// 為weak防止循環(huán)引用
__weak typeof(self) weakSelf = self;
self.timer = [NSTimer sl_scheduledTimerWithTimeInterval:1.0f repeats:YES block:^{
// 防止self提前釋放
__strong typeof(weakSelf) strongSelf = weakSelf;
[strongSelf count];
}];
}
控制臺打印為:
- 采用block方式(iOS10之后才有)
+ (NSTimer *)timerWithTimeInterval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block API_AVAILABLE(macosx(10.12), ios(10.0), watchos(3.0), tvos(10.0));
+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block API_AVAILABLE(macosx(10.12), ios(10.0), watchos(3.0), tvos(10.0));
- (instancetype)initWithFireDate:(NSDate *)date interval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block API_AVAILABLE(macosx(10.12), ios(10.0), watchos(3.0), tvos(10.0));
invalidate
方法有啥用
在上文中, 我們所有做的一些都是圍繞invalidate
方法來做的。那么這個方法到底有什么用呢?
- 將timer從
Runloop
中移除 - 釋放他自身持有的資源 比如
target userInfo block
注意:
- 如果timer的引用為
weak
,在調(diào)用了invalidate
之后,timer會被釋放(ARC
會置空)如果在這之后還想用timer必須重新創(chuàng)建,所以 我們添加timer進Runloop
之前可以通過timer的isValid
方法判斷是否可用. - 如果
invalidate
的調(diào)用不在添加Runloop
的線程,那么timer雖然會釋放他持有的資源,但是它本身不會被釋放,他所在的Runloop
也不會被釋放,也會導(dǎo)致內(nèi)存泄漏。
timer是否準時
不準時
- 第一種不準時:有可能跳過去
- 線程在處理耗時的事情時會發(fā)生。
- 還有就是timer添加到的
Runloop
模式不是Runloop
當前運行的模式,這種情況經(jīng)常發(fā)生。
對于第一種情況我們不應(yīng)該在timer上下功夫,而是應(yīng)該避免這個耗時的工作。那么第二種情況,作為開發(fā)者這也是最應(yīng)該去關(guān)注的地方,要留意,然后視情況而定是否將timer添加到Runloop
多個模式。
雖然跳過去,但是,接下來的執(zhí)行不會依據(jù)被延遲的時間加上間隔時間,而是根據(jù)之前的時間來執(zhí)行。比如:
定時時間間隔為2
秒,t1
秒添加成功,那么會在t2、t4、t6、t8、t10
秒注冊好事件,并在這些時間觸發(fā)。假設(shè)第3
秒時,執(zhí)行了一個超時操作耗費了5.5
秒,則觸發(fā)時間是:t2、t8.5、t10
,第4
和第6
秒就被跳過去了,雖然在t8.5
秒觸發(fā)了一次,但是下一次觸發(fā)時間是t10
,而不是t10.5
。
- 第二種不準時:不準點
比如上面說的t2、t4、t6、t8、t10
,并不會在準確的時間觸發(fā),而是會延遲個很小的時間,原因也可以歸結(jié)為2點:
-
RunLoop
為了節(jié)省資源,并不會在非常準確的時間點觸發(fā) - 線程有耗時操作,或者其它線程有耗時操作也會影響
iOS7
以后,timer 有個屬性叫做 Tolerance
(時間寬容度,默認是0),標示了當時間點到后,容許有多少最大誤差。
它只會在準確的觸發(fā)時間到加上Tolerance
時間內(nèi)觸發(fā),而不會提前觸發(fā)(是不是有點像我們的火車,只會晚點。。。)。另外可重復(fù)定時器的觸發(fā)時間點不受Tolerance
影響,即類似上面說的t8.5
觸發(fā)后,下一個點不會是t10.5
,而是t10 + Tolerance
。