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