NSTimer定時器想必大家都用過,一個很常見的需求:進(jìn)入某個ViewController之后,需要開啟一個定時器,輪詢某個接口以獲取并更新本地數(shù)據(jù)??吹竭@種需求,腦海里下意識就有了想法,-viewDidLoad里創(chuàng)建一個NSTimer,然后-dealloc里結(jié)束掉它,ok看代碼:
@interface ViewController ()
@property (nonatomic, strong) NSTimer *timer;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.timer = [NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(fire) userInfo:nil repeats:YES];
}
- (void)fire {
NSLog(@"定時器調(diào)用");
}
- (void)dealloc {
[self.timer invalidate];
NSLog(@"dealloc - ViewController");
}
@end
搞定,跑一下看看。fire方法每隔一秒被調(diào)用一次,完美?。?!返回退掉控制器,,,誒,-fire方法怎么還在調(diào)用?控制器沒被釋放???!姿勢不對嗎?
看NSTimer的創(chuàng)建方法
NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(fire) userInfo:nil repeats:YES];
target傳的是self,這里應(yīng)該有一個強(qiáng)引用;
再看@property (nonatomic, strong) NSTimer *timer;
這里用的strong,嗦嘎!循環(huán)引用嘛!簡單,換weak破掉它!
重新跑驗(yàn)證看看??!進(jìn)入控制器,fire方法正常被調(diào)用;退出看看,,,怎么還在調(diào)用??!
.
.
.
沒轍,只好翻蘋果官方文檔了。Xcode工具欄找到Help->Developer Documentation,打開之后直接搜索NSTimer,進(jìn)去看看,找到這么一段:


看到一個關(guān)鍵詞:run loop。原來這個定時器還牽扯到了運(yùn)行循環(huán),看來關(guān)健點(diǎn)是將定時器移出這個run loop。(即調(diào)用-invalidate)
我們在控制器的-dealloc里調(diào)用-invalidate,很顯然dealloc沒有執(zhí)行。
那么換個地方調(diào)用唄!退出控制器時有幾個方法會被調(diào)用:
-
-viewWillDisappear:
如果是進(jìn)入下級頁面,這個定時器不應(yīng)該被停掉,所以這里不好。 -
-willMoveToParentViewController:
當(dāng)parent為nil時,表示控制器被移除了,此時停掉的時機(jī)正好。
- (void)willMoveToParentViewController:(UIViewController *)parent {
if(!parent){
[self.timer invalidate];
}
}
再跑一下看看!進(jìn)入控制器,fire方法被定時調(diào)用;退出控制器,dealloc方法打印了!fire方法停止調(diào)用,控制器釋放!搞定收工??!
.
.
.
等等!還有其它解決方式嗎?不在dealloc里停掉定時器總感覺怪怪的。。。(代碼強(qiáng)迫癥患者求治療~~)
繼續(xù)分析:
既然一定要在控制器的dealloc里停掉定時器,那么控制器就不該被定時器強(qiáng)引用。還記得創(chuàng)建定時器的時候傳的target嗎?
NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(fire) userInfo:nil repeats:YES];
我們在target這里作文章:你是強(qiáng)引用吧?我不給你控制器對象,再創(chuàng)建一個對象給你。這樣,控制器沒有被強(qiáng)引用,也就不會影響dealloc的調(diào)用,接著定時器就會被停掉。
但是還有一個問題:你新創(chuàng)建的對象怎么執(zhí)行我控制器里的fire方法????
.
.
.
消息轉(zhuǎn)發(fā),看代碼:
@interface ViewController ()
@property (nonatomic, strong) NSTimer *timer;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
HHTimerProxy *proxy = [HHTimerProxy alloc];
proxy.target = self;//這里是weak,不影響控制器的生命周期
self.timer = [NSTimer scheduledTimerWithTimeInterval:1 target:proxy selector:@selector(fire) userInfo:nil repeats:YES];
}
- (void)fire {
NSLog(@"定時器調(diào)用");
}
- (void)dealloc {
[self.timer invalidate];
NSLog(@"dealloc - ViewController");
}
@end
//HHTimerProxy.h
//這個類只用作消息轉(zhuǎn)發(fā)
@interface HHTimerProxy : NSProxy
@property (nonatomic, weak) id target;
@end
//HHTimerProxy.m
@implementation HHTimerProxy
- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel {
return [self.target methodSignatureForSelector:sel];
}
- (void)forwardInvocation:(NSInvocation *)invocation {
[invocation invokeWithTarget:self.target];
}
@end
重新運(yùn)行,進(jìn)入控制器,fire方法正常調(diào)用;退出控制器,dealloc方法打印,定時器停止調(diào)用fire方法,控制器正常釋放。問題解決。
不過,再想想這里的使用姿勢:我就用個定時器,還得記著創(chuàng)建一個proxy對象,然后把控制器傳給proxy的target屬性,再將這個proxy傳給定時器的target。。好麻煩不是嗎?
封裝走一波???
// NSTimer+HH.h
@interface NSTimer (HH)
+ (NSTimer *)HH_scheduledTimerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(__nullable id)userInfo repeats:(BOOL)yesOrNo;
@end
// NSTimer+HH.m
@implementation NSTimer (HH)
+ (NSTimer *)HH_scheduledTimerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(id)userInfo repeats:(BOOL)yesOrNo {
HHTimerProxy *proxy = [HHTimerProxy alloc];
proxy.target = aTarget;
return [NSTimer scheduledTimerWithTimeInterval:ti target:proxy selector:@selector(fire) userInfo:userInfo repeats:YES];
}
這樣,以后用到定時器就這么干:
#import "NSTimer+HH.h"
@interface ViewController ()
@property (nonatomic, strong) NSTimer *timer;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.timer = [NSTimer HH_scheduledTimerWithTimeInterval:1 target:self selector:@selector(fire) userInfo:nil repeats:YES];
}
- (void)fire {
NSLog(@"定時器調(diào)用");
}
- (void)dealloc {
[self.timer invalidate];
NSLog(@"dealloc - ViewController");
}
@end
依舊如此完美!
.
.
.
不過,你以為這樣就完了嗎??太天真了!
iOS10.0+有了block的調(diào)用姿勢,用起來很方便,代碼也緊湊,搞一下?
.
.
.
未完待續(xù)...