NSTimer的使用問(wèn)題
用NSTimer做計(jì)時(shí)器循環(huán)事件的時(shí)候,很有可能會(huì)遇到以下兩個(gè)問(wèn)題:
- 正常啟動(dòng)的
timer在滾動(dòng)視圖滾動(dòng)的時(shí)候不能夠接收事件消息了- 當(dāng)前引用
timer的類不能夠得到釋放,進(jìn)而造成內(nèi)存泄露的問(wèn)題
所以針對(duì)于以上問(wèn)題,進(jìn)行記錄與說(shuō)明。
產(chǎn)生原因以及解決方法
正常啟動(dòng)的
timer在滾動(dòng)視圖滾動(dòng)的時(shí)候不能夠接收事件消息了
因?yàn)橄到y(tǒng)的timer記時(shí)器是通過(guò)iOS中的Runloop實(shí)現(xiàn)的,每一個(gè)定時(shí)器timer的實(shí)例都需要加入到Runloop中才能夠有效,由于Runloop有五種模式,分別是NSDefaultRunLoopMode、NSEventTrackingRunLoopMode、NSModalPaneRunLoopMode、NSTrackingRunLoopMode、NSRunLoopCommonModes。

這五種模式會(huì)在Runloop的不同的場(chǎng)景下進(jìn)行來(lái)回切換,而定時(shí)器timer如果沒(méi)有加入到切換對(duì)應(yīng)的場(chǎng)景mode中,則就會(huì)導(dǎo)致當(dāng)前的mode中不存在加入的timer,也就會(huì)引發(fā)timer接收不到定時(shí)器消息的問(wèn)題。本質(zhì)是runloop因?yàn)榍袚Qmode,且對(duì)應(yīng)mode中沒(méi)有當(dāng)前的timer對(duì)象,在當(dāng)前的mode中,導(dǎo)致timer收不到事件消息的問(wèn)題。
解決方法其實(shí)很簡(jiǎn)單,在創(chuàng)建定時(shí)器的時(shí)候,將定時(shí)器加入到runloop的不同的mode中,這樣就能確保runloop在切換mode的時(shí)候能夠找到對(duì)應(yīng)mode中的定時(shí)器,也就能夠發(fā)送定時(shí)器消息以保證定時(shí)器回調(diào)事件的正常了。
//注意,以下的方法會(huì)導(dǎo)致循環(huán)引用的發(fā)生,直接導(dǎo)致timer釋放不掉,解決方案在第二個(gè)問(wèn)題記錄中
- (void)normalTimer{
self.timer = [NSTimer timerWithTimeInterval:1.f target:self selector:@selector(timerEvent) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSRunLoopCommonModes];
}
- (void)cycleTimer{
self.timer =
[NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(timerEvent) userInfo:nil repeats:YES];
}
- (void)timerEvent{
NSLog(@"timer事件--%s",__func__);
}
- (void)dealloc{
NSLog(@"%s",__func__);
}
當(dāng)前引用
timer的類不能夠得到釋放,進(jìn)而造成內(nèi)存泄露的問(wèn)題
以上的定時(shí)器timer雖然能夠在Runloop的各種mode中完美運(yùn)行,但是會(huì)導(dǎo)致當(dāng)前的對(duì)象與timer相互引用導(dǎo)致循環(huán)引用問(wèn)題的產(chǎn)生??偨Y(jié)來(lái)說(shuō)就是:
由于定時(shí)器timer被當(dāng)前的對(duì)象引用,而啟動(dòng)定時(shí)器的時(shí)候,又將當(dāng)前對(duì)象作為參數(shù)傳入到定時(shí)器中,二者相互引用導(dǎo)致循環(huán)引用的產(chǎn)生。如下圖:

