说到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常见的坑。
网友评论