阿峥教你实现UITableView循环利用

前言

大家都知道UITableView,最经典在于循环利用,这里我自己模仿UITableView循环利用,写了一套自己的TableView实现方案,希望大家看了我的文章,循环利用思想有显著提升。

效果如图:

304825-6ff3a08ced53b208.gif

研究UITableView底层实现

1.系统UITabelView的简单使用,这里就不考虑分组了,默认为1组。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// 返回第section组有多少行
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
     NSLog(@ "%s" ,__func__);
     return  10;
}
 
// 返回每一行cell的样子
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
     NSLog(@ "%s" ,__func__);
     static NSString *ID = @ "cell" ;
     UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:ID];
     
     if  (cell == nil) {
         
         cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:ID];
     }
     
     cell.textLabel.text = [NSString stringWithFormat:@ "%ld" ,indexPath.row];
     
     return  cell;
}
// 返回每行cell的高度
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
     NSLog(@ "%s--%@" ,__func__,indexPath);
     return  100;
}

2.验证UITabelView的实现机制。

如图打印结果:

03.png

分析:底层先获取有多少cell(10个),在获取每个cell的高度,返回高度的方法一开始调用10次。

目的:确定tableView的滚动范围,一开始计算所有cell的frame,就能计算下tableView的滚动范围。

分析:tableView:cellForRowAtIndexPath:方法什么时候调用。

打印验证,如图:

04.png

一开始调用了7次,因为一开始屏幕最多显示7个cell

目的:一开始只加载显示出来的cell,等有新的cell出现的时候会继续调用这个方法加载cell。

3.UITableView循环利用思想

当新的cell出现的时候,首先从缓存池中获取,如果没有获取到,就自己创建cell。

当有cell移除屏幕的时候,把cell放到缓存池中去。

二、自定义UIScroolView,模仿UITableView循环利用

1.提供数据源和代理方法,命名和UITableView一致。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@class YZTableView;
@protocol YZTableViewDataSource
 
@required
 
// 返回有多少行cell
- (NSInteger)tableView:(YZTableView *)tableView numberOfRowsInSection:(NSInteger)section;
 
 
// 返回每行cell长什么样子
- (UITableViewCell *)tableView:(YZTableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath;
 
@end
 
@protocol YZTableViewDelegate
 
// 返回每行cell有多高
- (CGFloat)tableView:(YZTableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath;
 
@end

2.提供代理和数据源属性

1
2
3
4
5
6
7
@interface YZTableView : UIScrollView
 
@property (nonatomic, weak) id dataSource;
 
@property (nonatomic, weak) id delegate;
 
@end

警告:

05.png

解决,在YZTableView.m的实现中声明。

06.png

原因:有人会问为什么我要定义同名的delegate属性,我主要想模仿系统的tableView,系统tableView也有同名的属性。

思路:这样做,外界在使用设置我的tableView的delegate,就必须遵守的我的代理协议,而不是UIScrollView的代理协议。

3.提供刷新方法reloadData,因为tableView通过这个刷新tableView。

1
2
3
4
5
6
7
8
9
10
@interface YZTableView : UIScrollView
 
@property (nonatomic, weak) id dataSource;
 
@property (nonatomic, weak) id delegate;
 
// 刷新tableView
- (void)reloadData;
 
@end

4.实现reloadData方法,刷新表格

回顾系统如何刷新tableView

1).先获取有多少cell,在获取每个cell的高度。因此应该是先计算出每个cell的frame.

2).然后再判断当前有多少cell显示在屏幕上,就加载多少

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
// 刷新tableView
- (void)reloadData
{
     // 这里不考虑多组,假设tableView默认只有一组。
     
     // 先获取总共有多少cell
     NSInteger rows = [self.dataSource tableView:self numberOfRowsInSection:0];
     
     // 遍历所有cell的高度,计算每行cell的frame
     CGRect cellF;
     CGFloat cellX = 0;
     CGFloat cellY = 0;
     CGFloat cellW = self.bounds.size.width;
     CGFloat cellH = 0;
     CGFloat totalH = 0;
     
     for  (int i = 0; i < rows; i++) {
        
         NSIndexPath *indexPath = [NSIndexPath indexPathForRow:i inSection:0];
         // 注意:这里获取的delegate,是UIScrollView中声明的属性
         if  ([self.delegate respondsToSelector:@selector(tableView:heightForRowAtIndexPath:)]) {
             cellH = [self.delegate tableView:self heightForRowAtIndexPath:indexPath];
         } else {
             cellH = 44;
         }
         cellY = i * cellH;
         
         cellF = CGRectMake(cellX, cellY, cellW, cellH);
         
         // 记录每个cell的y值对应的indexPath
         self.indexPathDict[@(cellY)] = indexPath;
         
         // 判断有多少cell显示在屏幕上,只加载显示在屏幕上的cell
         if  ([self isInScreen:cellF]) {  // 当前cell的frame在屏幕上
             // 通过数据源获取cell
             UITableViewCell *cell = [self.dataSource tableView:self cellForRowAtIndexPath:indexPath];
             
             cell.frame = cellF;
             
             [self addSubview:cell];
             
         }
         
         // 添加分割线
         UIView *divideV = [[UIView alloc] initWithFrame:CGRectMake(0, cellY + cellH - 1, cellW, 1)];
         divideV.backgroundColor = [UIColor lightGrayColor];
         divideV.alpha = 0.3;
         [self addSubview:divideV];
         
         // 添加到cell可见数组中
             [self.visibleCells addObject:cell];
         
         // 计算tableView内容总高度
         totalH += cellY + cellH;
     
     }
     
     // 设置tableView的滚动范围
     self.contentSize = CGSizeMake(self.bounds.size.width, totalH);
     
}

