当我们reloadData的时候,会刷新UITableView,随后会进入一系列UITableViewDataSource和UITableViewDelegate的回调,其中有些是和reloadData同步发生的,有些则是异步发生的。
- (void)reload{
NSLog(@"reloadData 之前");
self.array = @[@"1",@"2"];
[self.tableview reloadData];
NSLog(@"reloadData 之后");
}
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView{
NSLog(@"numberOfSectionsInTableView");
return 1;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section{
NSLog(@"numberOfRowsInSection");
return self.array.count;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{
NSLog(@"cellForRowAtIndexPath");
UITableViewCell *cell = [UITableViewCell new];
cell.textLabel.text = self.array[indexPath.row];
return cell;;
}
执行reload方法的日志顺序如下:
reloadData 之前
numberOfSectionsInTableView
numberOfRowsInSection
reloadData 之后
cellForRowAtIndexPath
cellForRowAtIndexPath
从日志顺序可以看出,[self.tableview reloadData]执行时,内部调用了numberOfSectionsInTableView和numberOfRowsInSection两个代理方法;当[self.tableview reloadData]执行完成后,在某个时间调用了cellForRowAtIndexPath代理方法。由此可以得出cellForRowAtIndexPath是异步执行的,当reloadData执行完成后,才会执行cellForRowAtIndexPath。这就是cellForRowAtIndexPath经常出现数组越界的根源。
在实际业务开发中,当接口返回数据后,会调用tableView的reloadData方法刷新列表,更新数据。当reloadData执行完成后,numberOfSectionsInTableView和numberOfRowsInSection这两个代理方法已经执行完毕,tableView的 section 和row已经确定,如果此时数据array在cellForRowAtIndexPath执行之前被修改了,如果长度比之前小,就会出现数组越界的情况。
在numberOfRowsInSection打断点,然后执行[self.tableview reloadData],调用堆栈如下图:
- [UITableView reloadData]
- [UITableView noteNumberOfRowsChanged]
- [UITableViewRowData numberOfRows]
- [UISectionRowData refreshWithSection:tableView:tableViewRowData:]
- [UITableView _numberOfRowsInSection:]
- numberOfRowsInSection
结合上面的打印顺序,可以看到当执行[UITableView reloadData]时,会立马调用numberOfRowsInSection更新rows
在cellForRowAtIndexPath打断点,调用堆栈如下
- [CALayer layoutSublayers]
- [UIView(CALayerDelegate) layoutSublayersOfLayer:]
- [UITableView layoutSubviews]
- [UITableView _updateVisibleCellsNow:]
- [UITableView _createPreparedCellForGlobalRow:withIndexPath:willDisplay:]
- cellForRowAtIndexPath
通过调用堆栈看到,cellForRowAtIndexPath数据源方法是在layoutSublayers后调用的,而layoutSublayers是下一个runloop执行的,所以可以得到cellForRowAtIndexPath是异步执行,并且在下一个runloop中执行。
如果在reloadData之后删除一个数据,会出现数组越界crash,代码如下
- (void)reload{
NSLog(@"reloadData 之前");
self.array = @[@"1",@"2"];
[self.tableview reloadData];
self.array = @[@"1"];
NSLog(@"reloadData 之后");
}
[图片上传中...(1618315930883.jpg-b42cf2-1618315949497-0)]
通过上面的堆栈可以分析出,如果在reloadData之后,立马通知系统调用layoutSublayers,则不会出现数组越界
- 1.通过layoutIfNeeded,layoutIfNeeded的作用:立即执行layoutSubviews ,在当前函数栈中
- (void)reload{
NSLog(@"reloadData 之前");
self.array = @[@"1",@"2"];
[self.tableview reloadData];
[self.view layoutIfNeeded];
self.array = @[@"1"];
NSLog(@"reloadData 之后");
}
可以看到在reload中就执行了cellForRowAtIndexPath,不会出现crash。
- 2.[self.tableview visibleCells] 也可以让cellForRowAtIndexPath在当前函数中执行