当前位置: 首页 > news >正文

UITableVIew性能优化概述

UITableVIew性能优化概述

文章目录

  • UITableVIew性能优化概述
    • 前言
    • 如何优化
      • 优化的本质
      • 卡顿的原因
    • CPU层级
      • cell复用
        • UITableVIew尽量采用复用
      • 定义cell的种类尽量少,可以多用hidden
      • 缓存cell高度
        • 基础设置
        • 预先设置高度
        • 设置一个预先缓存
      • 异步绘制
      • 滑动按照需加载
      • 尽量显示大小刚好合适的图片资源
      • 图片异步加载
      • 直接设置frame
      • 采用轻量级对象
    • GPU层级
      • 减少透明处理
      • 控制尺寸
      • 减少图层混合操作
      • 避免离屏渲染
        • 为什么要避免离屏渲染?
        • 优化圆角
        • 阴影优化
    • 总结

前言

笔者这段时间把项目也结束了,现在整理一下这段实现学到的东西,以及一些性能优化的内容,这篇主要讲一下iOS开发中比较重要的一个UITableView的优化。

如何优化

UITableView 的优化主要从四个方面入手:
提前计算并缓存好高度(布局),因为 tableView:heightForRowAtIndexPath: 是调用最频繁的方法;
滑动时按需加载,防止卡顿。这个在大量图片展示,网络加载的时候很管用,配合 SDWebImage;
异步绘制,遇到复杂界面,遇到性能瓶颈时,可能就是突破口;
缓存一切可以缓存的,这个在开发的时候,往往是性能优化最多的方向。

大概需要关注的:
cell 复用;
cell 高度的计算;
渲染(混合问题);
减少视图的数目(重写 drawRect:);
减少多余的绘制操作;
不要给 cell 动态添加 subView;
异步化 UI,不要阻塞主线程;
滑动时按需加载对应的内容。

引用自iOS之性能优化·UITableView深度优化

优化的本质

UITableView 的优化本质在于提高滚动性能和减少内存使用,以保证流畅的用户体验,从计算机层面来讲,其核心本质为降低 CPU和GPU 的工作来提升性能

CPU:对象的创建和销毁、对象属性的调整、布局计算、文本的计算和排版、图片的格式转换和解码、图像的绘制
GPU:接收提交的纹理和顶点描述、应用变换、混合并渲染、输出到屏幕
引用自【iOS】UITableView性能优化

卡顿的原因

App主线程在CPU中显示计算内容,比如视图的创建,布局的计算,图片解码,文本绘制,然后我们的CPU会将计算好的内容提交到GPU中进行变换,合成,渲染,这其中也包括我们常说的离屏渲染

在开发中,CPUGPU任何一个压力过大都会导致掉帧

引用自【iOS】UITableView性能优化

CPU层级

cell复用

UITableVIew尽量采用复用
  • 首先我们都知道UITableVIew最核心的一个思想就是UITableViewCell的一个复用,也就是一个享元模式的思想,这个方式极大程度上的降低了内存的开销。当要显示某一位置的 Cell 时,会先去集合(或数组)中取,如果有,就直接拿来显示;如果没有,才会创建。

首先我们在很多地方都可以看到对于这两个函数做的一个性能优化:

tableView:cellForRowAtIndexPath: 
tableView:heightForRowAtIndexPath:

某些时候,我们会认为他是先调用height再调用cell方法,但是实际上并非如此,我们可以看下面打印信息

image-20250413203518757

显然这里颠覆了我们的一个普遍认知。

这里笔者找到了一段解释这个原因比较好的话:

我们都知道,UITableView 是继承自 UIScrollView 的,需要先确定它的 contentSize 及每个 Cell 的位置,然后才会把重用的 Cell 放置到对应的位置。所以事实上,UITableView 的回调顺序是先多次调用 tableView:heightForRowAtIndexPath: 以确定 contentSize 及 Cell 的位置,然后才会调用 tableView:cellForRowAtIndexPath:,从而来显示在当前屏幕的 Cell。

所以我们创建一个对应cell标识符的时候,可以采用这种方式:

static NSString *cellId = @"Cell";

这样可以减少我们的一个字符串的开销。

