最近項目中用到了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 = @"停止";
}
}