5.如何判断cell显示在屏幕上

  • 当tableView内容往下走

01.gif

  • 当tableView内容往上走

02.gif

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 根据cell尺寸判断cell在不在屏幕上
- (BOOL)isInScreen:(CGRect)cellF
{
     // tableView能滚动,因此需要加上偏移量判断
     
     // 当tableView内容往下走,offsetY会一直增加 ,cell的最大y值 < offsetY偏移量   ,cell移除屏幕
     // tableView内容往上走 , offsetY会一直减少,屏幕的最大Y值 <  cell的y值 ,Cell移除屏幕
     // 屏幕最大y值 = 屏幕的高度 + offsetY
     
     // 这里拿屏幕来比较,其实是因为tableView的尺寸我默认等于屏幕的高度,正常应该是tableView的高度。
     // cell在屏幕上, cell的最大y值 > offsetY && cell的y值 < 屏幕的最大Y值(屏幕的高度 + offsetY)
     
     CGFloat offsetY = self.contentOffset.y;
     
     return  CGRectGetMaxY(cellF) > offsetY && cellF.origin.y < self.bounds.size.height + offsetY;
 
     }

6.在滚动的时候,如果有新的cell出现在屏幕上,先从缓存池中取,没有取到,在创建新的cell.

分析:

  • 需要及时监听tableView的滚动,判断下有没有新的cell出现。

  • 大家都会想到scrollViewDidScroll方法,这个方法只要一滚动scrollView就会调用,但是这个方法有个弊端,就是tableView内部需要作为自身的代理,才能监听,这样不好,有时候外界也需要监听滚动,因此自身类最好不要成为自己的代理。(设计思想)

解决:

  • 重写layoutSubviews,判断当前哪些cell显示在屏幕上。

  • 因为只要一滚动,就会修改contentOffset,就会调用layoutSubviews,其实修改contentOffset,内部其实是修改tableView的bounds,而layoutSubviews刚好是父控件尺寸一改就会调用.具体需要了解scrollView底层实现

思路:

判断下,当前tableView内容往上移动,还是往下移动,如何判断,取出显示在屏幕上的第一次cell,当前偏移量 > 第一个cell的y值,往下走。

需要搞个数组记录下,当前有多少cell显示在屏幕上,在一开始的时候记录.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@interface YZTableView ()
 
@property (nonatomic, strong) NSMutableArray *visibleCells;
 
@end
 
@implementation YZTableView
 
@dynamic delegate;
 
- (NSMutableArray *)visibleCells
{
 
     if  (_visibleCells == nil) {
         _visibleCells = [NSMutableArray array];
     }
     return  _visibleCells;
     
}
@end
  • 往下移动

1.如果已经滚动到tableView内容最底部,就不需要判断新的cell,直接返回.

2.需要判断之前显示在屏幕cell有没有移除屏幕

3.只需要判断下当前可见cell数组中第一个cell有没有离开屏幕