这里我们在看一下创建cell的两个函数:

 - (nullable __kindof UITableViewCell *)dequeueReusableCellWithIdentifier:(NSString *)identifier;  // Used by the delegate to acquire an already allocated cell, in lieu of allocating a new one.
 - (__kindof UITableViewCell *)dequeueReusableCellWithIdentifier:(NSString *)identifier forIndexPath:(NSIndexPath *)indexPath NS_AVAILABLE_IOS(6_0); // newer dequeue method guarantees a cell is returned and resized properly, assuming identifier is registered
  • dequeueReusableCellWithIdentifier:forIndexPath 如果没有注册复用 identifier,执行这句时会崩溃
  • dequeueReusableCellWithIdentifier 如果没有注册复用 identifier,语句返回 nil,继续执行会崩溃
if (!cell) {
        cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:@"cell"];
    }//采用这个可以防止他nil带来的一个问题
  • 为什么需要 forIndexPath:?
  • 因为在返回 cell 之前,会调用委托 tableView:heightForRowAtIndexPath:来确定 cell 尺寸(如果已经定义该函数)。
    我们经常在 tableView:cellForRowAtIndexPath: 中为每一个 cell 绑定数据,实际上在调用 cellForRowAtIndexPath: 的时候 cell 还没有被显示出来,为了提高效率应该把数据绑定的操作放在 cell 显示出来后再执行,可以在 tableView:willDisplayCell:forRowAtIndexPath: 方法中绑定数据。
  • 注意 willDisplayCell 中 cell 在 tableview 展示之前就会调用,此时 cell 实例已经生成,所以不能更改 cell 的结构,只能是改动 cell 上的 UI 的一些属性,如 label 的内容、控件的隐藏等。

所以我们千万不要去放弃UITableView的一个复用,倘若放弃了就会浪费跟多内容。

定义cell的种类尽量少,可以多用hidden

分析 Cell 结构,尽可能的将相同内容的抽取到一种样式 Cell 中,这样可以尽量少的创建cell的,尽管存在复用,但是如果cell种类过多还是会造成较大的内存开销。

可以这么理解,倘若一个屏幕的cell有M个,但是复用池会创建M+C个,但是如果有多个cell那么就会创建N*(M + C)个cell,那么这样一对比就会发现浪费内存。

既然只定义一种 Cell,那么需要把所有不同类型的 view 都定义好,放在 Cell 里面,通过 hidden 属性控制,来显示不同类型的内容。毕竟,在用户快速滑动中,只是单纯的显示/隐藏 subview 比实时创建要快得多。
尽量少用 [cell addSubview:] 动态添加 View,可以初始化时就添加,然后通过 hidden 属性来控制。

缓存cell高度

基础设置

这里是最经典的一个优化方式:

self.tableView.rowHeight = 88; //倘如所有cell都一个高度,可以直接采用这个方法,来解决那个函数多次调用的问题

另一种则是我们更加常用的一个UITableView的方法

- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
    NSLog(@"对应的一个高度调用次序");
    return 100;
}
预先设置高度

后面有iOS推出了一个:estimatedRowHeight这个可以预估高度的属性

UITableView 是个 UIScrollView,就像平时使用 UIScrollView 一样,加载时指定 contentSize 后它才能根据自己的 bounds、contentInset、contentOffset 等属性共同决定是否可以滑动以及滚动条的长度。而 UITableView 在一开始并不知道自己会被填充多少内容,于是询问 data source 个数和创建 cell,同时询问 delegate 这些 cell 应该显示的高度,这就造成它在加载的时候浪费了多余的计算在屏幕外边的 cell 上。

这个属性可以让我们更加迅速的加载,也更适合我们平时的一个自适应行高的内容,但是他也会带来一些缺点:

  • 可能会导致滚动条出现一个跳动的问题影响用户体验

因此,tableView:estimatedHeightForRowAtIndexPath: -> tableView:heightForRowAtIndexPath: 获取每个 Cell 即将显示的高度,从而确定表格视图的布局,实际是要获取滚动视图的 contentSize,然后调用 tableView:cellForRowAtIndexPath:,获取每个 Cell,进行赋值。如果有很多个 Cell 要显示,那么方法会执行很多次。
所以我们可以采用一个缓存高度数组的方式来解决这个问题。

- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
    NSLog(@"对应的一个高度调用次序");
    return heightAry[indexPath.row];
}
设置一个预先缓存

这里笔者看到一个开源库,这里他采用了一个预先计算高度的内容:预缓存机制将在 UITableView 没有滑动的空闲时刻执行,计算和缓存那些还没有显示到屏幕中的 cell,整个缓存过程完全没有感知,这使得完整列表的高度计算既没有发生在加载时,又没有发生在滑动时,同时保证了加载速度和滑动流畅性。

UITableView+FDTemplateLayoutCell 开源库之后笔者有时间去简单阅读源码,之后会对这一块内容做一个补充

异步绘制

遇到比较复杂的界面时(复杂点的图文混排),上面缓存行高的方式可能就不能满足要求。绘制的各个信息都是根据之前算好的布局进行绘制的,那么就需要异步绘制。这里笔者还不是很清楚这部分内容,之后会补充,主要还是对于GCD的内容不够熟练

image-20250413213215142

- (void)draw {
    // 异步绘制
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        
    });
}

/** 
 *  @brief  重写 drawRect: 方法 
 */
- (void)drawRect:(CGRect)rect {
    // 不需要用 GCD 异步线程,因为 drawRect: 本来就是异步绘制的
}

滑动按照需加载

现在iOS其实引入了两个与加载函数:

- (void)tableView:(UITableView *)tableView prefetchRowsAtIndexPaths:(NSArray<NSIndexPath *> *)indexPaths;
- (void)tableView:(UITableView *)tableView cancelPrefetchingForRowsAtIndexPaths:(NSArray<NSIndexPath *> *)indexPaths; //继承UITableViewDataSourcePrefetching协议

这两个函数支持一个与加载数据的内容,这里笔者可以分享一篇博客:

iOS 优雅的处理网络数据,你真的会吗?不如看看这篇.

笔者认为他讲的比较透彻就不多赘述了,感兴趣的可以去看这篇博客

尽量显示大小刚好合适的图片资源

这里我们加载图片尽量采用大小合适的图片,避免大量的图片缩放、颜色渐变等。

图片异步加载

这个内容比较重要,我们都会采用SDWebImage来异步加载图片资源。

  • 使用异步子线程处理,然后再返回主线程操作;
  • 图片缓存处理,避免多次处理操作;
  • 图片圆角处理时,设置 layer 的 shouldRasterize 属性为 YES,可以将负载转移给 CPU;

这部分代码我会在学习SDWebImage后继续补充

直接设置frame

对于一些不用自使用高度的,我们可以直接给cell设置一个frame来减小CPU压力,使用Masonry布局的时候会增加CPU运算负担

采用轻量级对象

在某些情况下,我们可以采用CALyaer来代替我们的一个UIView:

同时实际上每一个UIView都有一个CALayer的属性,其实我们可以把UIView理解为CALayer的高级封装,他们的本质区别在于是否能响应事件,这也是CALayer性能优于UIView的主要原因。

可以说CALayer只负责一个绘制,而且更接近底层,我们采用CALayer可以降低CPU消耗

GPU层级

减少透明处理

减少透明的视图,不透明的就设置opaque = YES

控制尺寸

GPU能处理的最大纹理尺寸是4096x4096,超过这个尺寸就会占用CPU资源进行处理,所以纹理尽量不要超过这个尺寸。

纹理(Texture)就是贴在模型或 UI 元素上的图片,比如游戏里的人物皮肤、按钮背景图,都是纹理。

减少图层混合操作

当多个视图叠加,放在上面的视图是半透明的,那么这个时候GPU就要进行混合,把透明的颜色加上放在下面的视图的颜色混合之后得出一个颜色再显示在屏幕上,这一步是消耗GPU资源

  • UIViewbackgroundColor不要设置为clearColor,最好设置和superViewbackgroundColor颜色一样
  • 图片避免使用带alpha通道的图片

避免离屏渲染

笔者不是很了解什么是离屏渲染。所以笔者查询资料了解到:

