cell的复用机制导致的倒计时问题解决

  最近项目中用到了tableView的多个cell倒计时系统问题,本觉得很简单的一个事,一做发现还没这么简单,就此记录。

  下面方法模拟网络请求返回数据。

  按照常规思路,根据网络请求返回remainTime,封装模型,存到数组中,再在表格代理方法中赋值给cell

  cell中根据传入模型中的remainTime属性,开启定时器每隔1s调用如下方法

  程序一运行发现问题:每当表格滚动时,表格代理方法cellForRowAtIndexPath会不断重复调用,从数组中取得模型赋值给cell,而模型中的remainTime是固定的,于是倒计时系统不断重复开始倒计时。

​  发现问题点,开始着手解决。开始想到的是方法是在控制器中也开启一套定时器系统,当服务器数据remainTime返回时,将其中remainTime大于0s的数据保存在一个字典中,对所有键值对开始倒计时。

  下面方法模拟网络数据返回,对所有remainTime大于0的字段保存到字典self.timerDic中

  控制器中,定时器每隔1s调用方法,对remainTime减一后再覆盖掉原本键值对
 
 
 
  于是,字典self.timerDi中所有键值对的value每隔1s递减1,直到最后value都变为0。在cellForRowAtIndexPath方法中插入如下
 
 

  cell属性model中的remainTime字段从这个一直变化的self.timerDic字典中取值,于是滚动视图时cell获取到的就不是一个固定的remainTime,效果如下

​​

​  此时已经解决了表格滚动时倒计时重复计时问题,但可以看到多次滚动后会造成如上显示错误,这是由于控制器和cell两套定时系统时差而引起的,具体后面分析。

    此路貌似不通,于是我想到了KVO,让cell监听控制器中remainTime的数值变化​

 这方法还真走效,在cell的observeValueForKeyPath中确实能监听到remainTime的数值变化,数值也异常正确,但同时一个重大问题也产生了,由于cell的复用,cell上所显示的倒计时系统相互错乱,试了几种方法都无法解决,放弃。
 

  仔细分析上面倒计时时差原因,发现时差产生是由于定时器调用频率导致。举个场景说明:控制器返回数据时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中的生成方法,核心代码如下:

 
  刚开始学swift,因工作还在用OC,只能平时练练手了,github上代码为swift版
  github地址:https://github.com/zhangmaliang/CountDown
 
 
发现上面方法有误。实测更正如下:
tableViewController:

@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 = @"停止";

    }

}

原文地址:https://www.cnblogs.com/zhangmaliang/p/5102518.html