NSTimer那些事

定時器:

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

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

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

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

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

注意:

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

不準時

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

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

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

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

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