分析原生TableView數(shù)據(jù)加載過程

當tableView走reloadData方法的時候,走的方法順序如下:
- numberOfSectionsInTableView
- numberOfRowsInSection
- heightForRowAtIndexPath
- cellForRowAtIndexPath
- heightForRowAtIndexPath
會先實現(xiàn)一個tableView中有幾組數(shù)據(jù),然后每組有多少行,然后再每組默認的高度,然后再加載cell,最后再重新返回一次準確的高度,并且加載的也是當前頁面的cell,超過頁面的不會加載。
其中當?shù)谝淮芜\行tableView的時候,即一開始進來,tableView展示數(shù)據(jù),而不是走reloadData方法的時候,上面圖一中紅色框的方法會多走兩遍,而且就算是有多組,每組也會多走兩遍。
如果每個cell的高度不一,需要動態(tài)計算cell的高度,我們可以第一次算出來過后將cell的高度保存,避免每次滑動tableView,或者reloadData的時候都再計算一次cell的高度。
模仿tableView
由系統(tǒng)的可知,要實現(xiàn)一個tableView,需要繼承自UIScrollView,并且定義一個tableView代理和代理中必須實現(xiàn)的方法
#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值和高度的模型數(shù)組
NSMutableArray *_reuseCellPoolArr; //重用池
NSMutableDictionary *_visibleCellPoolDict; //現(xiàn)有池
}
@property (nonatomic, weak)id<EOCTableViewDelegate> eocDelegate;
// 從重用池拿cell的方法
- (EOCTableViewCell *)dequeueReusableCellWithIdentifier:(NSString*)identifier;
- (void)reloadData;
@end
還需要自定義一個默認有l(wèi)abel的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方法的內(nèi)部實現(xiàn)
- (void)reloadData{
[self countRowPosition];// 數(shù)據(jù)準備
[self setNeedsLayout];// setNeedsLayout不會在當前事件循環(huán)操作,會放在下一個事件循環(huán)
}
// 保存postion數(shù)據(jù)
- (void)countRowPosition{
// 先清空原數(shù)據(jù),避免疊加
[_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的數(shù)據(jù)數(shù)組中
addUpHigh += cellHigh; // 累積得到所有cell加在一起的高度,即contentSize
}
//重新設(shè)置tableView的contentSize
[self setContentSize:CGSizeMake(self.frame.size.width, addUpHigh)];
}
setNeedsLayout方法并不是同步的,它不會在本次的事件循環(huán)中進行操作,它會在下次的事件循環(huán)中進行操作,所以當你剛剛調(diào)用了tableView的reloadData方法后,去獲取tableView的contentSize之類的會往往不準確,從而造成一些BUG。
setNeedsLayout中l(wèi)ayoutSubviews中進行的操作
- (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 即內(nèi)容沒有超過屏幕,所以取內(nèi)容的高
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;
//二分查找法找出可視區(qū)域內(nèi)的第一個和最后一個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);
// 系統(tǒng)的二分查找法
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. 加載即將出現(xiàn)的cell。 首先判斷重用池里面是否有cell,有就重用,沒有就申請新的
重用池(保存不在界面上的) 現(xiàn)有池(保存在界面上的cell)
*/
//visibleCellRange.location 即是startIndex visibleCellRange.location + visibleCellRange.length 即是endIndex
for (NSInteger i = visibleCellRange.location; i < visibleCellRange.location + visibleCellRange.length; i++) {
//如果cell已經(jīng)在可視界面中,就不需要再重新加載了
EOCTableViewCell *cell = _visibleCellPoolDict[@(i)];
if (!cell) {
NSIndexPath *indexPath = [NSIndexPath indexPathForRow:i inSection:0];
cell = [_eocDelegate eocTableView:self cellForRowAtIndexPath:indexPath];
//保存到現(xiàn)有池中
[_visibleCellPoolDict setObject:cell forKey:@(i)];
//從復(fù)用池中移除,一定要先保存在現(xiàn)有池中再remove,不然很有可能就被釋放掉了
[_reuseCellPoolArr removeObject:cell];
}
RowInfoModel *rowInfoM = _rowModeAry[i];
cell.frame = CGRectMake(0, rowInfoM.originY, self.frame.size.width, rowInfoM.sizeHeight);
//cell的父視圖還不存在,即cell還沒有被添加,這時就應(yīng)該添加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范圍內(nèi)
//添加到復(fù)用池
[_reuseCellPoolArr addObject:_visibleCellPoolDict[@(indexKey)]];
//從現(xiàn)有池中移走
[_visibleCellPoolDict removeObjectForKey:@(indexKey)];
}
}
}
dequeueReusableCellWithIdentifier方法的內(nèi)部實現(xiàn)
- (EOCTableViewCell *)dequeueReusableCellWithIdentifier:(NSString*)identifier{
//遍歷復(fù)用池數(shù)組,查找是否有identifier相關(guān)的cell
for (int i = 0; i < _reuseCellPoolArr.count; i++) {
EOCTableViewCell *cell = _reuseCellPoolArr[i];
if ([cell.identifier isEqual:identifier]) {
return cell;
}
}
return nil;
}
即是在復(fù)用池中查找有相同identifier的cell,如果有就取出來重用,沒有就返回nil,下一步進行創(chuàng)建
二分查找算當前可見區(qū)域的cell(第一個和最后一個cell是多少)
二分法需要一個有序的數(shù)組,而且查找的特別快,適合數(shù)量比較大的查找
/*
二分算法 數(shù)組有序
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的數(shù)組,x是最小值,y是最大值,v是中間值*/
// C語言的寫法
int binarySearch(int * a, int min, int max, int v)//半開區(qū)間[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;
}
/*
先配置好數(shù)據(jù)
再配置試圖
先計算所有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 即內(nèi)容沒有超過屏幕,所以取內(nèi)容的高
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;
//二分查找法找出可視區(qū)域內(nèi)的第一個和最后一個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);
// 系統(tǒng)的二分查找法
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. 加載即將出現(xiàn)的cell。 首先判斷重用池里面是否有cell,有就重用,沒有就申請新的
重用池(保存不在界面上的) 現(xiàn)有池(保存在界面上的cell)
*/
//visibleCellRange.location 即是startIndex visibleCellRange.location + visibleCellRange.length 即是endIndex
for (NSInteger i = visibleCellRange.location; i < visibleCellRange.location + visibleCellRange.length; i++) {
//如果cell已經(jīng)在可視界面中,就不需要再重新加載了
EOCTableViewCell *cell = _visibleCellPoolDict[@(i)];
if (!cell) {
NSIndexPath *indexPath = [NSIndexPath indexPathForRow:i inSection:0];
cell = [_eocDelegate eocTableView:self cellForRowAtIndexPath:indexPath];
//保存到現(xiàn)有池中
[_visibleCellPoolDict setObject:cell forKey:@(i)];
//從復(fù)用池中移除,一定要先保存在現(xiàn)有池中再remove,不然很有可能就被釋放掉了
[_reuseCellPoolArr removeObject:cell];
}
RowInfoModel *rowInfoM = _rowModeAry[i];
cell.frame = CGRectMake(0, rowInfoM.originY, self.frame.size.width, rowInfoM.sizeHeight);
//cell的父視圖還不存在,即cell還沒有被添加,這時就應(yīng)該添加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范圍內(nèi)
//添加到復(fù)用池
[_reuseCellPoolArr addObject:_visibleCellPoolDict[@(indexKey)]];
//從現(xiàn)有池中移走
[_visibleCellPoolDict removeObjectForKey:@(indexKey)];
}
}
}
- (EOCTableViewCell *)dequeueReusableCellWithIdentifier:(NSString*)identifier{
//遍歷復(fù)用池數(shù)組,查找是否有identifier相關(guān)的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];// 數(shù)據(jù)準備
[self setNeedsLayout];// setNeedsLayout不會在當前事件循環(huán)操作,會放在下一個事件循環(huán)
}
// 保存postion數(shù)據(jù)
- (void)countRowPosition{
// 先清空原數(shù)據(jù),避免疊加
[_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的數(shù)據(jù)數(shù)組中
addUpHigh += cellHigh; // 累積得到所有cell加在一起的高度,即contentSize
}
//重新設(shè)置tableView的contentSize
[self setContentSize:CGSizeMake(self.frame.size.width, addUpHigh)];
}
// 計算當前可見區(qū)域的 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 即內(nèi)容沒有超過屏幕,所以取內(nèi)容的高
float endY = (startY + self.frame.size.height)>self.contentSize.height?self.contentSize.height:startY + self.frame.size.height;
//開始索引
NSInteger startIndex = -1;
//結(jié)束索引
NSInteger endIndex = - 1;
for (int i = 0; i < _rowModeAry.count; i++) {
RowInfoModel *rowInfoM = _rowModeAry[i];
if (startIndex == -1) {
//找到可見區(qū)域內(nèi)的第一個cell
if (startY >= rowInfoM.originY && startY < rowInfoM.originY + rowInfoM.sizeHeight) {
startIndex = i;
}
}else{
//找到可見區(qū)域內(nèi)的最后一個cell
if (endY >= rowInfoM.originY && endY < rowInfoM.originY + rowInfoM.sizeHeight) {
endIndex = i;
}
}
}
// 需要 +1 是因為startIndex是從0開始的
return NSMakeRange(startIndex, endIndex-startIndex+1);
}
/*
二分算法 數(shù)組有序
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的數(shù)組,x是最小值,y是最大值,v是中間值*/
int binarySearch(int * a, int min, int max, int v)//半開區(qū)間[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