這里說(shuō)一種錯(cuò)誤的解決方法:將self改成weak類型后依舊會(huì)有循環(huán)引用,原因是修改weak屬性只對(duì)block有效,對(duì)于timer對(duì)象的內(nèi)部Target的strong引用是沒(méi)有效果的。
本質(zhì)是循環(huán)引用導(dǎo)致的內(nèi)存泄露,所以在相互引用上解除引用才是解決的根本。這里有兩種方案去解決這樣的問(wèn)題:
- 如果是iOS10以上,我們可以直接使用
timer的scheduledTimerWithTimeInterval:repeats:block:方法進(jìn)行設(shè)置
- (void)timerBlock{
if (__builtin_available(iOS 10, *)) {
self.timer = [NSTimer scheduledTimerWithTimeInterval:1.f repeats:YES block:^(NSTimer * _Nonnull timer) {
NSLog(@"%s",__func__);
}];
} else {}
}
- 可以引入新的對(duì)象C,將引用鏈由
A->B,B->A改成A->C, C->B, B->A

引入新對(duì)象C之后,三者引用關(guān)系就如上圖,這樣就不存在兩個(gè)對(duì)象之間相互引用了,在銷毀對(duì)象的時(shí)候,只需要消除其中一條引用,則可以全部消除引用關(guān)系。比如ObjectA在銷毀前,可以向Timer發(fā)送invalidate消息,消除對(duì)于ObjectC的引用,這樣就消除了一個(gè)引用關(guān)系,過(guò)程如下:
- 調(diào)用
timer的invidate方法結(jié)束定時(shí)器對(duì)對(duì)象C的引用,讓引入的新對(duì)象C先dealloc- 引入的新
對(duì)象C的釋放,結(jié)束了對(duì)于對(duì)象A的引用,當(dāng)前對(duì)象A也緊接著dealloc了- 當(dāng)前
對(duì)象A的釋放,結(jié)束了對(duì)于定時(shí)器B的引用,定時(shí)器對(duì)象B也緊接著dealloc了
基于上述的問(wèn)題,我們可以封裝一個(gè)解除timer引用的臨時(shí)對(duì)象,對(duì)象的內(nèi)容實(shí)現(xiàn)如下:
LCSafeObj.h
//
// LCSafeObj.h
// Timer
//
// Created by Leo on 2020/12/1.
//
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface LCSafeObj : NSObject
+ (NSTimer *)addTimerInterval:(NSTimeInterval)interval target:(id)target selecter:(SEL)selecter isrepeat:(BOOL)repeat obj:(id)object;
@end
NS_ASSUME_NONNULL_END
LCSafeObj.m
//
// LCSafeObj.m
// Timer
//
// Created by Leo on 2020/12/1.
//
#import "LCSafeObj.h"
@interface LCSafeObj ()
@property (nonatomic, strong) id target;
@property (nonatomic, assign) SEL selecter;
@end
@implementation LCSafeObj
- (instancetype)initWithTarget:(id)target selecter:(SEL)selecter{
if (self = [super init]) {
self.target = target;
self.selecter = selecter;
}
return self;
}
+ (NSTimer *)addTimerInterval:(NSTimeInterval)interval target:(id)target selecter:(SEL)selecter isrepeat:(BOOL)repeat obj:(id)object{
//此時(shí)LCSafeObj單獨(dú)引用外部對(duì)象
LCSafeObj *safeObj = [[LCSafeObj alloc] initWithTarget:target selecter:selecter];
//注意這里的Target傳入的是LCSafeObj類型的,并不是外部對(duì)象,目的是讓定時(shí)器timer引用新引入的對(duì)象C,
NSTimer *timer =
[NSTimer scheduledTimerWithTimeInterval:interval target:safeObj selector:selecter userInfo:object repeats:repeat];
//返回給傳入的對(duì)象,讓其單引用定時(shí)器timer,且控制定時(shí)器的invalid的時(shí)間,至此完成單鏈的引用
return timer;
}
/// 使用消息轉(zhuǎn)發(fā)來(lái)將SafeObj中沒(méi)有的方法調(diào)用轉(zhuǎn)移到傳入的對(duì)象中
/// @param aSelector 方法轉(zhuǎn)發(fā)
- (id)forwardingTargetForSelector:(SEL)aSelector{
if (aSelector == self.selecter) {
return self.target;
}
return [super forwardingTargetForSelector:aSelector];
}
- (void)dealloc{
NSLog(@"%s",__func__);
}
@end
這里實(shí)現(xiàn)的過(guò)程中注意用到了運(yùn)行時(shí)的消息轉(zhuǎn)發(fā)機(jī)制,以確保傳入對(duì)象的正確方法調(diào)用,以及代碼的簡(jiǎn)潔。
優(yōu)化內(nèi)容
上述的方法存在一些瑕疵,就是使用的時(shí)候可能還是需要在當(dāng)前使用的類中去手動(dòng)invalidDate timer計(jì)時(shí)器才能夠?qū)⑷哚尫诺?,這樣在開(kāi)發(fā)的過(guò)程中也是比較繁瑣的,可以考慮將釋放工作放到引入的三方對(duì)象C中,具體做法參考如下:
//
// LCSafeObj.m
// Timer
//
// Created by Leo on 2020/12/1.
//
#import "LCSafeObj.h"
@interface LCSafeObj ()
@property (nonatomic, weak) NSTimer *timer;
@property (nonatomic, weak) id target;
@property (nonatomic, assign) SEL selecter;
@property (nonatomic, copy) void (^timerEventBlock)(void);
@end
@implementation LCSafeObj
- (instancetype)initWithTarget:(id)target selecter:(SEL)selecter timerEventBlock:(void (^)(void))timerEventBlock{
if (self = [super init]) {
self.target = target;
self.selecter = selecter;
self.timerEventBlock = timerEventBlock;
}
return self;
}
- (void)dealloc{
NSLog(@"%s",__func__);
}
- (void)setTimer:(NSTimer *)timer{
_timer = timer;
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
}
+ (NSTimer *)addTimerInterval:(NSTimeInterval)interval target:(id)target selecter:(SEL)selecter isrepeat:(BOOL)repeat{
//此時(shí)LCSafeObj單獨(dú)引用外部對(duì)象
LCSafeObj *safeObj = [[LCSafeObj alloc] initWithTarget:target selecter:selecter timerEventBlock:nil];
//注意這里的Target傳入的是LCSafeObj類型的,并不是外部對(duì)象,目的是讓定時(shí)器timer引用新引入的對(duì)象C,
safeObj.timer =
[NSTimer scheduledTimerWithTimeInterval:interval target:safeObj selector:@selector(targetAction) userInfo:nil repeats:repeat];
//返回給傳入的對(duì)象,讓其單引用定時(shí)器timer,且控制定時(shí)器的invalid的時(shí)間,至此完成單鏈的引用
return safeObj.timer;
}
- (void)targetAction{
if (!self.target) {
[self.timer invalidate];
}
if (self.target && [self.target respondsToSelector:self.selecter]) {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
[self.target performSelector:self.selecter];
#pragma clang diagnostic pop
}
if (self.timerEventBlock) {self.timerEventBlock();}
}
+ (NSTimer *)addTimerInterval:(NSTimeInterval)interval isRepeat:(BOOL)repeat eventBlock:(void (^)(void))eventBlock{
//此時(shí)LCSafeObj單獨(dú)引用外部對(duì)象
LCSafeObj *safeObj = [[LCSafeObj alloc] initWithTarget:nil selecter:nil timerEventBlock:eventBlock];
//注意這里的Target傳入的是LCSafeObj類型的,并不是外部對(duì)象,目的是讓定時(shí)器timer引用新引入的對(duì)象C,
safeObj.timer =
[NSTimer scheduledTimerWithTimeInterval:interval target:safeObj selector:@selector(targetAction) userInfo:nil repeats:repeat];
//返回給傳入的對(duì)象,讓其單引用定時(shí)器timer,且控制定時(shí)器的invalid的時(shí)間,至此完成單鏈的引用
return safeObj.timer;
}
@end
優(yōu)化方案兩個(gè)要點(diǎn):
- 對(duì)外部target的引用采取weak弱引用,以保證外部對(duì)象的正常釋放
@property (nonatomic, weak) id target;
- 定時(shí)器事件方法中判斷target引用是否依舊存在,不存在則使用invalidDate 去除定時(shí)器timer對(duì)于引入的對(duì)象C的引用
- (void)targetAction{
if (!self.target) {
[self.timer invalidate];
}
if (self.target && [self.target respondsToSelector:self.selecter]) {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
[self.target performSelector:self.selecter];
#pragma clang diagnostic pop
}
if (self.timerEventBlock) {self.timerEventBlock();}
}