cell的复用机制导致的倒计时问题解决
最近项目中用到了tableView的多个cell倒计时系统问题,本觉得很简单的一个事,一做发现还没这么简单,就此记录。
下面方法模拟网络请求返回数据。
按照常规思路,根据网络请求返回remainTime,封装模型,存到数组中,再在表格代理方法中赋值给cell
cell中根据传入模型中的remainTime属性,开启定时器每隔1s调用如下方法
程序一运行发现问题:每当表格滚动时,表格代理方法cellForRowAtIndexPath会不断重复调用,从数组中取得模型赋值给cell,而模型中的remainTime是固定的,于是倒计时系统不断重复开始倒计时。
发现问题点,开始着手解决。开始想到的是方法是在控制器中也开启一套定时器系统,当服务器数据remainTime返回时,将其中remainTime大于0s的数据保存在一个字典中,对所有键值对开始倒计时。
下面方法模拟网络数据返回,对所有remainTime大于0的字段保存到字典self.timerDic中
cell属性model中的remainTime字段从这个一直变化的self.timerDic字典中取值,于是滚动视图时cell获取到的就不是一个固定的remainTime,效果如下
此时已经解决了表格滚动时倒计时重复计时问题,但可以看到多次滚动后会造成如上显示错误,这是由于控制器和cell两套定时系统时差而引起的,具体后面分析。
此路貌似不通,于是我想到了KVO,让cell监听控制器中remainTime的数值变化
仔细分析上面倒计时时差原因,发现时差产生是由于定时器调用频率导致。举个场景说明:控制器返回数据时remainTime是10,过了0.9999s后用户滚动表格,此时cell从字典self.timerDic中取到的remainTime仍旧是10,于是cell定时系统的remainTime值比控制器的慢了0.9999s。同理分析也可能快0.9999s,于是可能会引发最多2s的极限误差。
找到具体原因修改就比较容易了,使用CADisplayLink,一分钟调用60次countDown方法,每次减去1/60s,则最大误差只有2*1/60s,比较准确,能够满足要求
最后做下适当优化:定时器在主线程工作,调用频率很高,每次调用还要遍历字典对每一个value递减后覆盖旧值,故希望定时器能在后台工作。定时器工作在后台线程时会自动将其注册到后台线程的runloop,而runloop依托线程但并不会自动创建,此时countDown无法接收到事件回调,需要手动生成runloop并保证其不会退出。这里参照AFN中的生成方法,核心代码如下:
@implementation ViewController{
NSMutableArray *_arr;
NSTimer *_timer;
NSInteger _notifNum;
}
- (void)viewDidLoad {
[super viewDidLoad];
_arr = @[].mutableCopy;
[self loadNewData];
}
// 下拉刷新
- (void)loadNewData{
[_arr removeAllObjects];
for (int i = 0; i < 100; i++) {
[_arr addObject:@(10 * i)];
}
[self.tableView reloadData];
// 清空
_notifNum = 0;
[self startTimer];
}
// 上拉刷新
- (void)loadMoreData{
for (int i = 0; i < 100; i++) {
NSInteger num = 10 * i; // 服务器拿到数字
num += _notifNum; // 将数据加上当前计时器的数字
[_arr addObject:@(num)];
}
[self.tableView reloadData];
}
- (void)startTimer{
if (_timer) return;
_timer = [NSTimer scheduledTimerWithTimeInterval:1 repeats:YES block:^(NSTimer * _Nonnull timer) {
[[NSNotificationCenter defaultCenter] postNotificationName:@"NSNotification" object:@(_notifNum)];
_notifNum++;
}];
[[NSRunLoop currentRunLoop] addTimer:_timer forMode:NSRunLoopCommonModes];
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section{
return _arr.count;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{
TestTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"1111"];
if (!cell) {
cell = [[TestTableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:@"1111"];
}
cell.startTime = [_arr[indexPath.row] integerValue];
return cell;
}
cell:
@implementation TestTableViewCell{
NSInteger _num;
}
- (instancetype)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier{
if (self = [super initWithStyle:style reuseIdentifier:reuseIdentifier]) {
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(haha:) name:@"NSNotification" object:nil];
}
return self;
}
- (void)haha:(NSNotification *)noti{
_num = [noti.object integerValue];
self.startTime = _startTime;
}
- (void)setStartTime:(NSInteger)startTime{
_startTime = startTime;
if (_startTime - _num > 0) {
self.textLabel.text = @(_startTime - _num).description;
}else{
self.textLabel.text = @"停止";
}
}