分析原生TableView数据加载过程
当tableView走reloadData方法的时候,走的方法顺序如下:
- numberOfSectionsInTableView
- numberOfRowsInSection
- heightForRowAtIndexPath
- cellForRowAtIndexPath
- heightForRowAtIndexPath
会先实现一个tableView中有几组数据,然后每组有多少行,然后再每组默认的高度,然后再加载cell,最后再重新返回一次准确的高度,并且加载的也是当前页面的cell,超过页面的不会加载。
其中当第一次运行tableView的时候,即一开始进来,tableView展示数据,而不是走reloadData方法的时候,上面图一中红色框的方法会多走两遍,而且就算是有多组,每组也会多走两遍。
如果每个cell的高度不一,需要动态计算cell的高度,我们可以第一次算出来过后将cell的高度保存,避免每次滑动tableView,或者reloadData的时候都再计算一次cell的高度。
模仿tableView
由系统的可知,要实现一个tableView,需要继承自UIScrollView,并且定义一个tableView代理和代理中必须实现的方法
#import <UIKit/UIKit.h>
#import "EOCTableViewCell.h"
@class EOCTableView;
@protocol EOCTableViewDelegate <NSObject>
@required
- (NSInteger)eocTableView:(EOCTableView *)tableView numberOfRowsInSection:(NSInteger)section;
- (CGFloat)eocTableView:(EOCTableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath;
- (EOCTableViewCell *)eocTableView:(EOCTableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath;
@end
@interface EOCTableView : UIScrollView{
NSMutableArray *_rowModeAry; //装cell的起始Y值和高度的模型数组
NSMutableArray *_reuseCellPoolArr; //重用池
NSMutableDictionary *_visibleCellPoolDict; //现有池
}
@property (nonatomic, weak)id<EOCTableViewDelegate> eocDelegate;
// 从重用池拿cell的方法
- (EOCTableViewCell *)dequeueReusableCellWithIdentifier:(NSString*)identifier;
- (void)reloadData;
@end
还需要自定义一个默认有label的tableViewCell,而且还必须要绑定identifier属性
#import <UIKit/UIKit.h>
@interface EOCTableViewCell : UIView
- (id)initWithIdentifier:(NSString*)identifier;
@property (nonatomic, strong)UILabel *textLabel;
@property (nonatomic, strong)NSString *identifier;
@end
#import "EOCTableViewCell.h"
@implementation EOCTableViewCell
- (id)initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
if (self) {
_textLabel = [[UILabel alloc] init];
[self addSubview:_textLabel];
}
return self;
}
- (id)initWithIdentifier:(NSString*)identifier
{
self = [super init];
if (self) {
_identifier = identifier;
}
return self;
}
- (void)layoutSubviews
{
[super layoutSubviews];
[_textLabel setFrame:CGRectMake(15, 0, self.frame.size.width-15, self.frame.size.height)];
}
@end
还需要一个用于保存每个cell的identifier和起始Y坐标和cell高度的模型Model
#import <Foundation/Foundation.h>
@interface RowInfoModel : NSObject
@property (nonatomic, strong)NSString *identifi;
@property (nonatomic, assign)float originY;
@property (nonatomic, assign)float sizeHeight;
@end
.m文件中不做什么操作
reloadData方法的内部实现
- (void)reloadData{
[self countRowPosition];// 数据准备
[self setNeedsLayout];// setNeedsLayout不会在当前事件循环操作,会放在下一个事件循环
}
// 保存postion数据
- (void)countRowPosition{
// 先清空原数据,避免叠加
[_rowModeAry removeAllObjects];
float addUpHigh = 0;
for (int i = 0; i < [_eocDelegate eocTableView:self numberOfRowsInSection:0]; i++) {
NSIndexPath *path = [NSIndexPath indexPathForRow:i inSection:0];
//有多少cell就要先走多少遍heightForRowAtIndexPath这个方法
float cellHigh = [_eocDelegate eocTableView:self heightForRowAtIndexPath:path];
RowInfoModel *rowModel = [[RowInfoModel alloc] init];
rowModel.originY = addUpHigh; //记录每个cell的初始Y值
rowModel.sizeHeight = cellHigh; //记录每个cell的高度
[_rowModeAry addObject:rowModel]; //添加到cell的数据数组中
addUpHigh += cellHigh; // 累积得到所有cell加在一起的高度,即contentSize
}
//重新设置tableView的contentSize
[self setContentSize:CGSizeMake(self.frame.size.width, addUpHigh)];
}
setNeedsLayout方法并不是同步的,它不会在本次的事件循环中进行操作,它会在下次的事件循环中进行操作,所以当你刚刚调用了tableView的reloadData方法后,去获取tableView的contentSize之类的会往往不准确,从而造成一些BUG。
setNeedsLayout中layoutSubviews中进行的操作
- (void)layoutSubviews{
[super layoutSubviews];
//contentOffset的y值小于0就取0,从0开始添加cell
float startY = (self.contentOffset.y < 0)?0:self.contentOffset.y;
// (startY + self.frame.size.height)>self.contentSize.height 即内容没有超过屏幕,所以取内容的高
float endY = (startY + self.frame.size.height)>self.contentSize.height?self.contentSize.height:startY + self.frame.size.height;
RowInfoModel *rowStartModel = [RowInfoModel new];
rowStartModel.originY = startY;
RowInfoModel *rowEndModel = [RowInfoModel new];
rowEndModel.originY = endY;
//二分查找法找出可视区域内的第一个和最后一个cell
NSInteger startIndex = [self binarySearchFromAry:_rowModeAry object:rowStartModel];
NSInteger endIndex = [self binarySearchFromAry:_rowModeAry object:rowEndModel];
//NSRange visibleCellRange = [self visibleRowRange];
NSRange visibleCellRange = NSMakeRange(startIndex, endIndex-startIndex+1);
// 系统的二分查找法
startIndex = [_rowModeAry indexOfObject:rowStartModel inSortedRange:NSMakeRange(0, _rowModeAry.count-1) options:NSBinarySearchingInsertionIndex usingComparator:^NSComparisonResult(id _Nonnull obj1, id _Nonnull obj2) {
RowInfoModel *rowStartModel = obj1;
RowInfoModel *rowEndModel = obj2;
if (rowStartModel.originY > rowEndModel.originY &&rowStartModel.originY > rowEndModel.originY + rowEndModel.sizeHeight) {
return NSOrderedSame;
}else if (rowStartModel.originY < rowEndModel.originY) {
return NSOrderedDescending;
}else{
return NSOrderedAscending;
}
}];
// NSLog(@"%@--%@", NSStringFromRange(visibleCellRange), NSStringFromRange(binaryVisibleCellRange));
//加载cell界面
/*
1,不在可视界面的cell,remove --> 重用池
2. 加载即将出现的cell。 首先判断重用池里面是否有cell,有就重用,没有就申请新的
重用池(保存不在界面上的) 现有池(保存在界面上的cell)
*/
//visibleCellRange.location 即是startIndex visibleCellRange.location + visibleCellRange.length 即是endIndex
for (NSInteger i = visibleCellRange.location; i < visibleCellRange.location + visibleCellRange.length; i++) {
//如果cell已经在可视界面中,就不需要再重新加载了
EOCTableViewCell *cell = _visibleCellPoolDict[@(i)];
if (!cell) {
NSIndexPath *indexPath = [NSIndexPath indexPathForRow:i inSection:0];
cell = [_eocDelegate eocTableView:self cellForRowAtIndexPath:indexPath];
//保存到现有池中
[_visibleCellPoolDict setObject:cell forKey:@(i)];
//从复用池中移除,一定要先保存在现有池中再remove,不然很有可能就被释放掉了
[_reuseCellPoolArr removeObject:cell];
}
RowInfoModel *rowInfoM = _rowModeAry[i];
cell.frame = CGRectMake(0, rowInfoM.originY, self.frame.size.width, rowInfoM.sizeHeight);
//cell的父视图还不存在,即cell还没有被添加,这时就应该添加cell
if (![cell superview]) {
[self addSubview:cell];
}
}
// 移除不在可见试图上的cell
NSArray *vCellKeyAry = [_visibleCellPoolDict allKeys];
for (int i = 0; i < vCellKeyAry.count; i++) {
// 哪些不在界面上移除到 重用池
NSInteger indexKey = [vCellKeyAry[i] integerValue];
if (indexKey < visibleCellRange.location || indexKey > visibleCellRange.location + visibleCellRange.length) { //不在可见cell范围内
//添加到复用池
[_reuseCellPoolArr addObject:_visibleCellPoolDict[@(indexKey)]];
//从现有池中移走
[_visibleCellPoolDict removeObjectForKey:@(indexKey)];
}
}
}
dequeueReusableCellWithIdentifier方法的内部实现
- (EOCTableViewCell *)dequeueReusableCellWithIdentifier:(NSString*)identifier{
//遍历复用池数组,查找是否有identifier相关的cell
for (int i = 0; i < _reuseCellPoolArr.count; i++) {
EOCTableViewCell *cell = _reuseCellPoolArr[i];
if ([cell.identifier isEqual:identifier]) {
return cell;
}
}
return nil;
}
即是在复用池中查找有相同identifier的cell,如果有就取出来重用,没有就返回nil,下一步进行创建
二分查找算当前可见区域的cell(第一个和最后一个cell是多少)
二分法需要一个有序的数组,而且查找的特别快,适合数量比较大的查找
/*
二分算法 数组有序
3
1 2 3 4 5 6 7 8 9 10 11 12
123 456
1 - 1024 1024 = 2*8
1-512 512-1024
1-256 256-512
*/
/* a[1024] x = 0 y = 1024 v = 123 a是长度为1024的数组,x是最小值,y是最大值,v是中间值*/
// C语言的写法
int binarySearch(int * a, int min, int max, int v)//半开区间[x,y)
{
int mid;
while(min < max)
{
mid = min + (max-min)/2; // mid = 512
if(v == a[mid]) {
return mid;//找到了
}
else if(v < a[mid]){
max = mid;//在左边
}
else {
min = mid+1;//在右边
}
}
return -1;
}
//OC的写法
- (NSInteger)binarySearchFromAry:(NSArray*)arry object:(RowInfoModel*)targetModel{
NSInteger min = 0;
NSInteger max = arry.count -1;
NSInteger mid;
while (min < max) {
mid = min + (max - min)/2;
RowInfoModel *midModel = arry[mid];
if (targetModel.originY >= midModel.originY && targetModel.originY < midModel.originY + midModel.sizeHeight) {
return mid;
}else if(targetModel.originY < midModel.originY){ // 在左边
max = mid;
if (max - min == 1) { // 在左边的时候,只剩两个的时候,返回小的那个
return min;
}
}else{ // 在右边
min = mid;
if (max - min == 1) { // 在右边的时候,只剩两个的时候,返回大的那个
return max;
}
}
}
return -1;
}
最后附上整个tableView的.m文件
#import "EOCTableView.h"
#import "EOCTableViewCell.h"
#import "RowInfoModel.h"
@implementation EOCTableView{
}
- (instancetype)initWithFrame:(CGRect)frame{
self = [super initWithFrame:frame];
if (self) {
_rowModeAry = [NSMutableArray array];
_reuseCellPoolArr = [NSMutableArray array];
_visibleCellPoolDict = [NSMutableDictionary dictionary];
}
return self;
}
/*
先配置好数据
再配置试图
先计算所有cell高度
再计算每个cell位置Y
*/
- (void)layoutSubviews{
[super layoutSubviews];
//contentOffset的y值小于0就取0,从0开始添加cell
float startY = (self.contentOffset.y < 0)?0:self.contentOffset.y;
// (startY + self.frame.size.height)>self.contentSize.height 即内容没有超过屏幕,所以取内容的高
float endY = (startY + self.frame.size.height)>self.contentSize.height?self.contentSize.height:startY + self.frame.size.height;
RowInfoModel *rowStartModel = [RowInfoModel new];
rowStartModel.originY = startY;
RowInfoModel *rowEndModel = [RowInfoModel new];
rowEndModel.originY = endY;
//二分查找法找出可视区域内的第一个和最后一个cell
NSInteger startIndex = [self binarySearchFromAry:_rowModeAry object:rowStartModel];
NSInteger endIndex = [self binarySearchFromAry:_rowModeAry object:rowEndModel];
//NSRange visibleCellRange = [self visibleRowRange];
NSRange visibleCellRange = NSMakeRange(startIndex, endIndex-startIndex+1);
// 系统的二分查找法
startIndex = [_rowModeAry indexOfObject:rowStartModel inSortedRange:NSMakeRange(0, _rowModeAry.count-1) options:NSBinarySearchingInsertionIndex usingComparator:^NSComparisonResult(id _Nonnull obj1, id _Nonnull obj2) {
RowInfoModel *rowStartModel = obj1;
RowInfoModel *rowEndModel = obj2;
if (rowStartModel.originY > rowEndModel.originY &&rowStartModel.originY > rowEndModel.originY + rowEndModel.sizeHeight) {
return NSOrderedSame;
}else if (rowStartModel.originY < rowEndModel.originY) {
return NSOrderedDescending;
}else{
return NSOrderedAscending;
}
}];
// NSLog(@"%@--%@", NSStringFromRange(visibleCellRange), NSStringFromRange(binaryVisibleCellRange));
//加载cell界面
/*
1,不在可视界面的cell,remove --> 重用池
2. 加载即将出现的cell。 首先判断重用池里面是否有cell,有就重用,没有就申请新的
重用池(保存不在界面上的) 现有池(保存在界面上的cell)
*/
//visibleCellRange.location 即是startIndex visibleCellRange.location + visibleCellRange.length 即是endIndex
for (NSInteger i = visibleCellRange.location; i < visibleCellRange.location + visibleCellRange.length; i++) {
//如果cell已经在可视界面中,就不需要再重新加载了
EOCTableViewCell *cell = _visibleCellPoolDict[@(i)];
if (!cell) {
NSIndexPath *indexPath = [NSIndexPath indexPathForRow:i inSection:0];
cell = [_eocDelegate eocTableView:self cellForRowAtIndexPath:indexPath];
//保存到现有池中
[_visibleCellPoolDict setObject:cell forKey:@(i)];
//从复用池中移除,一定要先保存在现有池中再remove,不然很有可能就被释放掉了
[_reuseCellPoolArr removeObject:cell];
}
RowInfoModel *rowInfoM = _rowModeAry[i];
cell.frame = CGRectMake(0, rowInfoM.originY, self.frame.size.width, rowInfoM.sizeHeight);
//cell的父视图还不存在,即cell还没有被添加,这时就应该添加cell
if (![cell superview]) {
[self addSubview:cell];
}
}
// 移除不在可见试图上的cell
NSArray *vCellKeyAry = [_visibleCellPoolDict allKeys];
for (int i = 0; i < vCellKeyAry.count; i++) {
// 哪些不在界面上移除到 重用池
NSInteger indexKey = [vCellKeyAry[i] integerValue];
if (indexKey < visibleCellRange.location || indexKey > visibleCellRange.location + visibleCellRange.length) { //不在可见cell范围内
//添加到复用池
[_reuseCellPoolArr addObject:_visibleCellPoolDict[@(indexKey)]];
//从现有池中移走
[_visibleCellPoolDict removeObjectForKey:@(indexKey)];
}
}
}
- (EOCTableViewCell *)dequeueReusableCellWithIdentifier:(NSString*)identifier{
//遍历复用池数组,查找是否有identifier相关的cell
for (int i = 0; i < _reuseCellPoolArr.count; i++) {
EOCTableViewCell *cell = _reuseCellPoolArr[i];
if ([cell.identifier isEqual:identifier]) {
return cell;
}
}
return nil;
}
/* section = 0*/
- (void)reloadData{
[self countRowPosition];// 数据准备
[self setNeedsLayout];// setNeedsLayout不会在当前事件循环操作,会放在下一个事件循环
}
// 保存postion数据
- (void)countRowPosition{
// 先清空原数据,避免叠加
[_rowModeAry removeAllObjects];
float addUpHigh = 0;
for (int i = 0; i < [_eocDelegate eocTableView:self numberOfRowsInSection:0]; i++) {
NSIndexPath *path = [NSIndexPath indexPathForRow:i inSection:0];
//有多少cell就要先走多少遍heightForRowAtIndexPath这个方法
float cellHigh = [_eocDelegate eocTableView:self heightForRowAtIndexPath:path];
RowInfoModel *rowModel = [[RowInfoModel alloc] init];
rowModel.originY = addUpHigh; //记录每个cell的初始Y值
rowModel.sizeHeight = cellHigh; //记录每个cell的高度
[_rowModeAry addObject:rowModel]; //添加到cell的数据数组中
addUpHigh += cellHigh; // 累积得到所有cell加在一起的高度,即contentSize
}
//重新设置tableView的contentSize
[self setContentSize:CGSizeMake(self.frame.size.width, addUpHigh)];
}
// 计算当前可见区域的 cell
- (NSRange)visibleRowRange{
//contentOffset的y值小于0就取0,从0开始添加cell
float startY = (self.contentOffset.y < 0)?0:self.contentOffset.y;
// (startY + self.frame.size.height)>self.contentSize.height 即内容没有超过屏幕,所以取内容的高
float endY = (startY + self.frame.size.height)>self.contentSize.height?self.contentSize.height:startY + self.frame.size.height;
//开始索引
NSInteger startIndex = -1;
//结束索引
NSInteger endIndex = - 1;
for (int i = 0; i < _rowModeAry.count; i++) {
RowInfoModel *rowInfoM = _rowModeAry[i];
if (startIndex == -1) {
//找到可见区域内的第一个cell
if (startY >= rowInfoM.originY && startY < rowInfoM.originY + rowInfoM.sizeHeight) {
startIndex = i;
}
}else{
//找到可见区域内的最后一个cell
if (endY >= rowInfoM.originY && endY < rowInfoM.originY + rowInfoM.sizeHeight) {
endIndex = i;
}
}
}
// 需要 +1 是因为startIndex是从0开始的
return NSMakeRange(startIndex, endIndex-startIndex+1);
}
/*
二分算法 数组有序
3
1 2 3 4 5 6 7 8 9 10 11 12
123 456
1 - 1024 1024 = 2*8
1-512 512-1024
1-256 256-512
*/
/* a[1024] x = 0 y = 1024 v = 123 a是长度为1024的数组,x是最小值,y是最大值,v是中间值*/
int binarySearch(int * a, int min, int max, int v)//半开区间[x,y)
{
int mid;
while(min < max)
{
mid = min + (max-min)/2; // mid = 512
if(v == a[mid]) {
return mid;//找到了
}
else if(v < a[mid]){
max = mid;//在左边
}
else {
min = mid+1;//在右边
}
}
return -1;
}
- (NSInteger)binarySearchFromAry:(NSArray*)arry object:(RowInfoModel*)targetModel{
NSInteger min = 0;
NSInteger max = arry.count -1;
NSInteger mid;
while (min < max) {
mid = min + (max - min)/2;
RowInfoModel *midModel = arry[mid];
if (targetModel.originY >= midModel.originY && targetModel.originY < midModel.originY + midModel.sizeHeight) {
return mid;
}else if(targetModel.originY < midModel.originY){ // 在左边
max = mid;
if (max - min == 1) { // 在左边的时候,只剩两个的时候,返回小的那个
return min;
}
}else{ // 在右边
min = mid;
if (max - min == 1) { // 在右边的时候,只剩两个的时候,返回大的那个
return max;
}
}
}
return -1;
}
@end