原文 : 與佳期的個人博客(gonghonglou.com)
+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)ti
target:(id)aTarget
selector:(SEL)aSelector
userInfo:(nullable id)userInfo
repeats:(BOOL)yesOrNo;
用此方法創(chuàng)建出來的計時器,會在指定的時間間隔之后執(zhí)行任務。也可以令其反復執(zhí)行任務,直到開發(fā)者稍后將其手動關閉為止牵祟。target 與 selector 參數表示計時器將在哪個對象上調用哪個方法。計時器會保留其目標對象鄙陡,等到自身“失效”時再釋放此對象。調用 invalidate 方法可令計時器失效霹俺;執(zhí)行完相關任務之后柔吼,一次性的計時器也會失效。開發(fā)者若將計時器設置成重復執(zhí)行模式丙唧,那么必須自己調用 invalidate 方法,才能令其停止觅玻。
由于計時器會保留其目標對象想际,所以反復執(zhí)行任務通常會導致應用程序出問題培漏。也就是說,設置成重復執(zhí)行模式的那種計時器胡本,很容易引入“保留環(huán)”牌柄。
這是《Effective Objective-C 2.0》書中”第 52 條:別忘了 NSTimer 會保留其目標對象“ 一章中的說法。蘋果在其文檔中的說明:
repeats
If YES, the timer will repeatedly reschedule itself until invalidated. If NO, the timer will be invalidated after it fires.
并且我們在 Demo 中實驗也確實如此侧甫,調用 + (NSTimer *)scheduledTimerWithTimeInterval:
方法時如果 repeats = NO 的話是沒什么問題的珊佣,執(zhí)行一次后 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,造成循環(huán)引用守屉,所以 dealloc 永遠不會執(zhí)行惑艇。反復執(zhí)行任務則有可能出現崩潰。當然蘋果在 iOS10 之后出了新的方法使用 block 的方式可以避免循環(huán)引用:
+ (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 執(zhí)行蜈垮。這樣就能避免循環(huán)引用了。
BlocksKit 實現
BlocksKit 也提供了一個 NSTimer+BlocksKit.m 分類實現了相同的功能
BlocksKit 最新的 tag(2.2.5)及之前的版本的實現是:
@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》書中所講的思路:
這段代碼將計時器所應執(zhí)行的任務封裝成“塊”窃款,在調用的計時器函數時,把它作為 userInfo 參數傳進去牍氛。該參數可用來存放“萬能值”晨继,只要計時器還有效,就會一直保留著它搬俊。傳入參數時要通過 copy 方法將 block 拷貝到“堆”上紊扬,否則等到稍后要執(zhí)行他的時候,該塊可能已經無效了唉擂。計時器現在的 target 是 NSTimer 類對象餐屎,這是個單例,因為計時器是否會保留它玩祟,其實都無所謂腹缩。此處依然有保留環(huán),然而因為類對象(class object)無須回收,所以不用擔心藏鹊。
只需要在使用的時候注意避免 block 產生循環(huán)引用即可润讥,用 __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
后記
小白出手竿痰,請多指教脆粥。如言有誤,還望斧正影涉!
轉載請保留原文地址:http://gonghonglou.com/2019/07/07/crash-guard-nstimer/