說到NSTimer大家應(yīng)該都很熟悉,是的,我剛?cè)隝OS坑的第一個任務(wù)就是寫一個找回密碼的界面和功能,點擊獲取驗證碼倒數(shù)60秒才可以重發(fā)就是這么簡單,大部分APP幾乎都能見到的功能。
但第一眼看到NSTimer這個詞時我心里就嘀咕著是不是跟Java的Timer一樣有坑,結(jié)果還真有不少坑!!
經(jīng)過一段時間的歷練自己也不斷在成長,但發(fā)現(xiàn)身邊依然有不少新人被這個NSTimer坑了一次又一次。于是就有了這篇簡單的NSTimer避坑指南,順便鞏固下自己的知識,歡迎各位拍磚。
?下面是已經(jīng)避好坑的倒計時源碼,對一些坑寫了注釋,可以直接食用:
#import "ViewController.h"
@interface ViewController ()<UITableViewDelegate,UITableViewDataSource>
@property(nonatomic,strong)NSTimer *timer; // timer
@property(nonatomic,assign)int countDown; // 倒數(shù)計時用
@property(nonatomic,strong)NSDate *beforeDate; // 上次進(jìn)入后臺時間
@property(nonatomic,strong)UITableView *tableView; // tableView
@end
static NSString * const tableViewCellId = @"tableViewCellId";
static int const tick = 60;
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.title = @"簡單的倒計時Demo";
[self setup];
[self setupNotification];
[self startCountDown]; //< 假裝點擊了按鈕,開始計時
}
-(void)viewDidDisappear:(BOOL)animated {
[self viewDidDisappear:animated];
[self stopTimer]; //< 離開viewController后銷毀定時器,否則self被NSTimer強引用無法釋放,當(dāng)然也就輪不到dealloc執(zhí)行了
}
-(void)dealloc {
_tableView.delegate = nil;
_tableView.dataSource = nil;
[[NSNotificationCenter defaultCenter]removeObserver:self name:UIApplicationDidEnterBackgroundNotification object:nil];
[[NSNotificationCenter defaultCenter]removeObserver:self name:UIApplicationWillEnterForegroundNotification object:nil];
[self stopTimer]; //< 如果沒有在合適的地方銷毀定時器就會內(nèi)存泄漏啦,delloc也不可能執(zhí)行。正確的銷毀定時器這里可以不用寫這個方法了,這里只是提個醒
}
-(void)setup {
// tableView
_tableView = [[UITableView alloc]initWithFrame:self.view.frame style:UITableViewStylePlain];
_tableView.delegate = self;
_tableView.dataSource = self;
_tableView.backgroundColor = [UIColor whiteColor];
_tableView.rowHeight = 44;
[self.view addSubview:_tableView];
}
-(void)setupNotification {
[[NSNotificationCenter defaultCenter]addObserver:self selector:@selector(enterBG) name:UIApplicationDidEnterBackgroundNotification object:nil];
[[NSNotificationCenter defaultCenter]addObserver:self selector:@selector(enterFG) name:UIApplicationWillEnterForegroundNotification object:nil];
}
#pragma mark - method area
/**
* 進(jìn)入后臺記錄當(dāng)前時間
*/
-(void)enterBG {
NSLog(@"應(yīng)用進(jìn)入后臺啦");
_beforeDate = [NSDate date];
}
/**
* 返回前臺時更新倒計時值
*/
-(void)enterFG {
NSLog(@"應(yīng)用將要進(jìn)入到前臺");
NSDate * now = [NSDate date];
int interval = (int)ceil([now timeIntervalSinceDate:_beforeDate]);
int val = _countDown - interval;
if(val > 1){
_countDown -= interval;
}else{
_countDown = 1;
}
}
/**
* 開始倒計時
*/
-(void)startCountDown {
_countDown = tick; //< 重置計時
_timer = [NSTimer timerWithTimeInterval:1.0 target:self selector:@selector(timerFired:) userInfo:nil repeats:YES]; //< 需要加入手動RunLoop,需要注意的是在NSTimer工作期間self是被強引用的
[[NSRunLoop currentRunLoop] addTimer:_timer forMode:NSRunLoopCommonModes]; //< 使用NSRunLoopCommonModes才能保證RunLoop切換模式時,NSTimer能正常工作。
}
/**
* 停止倒計時
* 別小看銷毀定時器,沒用好可就內(nèi)存泄漏咯
*/
- (void)stopTimer {
if (_timer) {
[_timer invalidate];
}
}
/**
* 倒計時邏輯
*/
-(void)timerFired:(NSTimer *)timer {
switch (_countDown) {
case 1:
NSLog(@"重新發(fā)送");
[self stopTimer];
break;
default:
_countDown -=1;
NSLog(@"倒計時中:%d",_countDown);
break;
}
}
#pragma mark - tableView delegate
-(UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
UITableViewCell * cell = [tableView dequeueReusableCellWithIdentifier:tableViewCellId];
if(!cell) {
cell = [[UITableViewCell alloc]initWithStyle:UITableViewCellStyleDefault reuseIdentifier:tableViewCellId];
cell.textLabel.text = @"測試用";
}
return cell;
}
-(NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return 10;
}
-(void)scrollViewDidScroll:(UIScrollView *)scrollView {
NSLog(@"tableView滑動咯");
}
上面代碼中有兩類問題,但屬于可容忍范圍:
1.記錄時間的方法雖然能修正因計時器暫停期間的時間差,但有一個弊端,如果在應(yīng)用進(jìn)入后臺期間修改了手機時間會出現(xiàn)問題。
2.當(dāng)計時器工作時,突然來了一項耗時任務(wù)會使NSTimer跳過執(zhí)行時機。如果要求比較嚴(yán)格可以使用GCD定時器。
接下來細(xì)說下NSTimer常見的幾個坑吧:
1.默認(rèn)情況下NSTimer不能在后臺正常工作:
神馬你說還能跑?確定你用的是真機?真機和模擬器的行為不一樣的:(
在這種情況下可以在應(yīng)用進(jìn)入后臺時記錄下當(dāng)前時間,等待應(yīng)用恢復(fù)到前臺的時候修正值。
2.滑動UI時NSTimer不能工作:
這個坑應(yīng)該不少人遇到過吧,起初程序運行的很正常,當(dāng)哪天遇到UIScrollView,UITableView這樣可滑動的控件時就會把你坑到。
當(dāng)NSTimer運行在NSDefaultRunLoopMode下的時候會因為RunLoopMode的改變而無法正常工作。
需要切換到NSRunLoopCommonModes才能保證NSTimer在NSDefaultRunLoopMode和UITrackingRunLoopMode下正常工作。避開這坑的另一個方法是用GCD定時器。
3.NSTimer使用不當(dāng)會造成內(nèi)存泄漏
//這里介紹常見的兩種NSTimer初始化方法
+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)ti target:(id)target selector:(SEL)aSelector userInfo:(id)userInfo repeats:(BOOL)repeats
+ (NSTimer *)timerWithTimeInterval:(NSTimeInterval)ti target:(id)target selector:(SEL)aSelector userInfo:(id)userInfo repeats:(BOOL)repeats
1.帶scheduled的方法創(chuàng)建的timer會被加入到當(dāng)前RunLoop的默認(rèn)模式下。同時NSTimer會強引用target和userInfo,本身也會被RunLoop強引用。
Timers work in conjunction with run loops. To use a timer effectively, you should be aware of how run loops operate—see NSRunLoop
and Threading Programming Guide. Note in particular that run loops maintain strong references to their timers, so you don’t have to maintain your own strong reference to a timer after you have added it to a run loop.
2.不帶scheduled的方法需要我們手動將timer添加到RunLoop中。同樣的,NSTimer會強引用target和userInfo。(但timer似乎不會被強引用,我將NSTimer設(shè)置成weak時,還沒等加入RunLoop中就被釋放了,程序崩潰)
想要釋放被NSTimer強引用的對象,只需要調(diào)用- (void)invalidate
即可。當(dāng)然坑也就坑在如果調(diào)用的姿勢不正確就會發(fā)生內(nèi)存泄漏,delloc也無法執(zhí)行咯。
正確的姿勢是在viewDidDisappear
中調(diào)用,如果在viewWillDisappear
中調(diào)用的話,呵呵:)你試試用手勢將當(dāng)前界面劃一半離開再恢復(fù),定時器直接失效了。如果不是在VC中的話需要規(guī)定好一個失效邊界,保證invalidate
一定會被調(diào)用到。
別高興得太早,還沒完呢
如果invalidate方法與創(chuàng)建NSTimer的方法不在一個線程還無法銷毀NSTimer。官方文檔都把這些坑說的清清楚楚明明白白,沒事多看看文檔不會錯。
Special Considerations
You must send this message from the thread on which the timer was installed. If you send this message from another thread, the input source associated with the timer may not be removed from its run loop, which could prevent the thread from exiting properly.
4.NSTimer并不準(zhǔn)確
如果NSTimer執(zhí)行過程中由于某種原因被延遲,會略過本該在延遲期間需要執(zhí)行的方法。
解決方案是使用GCD定時器。
A repeating timer always schedules itself based on the scheduled firing time, as opposed to the actual firing time. For example, if a timer is scheduled to fire at a particular time and every 5 seconds after that, the scheduled firing time will always fall on the original 5 second time intervals, even if the actual firing time gets delayed. If the firing time is delayed so far that it passes one or more of the scheduled firing times, the timer is fired only once for that time period; the timer is then rescheduled, after firing, for the next scheduled firing time in the future.
希望本文能給大家?guī)韼椭皶r避開NSTimer常見的坑。