4.只需要判断下当前可见cell数组中最后一个cell的下一个cell显没显示在屏幕上即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
   // 判断有没有滚动到最底部
         if  (offsetY + self.bounds.size.height > self.contentSize.height) {
             return ;
         }
         
         // 判断下当前可见cell数组中第一个cell有没有离开屏幕
         if  ([self isInScreen:firstCell.frame] == NO) {  // 如果不在屏幕
             // 从可见cell数组移除
             [self.visibleCells removeObject:firstCell];
             
             // 删除第0个从可见的indexPath
             [self.visibleIndexPaths removeObjectAtIndex:0];
             
             // 添加到缓存池中
             [self.reuserCells addObject:firstCell];
             
             // 移除父控件
             [firstCell removeFromSuperview];
             
         }
         // 判断下当前可见cell数组中最后一个cell的下一个cell显没显示在屏幕上
         // 这里需要计算下一个cell的y值,需要获取对应的cell的高度
         // 而高度需要根据indexPath,从数据源获取
         // 可以数组记录每个可见cell的indexPath的顺序,然后获取对应可见的indexPath的角标,就能获取下一个indexPath.
         
         // 获取最后一个cell的indexPath
         NSIndexPath *indexPath = [self.visibleIndexPaths lastObject];
         
         // 获取下一个cell的indexPath
         NSIndexPath *nextIndexPath = [NSIndexPath indexPathForRow:indexPath.row + 1 inSection:0];
         
         // 获取cell的高度
         if  ([self.delegate respondsToSelector:@selector(tableView:heightForRowAtIndexPath:)]) {
             cellH = [self.delegate tableView:self heightForRowAtIndexPath:nextIndexPath];
         } else {
             cellH = 44;
         }
         
         // 计算下一个cell的y值
         cellY = lastCellY + cellH;
         
         // 计算下下一个cell的frame
         CGRect nextCellFrame = CGRectMake(cellX, cellY, cellW, cellH);
         
         if  ([self isInScreen:nextCellFrame]) {  // 如果在屏幕上,就加载
             
             // 通过数据源获取cell
             UITableViewCell *cell = [self.dataSource tableView:self cellForRowAtIndexPath:nextIndexPath];
             
             cell.frame = nextCellFrame;
             
             [self insertSubview:cell atIndex:0];
             
             // 添加到cell可见数组中
             [self.visibleCells addObject:cell];
             
             // 添加到可见的indexPaths数组
             [self.visibleIndexPaths addObject:nextIndexPath];
             
             
         }
  • 往上移动

1.如果已经滚动到tableView最顶部,就不需要判断了有没有心的cell,直接返回.

2.需要判断之前显示在屏幕cell有没有移除屏幕

3.只需要判断下当前可见cell数组中最后一个cell有没有离开屏幕

4.只需要判断下可见cell数组中第一个cell的上一个cell显没显示在屏幕上即可

注意点:如果可见cell数组中第一个cell的上一个cell显示到屏幕上,一定要记得是插入到可见数组第0个的位置。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
         // 判断有没有滚动到最顶部
         if  (offsetY < 0) {
             return ;
         }
         
         
         
         // 判断下当前可见cell数组中最后一个cell有没有离开屏幕
         if  ([self isInScreen:lastCell.frame] == NO) {  // 如果不在屏幕
             // 从可见cell数组移除
             [self.visibleCells removeObject:lastCell];
             
             // 删除最后一个可见的indexPath
             [self.visibleIndexPaths removeLastObject];
             
             // 添加到缓存池中
             [self.reuserCells addObject:lastCell];
             
             // 移除父控件
             [lastCell removeFromSuperview];
             
         }
         
         // 判断下可见cell数组中第一个cell的上一个cell显没显示在屏幕上
         // 获取第一个cell的indexPath
         NSIndexPath *indexPath = self.visibleIndexPaths[0];
         
         
         // 获取下一个cell的indexPath
         NSIndexPath *preIndexPath = [NSIndexPath indexPathForRow:indexPath.row - 1 inSection:0];
         
         // 获取cell的高度
         if  ([self.delegate respondsToSelector:@selector(tableView:heightForRowAtIndexPath:)]) {
             cellH = [self.delegate tableView:self heightForRowAtIndexPath:preIndexPath];
         } else {
             cellH = 44;
         }
         
         // 计算上一个cell的y值
         cellY = firstCellY - cellH;
         
         
         // 计算上一个cell的frame
         CGRect preCellFrame = CGRectMake(cellX, cellY, cellW, cellH);
         
         if  ([self isInScreen:preCellFrame]) {  // 如果在屏幕上,就加载
             
             // 通过数据源获取cell
             UITableViewCell *cell = [self.dataSource tableView:self cellForRowAtIndexPath:preIndexPath];
             
             cell.frame = preCellFrame;
             
             [self insertSubview:cell atIndex:0];
             
             // 添加到cell可见数组中,这里应该用插入,因为这是最上面一个cell,应该插入到数组第0个
             [self.visibleCells insertObject:cell atIndex:0];
             
             // 添加到可见的indexPaths数组,这里应该用插入,因为这是最上面一个cell,应该插入到数组第0个
             [self.visibleIndexPaths insertObject:preIndexPath atIndex:0];
             
         }
         
     }

