iOS 倒计时 与 NSTimer 踏坑记

说到NSTimer大家应该都很熟悉,是的,我刚入IOS坑的第一个任务就是写一个找回密码的界面和功能,点击获取验证码倒数60秒才可以重发就是这么简单,大部分APP几乎都能见到的功能。

但第一眼看到NSTimer这个词时我心里就嘀咕着是不是跟Java的Timer一样有坑,结果还真有不少坑!!
经过一段时间的历练自己也不断在成长,但发现身边依然有不少新人被这个NSTimer坑了一次又一次。于是就有了这篇简单的NSTimer避坑指南,顺便巩固下自己的知识,欢迎各位拍砖。

�下面是已经避好坑的倒计时源码,对一些坑写了注释,可以直接食用:

#import "ViewController.h"

@interface ViewController ()<UITableViewDelegate,UITableViewDataSource>
@property(nonatomic,strong)NSTimer *timer; // timer
@property(nonatomic,assign)int countDown; // 倒数计时用
@property(nonatomic,strong)NSDate *beforeDate; // 上次进入后台时间
@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强引用无法释放,当然也就轮不到dealloc执行了
}

-(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]; //< 如果没有在合适的地方销毁定时器就会内存泄漏啦,delloc也不可能执行。正确的销毁定时器这里可以不用写这个方法了,这里只是提个醒
}

-(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

/**
 *  进入后台记录当前时间
 */
-(void)enterBG {
    NSLog(@"应用进入后台啦");
    _beforeDate = [NSDate date];
}

/**
 *  返回前台时更新倒计时值
 */
-(void)enterFG {
    NSLog(@"应用将要进入到前台");
    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能正常工作。
}

/**
 *  停止倒计时
 *  别小看销毁定时器,没用好可就内存泄漏咯
 */
- (void)stopTimer {
    if (_timer) {
        [_timer invalidate];
    }
}

/**
 *  倒计时逻辑
 */
-(void)timerFired:(NSTimer *)timer {

    switch (_countDown) {
        case 1:
            NSLog(@"重新发送");
            
            [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.记录时间的方法虽然能修正因计时器暂停期间的时间差,但有一个弊端,如果在应用进入后台期间修改了手机时间会出现问题。


应用进入后台将手机时间调前一分钟

2.当计时器工作时,突然来了一项耗时任务会使NSTimer跳过执行时机。如果要求比较严格可以使用GCD定时器。

接下来细说下NSTimer常见的几个坑吧:

1.默认情况下NSTimer不能在后台正常工作:

神马你说还能跑?确定你用的是真机?真机和模拟器的行为不一样的:(
在这种情况下可以在应用进入后台时记录下当前时间,等待应用恢复到前台的时候修正值。


进入后台NSTimer就默默的暂停了

2.滑动UI时NSTimer不能工作:

这个坑应该不少人遇到过吧,起初程序运行的很正常,当哪天遇到UIScrollView,UITableView这样可滑动的控件时就会把你坑到。
当NSTimer运行在NSDefaultRunLoopMode下的时候会因为RunLoopMode的改变而无法正常工作。
需要切换到NSRunLoopCommonModes才能保证NSTimer在NSDefaultRunLoopMode和UITrackingRunLoopMode下正常工作。避开这坑的另一个方法是用GCD定时器。


滑动时NSDefaultRunLoopMode下的NSTimer不能工作

3.NSTimer使用不当会造成内存泄漏

//这里介绍常见的两种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的方法创建的timer会被加入到当前RunLoop的默认模式下。同时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设置成weak时,还没等加入RunLoop中就被释放了,程序崩溃)

想要释放被NSTimer强引用的对象,只需要调用- (void)invalidate即可。当然坑也就坑在如果调用的姿势不正确就会发生内存泄漏,delloc也无法执行咯。
正确的姿势是在viewDidDisappear中调用,如果在viewWillDisappear中调用的话,呵呵:)你试试用手势将当前界面划一半离开再恢复,定时器直接失效了。如果不是在VC中的话需要规定好一个失效边界,保证invalidate一定会被调用到。

别高兴得太早,还没完呢

如果invalidate方法与创建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并不准确

如果NSTimer执行过程中由于某种原因被延迟,会略过本该在延迟期间需要执行的方法。
解决方案是使用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.

希望本文能给大家带来帮助,及时避开NSTimer常见的坑。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,362评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,330评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,247评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,560评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,580评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,569评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,929评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,587评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,840评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,596评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,678评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,366评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,945评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,929评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,165评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,271评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,403评论 2 342

推荐阅读更多精彩内容