渲染类型说明
屏幕内渲染(On-Screen Rendering)图层直接渲染在屏幕帧缓存中
离屏渲染(Off-Screen Rendering)图层需要先渲染到一个缓冲区(off-screen buffer),渲染完成后再复制到屏幕上
为什么要避免离屏渲染?
问题描述
性能开销大多了一次 offscreen buffer 的创建、上下文切换,浪费 GPU 资源
内存增加离屏缓存占用额外内存,尤其在大量视图中(如 tableview cell)更严重
卡顿掉帧渲染帧率下降,导致页面滑动不流畅(尤其是 60fps 要求下)

下面操作会导致离屏渲染

  • 光栅化,layer.shouldRasterize = YES

  • 遮罩,layer.mask

  • 圆角,同时设置 layer.masksToBounds = YESlayer.cornerRadius > 0

  • 阴影,layer.shadow

  • layer.allowsGroupOpacity = YESlayer.opacity != 1

这里笔者还不是很清楚原理,仅仅了解到这些操作会造成一个大量的性能开销,之后笔者会单独写一篇有关于离屏渲染的博客

优化圆角

采用贝塞尔曲线来绘制圆形:

UIBezierPath *path = [UIBezierPath bezierPathWithRoundedRect:self.bounds byRoundingCorners:UIRectCornerTopLeft | UIRectCornerTopRight cornerRadii:CGSizeMake(10, 10)];
CAShapeLayer *layer = [[CAShapeLayer alloc] init];
layer.frame = self.bounds;
layer.path = path.CGPath;
CAShapeLayer *borderLayer = [CAShapeLayer layer];
borderLayer.path = path.CGPath;
borderLayer.lineWidth = 1.0;
borderLayer.strokeColor = [UIColor lightGrayColor].CGColor;
borderLayer.fillColor = [UIColor clearColor].CGColor;
阴影优化
imageView.layer.shadowColor = [UIColor grayColor].CGColor;
imageView.layer.shadowOpacity = 1.0;
imageView.layer.shadowRadius = 2.0;
UIBezierPath *path = [UIBezierPath bezierPathWithRect:imageView.frame];
imageView.layer.shadowPath = path.CGPath;

总结

  • 采用UITablecell的一个复用机制,减少对于cell种类的一个创建,多采用直接访问的方法。
  • 高度缓存数组的使用。
  • 预缓存高度。
  • 图片异步加载,采用SDWebImage库来实现一个异步加载。
  • 减少离屏渲染,这部分比较重要之后在做一篇博客的总结
  • 按照需要加载数据,根据UITableVIew的新推出的一些协议方法

参考博客:

【iOS】UITableView性能优化

[iOS开发]UITableView的性能优化

UITableViewCell复用机制及踩坑总结

相关文章:

  • 【DE2-115】Verilog实现DDS+Quartus仿真波形
  • 【算法】One-Stage检测器与Two-Stage检测器的原理和区别
  • 开启bitlocker使用windows的加密功能
  • (1)VTK环境配置
  • Unity 基于navMesh的怪物追踪惯性系统
  • CAP理论 与 BASE理论
  • RAG文献阅读——用于知识密集型自然语言处理任务的检索增强生成
  • 数据库删除表数据
  • 在C盘新建文本文档
  • Go环境变量配置
  • Qt报错dependent ‘..\..\..\..\..\..\xxxx\QMainWindow‘ 或者 QtCore\QObject not exist
  • QEMU学习之路(7)— ARM64 启动Linux
  • 每天学一个 Linux 命令(16):mkdir
  • 【寻找Linux的奥秘】第四章:基础开发工具(下)
  • 信息学奥赛一本通 1498:Roadblocks | 洛谷 P2865 [USACO06NOV] Roadblocks G
  • Ubuntu 各个常见长期支持历史版本与代号
  • 低资源需求的大模型训练项目---3、综合对比与选型建议
  • 计算机基础复习资料整理
  • AI数字消费第一股,重构商业版图的新物种
  • oracle怎么查看是否走了索引
  • 做网站得花多少钱/网站推广入口
  • 临武县网站建设/百度软件开放平台
  • 传奇网页游戏制作/seo查询 站长之家
  • 上海做宴会的网站/抖音seo关键词优化排名
  • 织梦修改网站源代码/知乎软文推广
  • 网站设计需要注意什么/网店网络推广方案