问题1:

  • 判断下当前可见cell数组中最后一个cell的下一个cell显没显示在屏幕上

  • 这里需要计算下一个cell的frame,frame就需要计算下一个cell的y值,需要获取对应的cell的高度 cellY = lastCellY + cellH

  • 而高度需要根据indexPath,从数据源获取

解决:

  • 可以搞个字典记录每个可见cell的indexPath,然后获取对应可见的indexPath,就能获取下一个indexPath.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@interface YZTableView ()
 
// 屏幕可见数组
@property (nonatomic, strong) NSMutableArray *visibleCells;
 
// 缓存池
@property (nonatomic, strong) NSMutableSet *reuserCells;
 
 
// 记录每个可见cell的indexPaths的顺序
@property (nonatomic, strong) NSMutableDictionary *visibleIndexPaths;
 
@end
 
- (NSMutableDictionary *)visibleIndexPaths
{
     if  (_visibleIndexPaths == nil) {
         _visibleIndexPaths = [NSMutableDictionary dictionary];
     }
     
     return  _visibleIndexPaths;
}

注意:

  • 当cell从缓存池中移除,一定要记得从可见数组cell中移除,还有可见cell的indexPath也要移除.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
         // 判断下当前可见cell数组中第一个cell有没有离开屏幕
         if  ([self isInScreen:firstCell.frame] == NO) {  // 如果不在屏幕
             // 从可见cell数组移除
             [self.visibleCells removeObject:firstCell];
             
             // 删除第0个从可见的indexPath
             [self.visibleIndexPaths removeObjectAtIndex:0];
             
             // 添加到缓存池中
             [self.reuserCells addObject:firstCell];
             
         }
         
  // 判断下当前可见cell数组中最后一个cell有没有离开屏幕
         if  ([self isInScreen:lastCell.frame] == NO) {  // 如果不在屏幕
             // 从可见cell数组移除
             [self.visibleCells removeObject:lastCell];
             
             // 删除最后一个可见的indexPath
             [self.visibleIndexPaths removeLastObject];
             
             // 添加到缓存池中
             [self.reuserCells addObject:lastCell];
             
         }

7.缓存池搭建,缓存池其实就是一个NSSet集合。

  • 搞一个NSSet集合充当缓存池.

  • cell离开屏幕,放进缓存池

  • 提供从缓存池获取方法,从缓存池中获取cell,记住要从NSSet集合移除cell.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
@interface YZTableView ()
 
// 屏幕可见数组
@property (nonatomic, strong) NSMutableArray *visibleCells;
 
// 缓存池
@property (nonatomic, strong) NSMutableSet *reuserCells;
 
// 记录每个cell的y值都对应一个indexPath
@property (nonatomic, strong) NSMutableDictionary *indexPathDict;
 
@end
@implementation YZTableView
- (NSMutableSet *)reuserCells
{
     if  (_reuserCells == nil) {
         _reuserCells = [NSMutableSet set];
     }
     return  _reuserCells;
}
 
// 从缓存池中获取cell
- (id)dequeueReusableCellWithIdentifier:(NSString *)identifier
{
     UITableViewCell *cell = [self.reuserCells anyObject];
     
     // 能取出cell,并且cell的标示符正确
     if  (cell && [cell.reuseIdentifier isEqualToString:identifier]) {     
         // 从缓存池中获取
         [self.reuserCells removeObject:cell];
         
         return  cell;
     }
     return  nil;
}
 
@end

8.tableView细节处理

原因:刷新方法经常要调用

解决:每次刷新的时候,先把之前记录的全部清空

1
2
3
4
5
6
7
8
9
10
11
12
// 刷新tableView
- (void)reloadData
{
     
     // 刷新方法经常要调用
     // 每次刷新的时候,先把之前记录的全部清空
     // 清空indexPath字典
     [self.indexPathDict removeAllObjects];
     // 清空屏幕可见数组
     [self.visibleCells removeAllObjects];
     ...
}

如果你喜欢这篇文章,可以继续关注我,微博:吖了个峥,欢迎交流。

点击这下载源代码

