NSTimer那些事

定時器:

  • 需要被添加到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添加到當前Runloopdefault 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之間為強引用

timer和vc(self)Runloop之間的關(guān)系圖

如圖所示 分析一下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會置空。

(別人對于weakstrong的解釋 我覺得挺好的 多讀幾遍會有更多了解)

1.(weakstrong)不同的是:當一個對象不再有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

注意:

  1. 如果timer的引用為weak,在調(diào)用了invalidate之后,timer會被釋放(ARC會置空)如果在這之后還想用timer必須重新創(chuàng)建,所以 我們添加timer進Runloop之前可以通過timer的isValid方法判斷是否可用.
  2. 如果invalidate的調(diào)用不在添加Runloop的線程,那么timer雖然會釋放他持有的資源,但是它本身不會被釋放,他所在的Runloop也不會被釋放,也會導(dǎo)致內(nèi)存泄漏。
timer是否準時

不準時

  • 第一種不準時:有可能跳過去
  1. 線程在處理耗時的事情時會發(fā)生。
  2. 還有就是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點:

  1. RunLoop為了節(jié)省資源,并不會在非常準確的時間點觸發(fā)
  2. 線程有耗時操作,或者其它線程有耗時操作也會影響

iOS7以后,timer 有個屬性叫做 Tolerance(時間寬容度,默認是0),標示了當時間點到后,容許有多少最大誤差。
它只會在準確的觸發(fā)時間到加上Tolerance時間內(nèi)觸發(fā),而不會提前觸發(fā)(是不是有點像我們的火車,只會晚點。。。)。另外可重復(fù)定時器的觸發(fā)時間點不受Tolerance影響,即類似上面說的t8.5觸發(fā)后,下一個點不會是t10.5,而是t10 + Tolerance

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,247評論 6 543
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 99,520評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,362評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,805評論 1 317
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,541評論 6 412
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,896評論 1 328
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,887評論 3 447
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 43,062評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 49,608評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 41,356評論 3 358
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,555評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,077評論 5 364
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 44,769評論 3 349
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,175評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,489評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,289評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,516評論 2 379

推薦閱讀更多精彩內(nèi)容

  • 引言 定時器:A timer waits until a certain time interval has el...
    時間已靜止閱讀 2,860評論 6 34
  • NSTimer是iOS最常用的定時器工具之一,在使用的時候常常會遇到各種各樣的問題,最常見的是內(nèi)存泄漏,通常我們使...
    bomo閱讀 1,241評論 0 7
  • 之前要做一個發(fā)送短信驗證碼的倒計時功能,打算用NSTimer來實現(xiàn),做的過程中發(fā)現(xiàn)坑還是有不少的。 基本使用 NS...
    WeiHing閱讀 4,398評論 1 8
  • 洛帶著稚在租屋周圍逛了一大圈,洛走路時有點顛兒顛兒的,看上去是在想什么高興的事情,她平時穿高跟鞋走路可不是這樣。 ...
    江戶川糯米亂步閱讀 166評論 0 0
  • 今天,我想聊聊最近辦號遇到的事兒。感覺自己做的挺帶勁的。任何事情你不做,你永遠不知道你會遇到些什么事。做公眾號最開...
    thanksgi閱讀 333評論 0 2