Crash 防禦方案(四):NSTimer

原文 : 與佳期的我的博客(gonghonglou.com)ios

+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)ti
                                     target:(id)aTarget
                                   selector:(SEL)aSelector
                                   userInfo:(nullable id)userInfo
                                    repeats:(BOOL)yesOrNo;
複製代碼

用此方法建立出來的計時器,會在指定的時間間隔以後執行任務。也能夠令其反覆執行任務,直到開發者稍後將其手動關閉爲止。target 與 selector 參數表示計時器將在哪一個對象上調用哪一個方法。計時器會保留其目標對象,等到自身「失效」時再釋放此對象。調用 invalidate 方法可令計時器失效;執行完相關任務以後,一次性的計時器也會失效。開發者若將計時器設置成重複執行模式,那麼必須本身調用 invalidate 方法,才能令其中止。git

因爲計時器會保留其目標對象,因此反覆執行任務一般會致使應用程序出問題。也就是說,設置成重複執行模式的那種計時器,很容易引入「保留環」。github

這是《Effective Objective-C 2.0》書中」第 52 條:別忘了 NSTimer 會保留其目標對象「 一章中的說法。蘋果在其文檔中的說明:macos

repeats If YES, the timer will repeatedly reschedule itself until invalidated. If NO, the timer will be invalidated after it fires.bash

而且咱們在 Demo 中實驗也確實如此,調用 + (NSTimer *)scheduledTimerWithTimeInterval: 方法時若是 repeats = NO 的話是沒什麼問題的,執行一次後 NSTimer 會自動 invalidate,但 repeats = YES 的話並不會,並且由於 NSTimer 的寫法是這樣的:函數

- (void)dealloc {
    [_timer invalidate];
}

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view.
    
    self.timer = [NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(repeatLog) userInfo:nil repeats:YES];
}

- (void)repeatLog {
    NSLog(@"timer");
}
複製代碼

self 持有 timer,timer 設置了 target 又會持有 self,形成循環引用,因此 dealloc 永遠不會執行。反覆執行任務則有可能出現崩潰。固然蘋果在 iOS10 以後出了新的方法使用 block 的方式能夠避免循環引用:oop

+ (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));
複製代碼

但咱們總要兼容老版本,不多有 APP 會直接捨棄 iOS 10 以前的用戶。因此《Effective Objective-C 2.0》書中也給出的解決方案是給 NSTimer 添加一個 Category,在 Category 裏添加對 +scheduledTimerWithTimeInterval: 方法的封裝方法,也就是想達到這樣的效果:在 VC 中使用的時候,self 持有 timer,timer 持有 category(NSTimer 類對象),self 調用 timer 的時候傳入 block 給 category 執行。這樣就能避免循環引用了。ui

BlocksKit 實現

BlocksKit 也提供了一個 NSTimer+BlocksKit.m 分類實現了相同的功能spa

BlocksKit 最新的 tag(2.2.5)及以前的版本的實現是:code

@implementation NSTimer (BlocksKit)

+ (id)bk_scheduledTimerWithTimeInterval:(NSTimeInterval)inTimeInterval block:(void (^)(NSTimer *timer))block repeats:(BOOL)inRepeats
{
	NSParameterAssert(block != nil);
	return [self scheduledTimerWithTimeInterval:inTimeInterval target:self selector:@selector(bk_executeBlockFromTimer:) userInfo:[block copy] repeats:inRepeats];
}

+ (id)bk_timerWithTimeInterval:(NSTimeInterval)inTimeInterval block:(void (^)(NSTimer *timer))block repeats:(BOOL)inRepeats
{
	NSParameterAssert(block != nil);
	return [self timerWithTimeInterval:inTimeInterval target:self selector:@selector(bk_executeBlockFromTimer:) userInfo:[block copy] repeats:inRepeats];
}

+ (void)bk_executeBlockFromTimer:(NSTimer *)aTimer {
	void (^block)(NSTimer *) = [aTimer userInfo];
	if (block) block(aTimer);
}

@end
複製代碼

代碼很簡單,正如《Effective Objective-C 2.0》書中所講的思路:

這段代碼將計時器所應執行的任務封裝成「塊」,在調用的計時器函數時,把它做爲 userInfo 參數傳進去。該參數可用來存放「萬能值」,只要計時器還有效,就會一直保留着它。傳入參數時要經過 copy 方法將 block 拷貝到「堆」上,不然等到稍後要執行他的時候,該塊可能已經無效了。計時器如今的 target 是 NSTimer 類對象,這是個單例,由於計時器是否會保留它,其實都無所謂。此處依然有保留環,然而由於類對象(class object)無須回收,因此不用擔憂。

只須要在使用的時候注意避免 block 產生循環引用便可,用 __weak typeof(self) weakSelf = self;__strong typeof(weakSelf) strongSelf = weakSelf; 便可避免。

BlocksKit 新實現(未打 tag)

值得提一下的是 BlocksKit 當前的最新代碼裏的 NSTimer+BlocksKit.m 又有了不一樣的實現,直接拋棄了 NSTimer,而是用了 CFRunLoopTimerCreateWithHandler 來實現:

@implementation NSTimer (BlocksKit)

+ (instancetype)bk_scheduleTimerWithTimeInterval:(NSTimeInterval)seconds repeats:(BOOL)repeats usingBlock:(void (^)(NSTimer *timer))block
{
    NSTimer *timer = [self bk_timerWithTimeInterval:seconds repeats:repeats usingBlock:block];
    [NSRunLoop.currentRunLoop addTimer:timer forMode:NSDefaultRunLoopMode];
    return timer;
}

+ (instancetype)bk_timerWithTimeInterval:(NSTimeInterval)inSeconds repeats:(BOOL)repeats usingBlock:(void (^)(NSTimer *timer))block
{
    NSParameterAssert(block != nil);
    CFAbsoluteTime seconds = fmax(inSeconds, 0.0001);
    CFAbsoluteTime interval = repeats ? seconds : 0;
    CFAbsoluteTime fireDate = CFAbsoluteTimeGetCurrent() + seconds;
    return (__bridge_transfer NSTimer *)CFRunLoopTimerCreateWithHandler(NULL, fireDate, interval, 0, 0, (void(^)(CFRunLoopTimerRef))block);
}

@end
複製代碼

Demo 地址:GHLCrashGuard

後記

相關文章
相關標籤/搜索