1、资源项目源码均已通过严格测试验证,保证能够正常运行; 2、项目问题、技术讨论,可以给博主私信或留言,博主看到后会第一时间与您进行沟通; 3、本项目比较适合计算机领域相关的毕业设计课题、课程作业等使用,尤其对于人工智能、计算机科学与技术等相关专业,更为适合; 4、下载使用后,可先查看README.md或论文文件(如有),本项目仅用作交流学习参考,请切勿用于商业用途。 5、资源来自互联网采集,如有侵权,私聊博主删除。 6、可私信博主看论文后选择购买源代码。 1、资源项目源码均已通过严格测试验证,保证能够正常运行; 2、项目问题、技术讨论,可以给博主私信或留言,博主看到后会第一时间与您进行沟通; 3、本项目比较适合计算机领域相关的毕业设计课题、课程作业等使用,尤其对于人工智能、计算机科学与技术等相关专业,更为适合; 4、下载使用后,可先查看README.md或论文文件(如有),本项目仅用作交流学习参考,请切勿用于商业用途。 5、资源来自互联网采集,如有侵权,私聊博主删除。 6、可私信博主看论文后选择购买源代码。 1、资源项目源码均已通过严格测试验证,保证能够正常运行; 2、项目问题、技术讨论,可以给博主私信或留言,博主看到后会第一时间与您进行沟通; 3、本项目比较适合计算机领域相关的毕业设计课题、课程作业等使用,尤其对于人工智能、计算机科学与技术等相关专业,更为适合; 4、下载使用后,可先查看README.md或论文文件(如有),本项目仅用作交流学习参考,请切勿用于商业用途。 5、资源来自互联网采集,如有侵权,私聊博主删除。 6、可私信博主看论文后选择购买源代码。
应用背景为变电站电力巡检,基于YOLO v4算法模型对常见电力巡检目标进行检测,并充分利用Ascend310提供的DVPP等硬件支持能力来完成流媒体的传输、处理等任务,并对系统性能做出一定的优化。.zip深度学习是机器学习的一个子领域,它基于人工神经网络的研究,特别是利用多层次的神经网络来进行学习和模式识别。深度学习模型能够学习数据的高层次特征,这些特征对于图像和语音识别、自然语言处理、医学图像分析等应用至关重要。以下是深度学习的一些关键概念和组成部分: 1. **神经网络(Neural Networks)**:深度学习的基础是人工神经网络,它是由多个层组成的网络结构,包括输入层、隐藏层和输出层。每个层由多个神经元组成,神经元之间通过权重连接。 2. **前馈神经网络(Feedforward Neural Networks)**:这是最常见的神经网络类型,信息从输入层流向隐藏层,最终到达输出层。 3. **卷积神经网络(Convolutional Neural Networks, CNNs)**:这种网络特别适合处理具有网格结构的数据,如图像。它们使用卷积层来提取图像的特征。 4. **循环神经网络(Recurrent Neural Networks, RNNs)**:这种网络能够处理序列数据,如时间序列或自然语言,因为它们具有记忆功能,能够捕捉数据中的时间依赖性。 5. **长短期记忆网络(Long Short-Term Memory, LSTM)**:LSTM 是一种特殊的 RNN,它能够学习长期依赖关系,非常适合复杂的序列预测任务。 6. **生成对抗网络(Generative Adversarial Networks, GANs)**:由两个网络组成,一个生成器和一个判别器,它们相互竞争,生成器生成数据,判别器评估数据的真实性。 7. **深度学习框架**:如 TensorFlow、Keras、PyTorch 等,这些框架提供了构建、训练和部署深度学习模型的工具和库。 8. **激活函数(Activation Functions)**:如 ReLU、Sigmoid、Tanh 等,它们在神经网络中用于添加非线性,使得网络能够学习复杂的函数。 9. **损失函数(Loss Functions)**:用于评估模型的预测与真实值之间的差异,常见的损失函数包括均方误差(MSE)、交叉熵(Cross-Entropy)等。 10. **优化算法(Optimization Algorithms)**:如梯度下降(Gradient Descent)、随机梯度下降(SGD)、Adam 等,用于更新网络权重,以最小化损失函数。 11. **正则化(Regularization)**:技术如 Dropout、L1/L2 正则化等,用于防止模型过拟合。 12. **迁移学习(Transfer Learning)**:利用在一个任务上训练好的模型来提高另一个相关任务的性能。 深度学习在许多领域都取得了显著的成就,但它也面临着一些挑战,如对大量数据的依赖、模型的解释性差、计算资源消耗大等。研究人员正在不断探索新的方法来解决这些问题。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值