转载:TableView性能优化

转载:TableView性能优化

原文链接:https://juejin.cn/post/6955731915672387592

tableView性能优化

Cell重用、标识重用

使用 static 修饰重用标识名称能够保证这个标识只会创建一次,提高性能。接着调用dequeueReusableCellWithIdentifier:方法 获取缓存池中的Cell。如果没有就调用 initWithStyle:ReusIdentifier:方法 创建一个新的Cell。注意事先需要调用registerNib/registerClass方法TableView 注册一下重用标识。

动态高度

我们需要实现它的代理,来给出高度:

objectivec复制代码- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {// return xxx
}

这个代理方法实现后,上面的 rowHeight 的设置将会变成无效。在这个方法中,我们需要提高cell高度的计算效率,来节省时间。

自从iOS8之后有了 self-sizing cell的概念,cell可以自己算出高度,使用self-sizing cell需要满足以下三个条件:

(1)使用 Autolayout 进行 UI布局约束(要求cell.contentView的四条边都与内部元素有约束关系)。

(2)指定 TableViewestimatedRowHeight属性 的默认值。

(3)指定 TableView的rowHeight 属性为 UITableViewAutomaticDimension

ini复制代码- (void)viewDidload {self.myTableView.estimatedRowHeight = 44.0;self.myTableView.rowHeight = UITableViewAutomaticDimension;
}

除了提高cell高度的计算效率之外,对于已经计算出的高度,我们需要进行缓存,对于已经计算过的高度,没有必要进行计算第二次。

减少视图的数目

我们在 cell 上添加系统控件的时候,实际上系统都会调用底层的接口进行绘制,大量添加控件时,会消耗很大的资源并且也会影响渲染的性能。当使用默认的 UITableViewCell 并且在它的 ContentView 上面添加控件时会相当消耗性能。所以目前最佳的方法还是继承 UITableViewCell,并重写drawRect方法

重绘操作仍然在 drawRect方法 中完成,但是苹果不建议直接调用 drawRect方法,当然如果你强直直接调用此方法,当然是没有效果的。苹果要求我们调用UIView类中的 setNeedsDisplay方法,则程序会自动调用 drawRect方法 进行重绘。(调用 setNeedsDisplay 会自动调用 drawRect)。

使用hidden隐藏图层

避免动态添加图层。在初始化cell的时候一并将所有图层预先创建好,通过hidden属性控制子图层的显示或隐藏,因为单纯的显示操作要比创建快的多。

在快速滚动时考虑使用界面外壳

使用有外壳的界面.png

当用户快速滚动列表视图时,虽然使用了所有的优化,但视图的重用和渲染仍然需要超过 16 毫秒,还有可能出现偶发的丢帧现象,从而导致不流畅的体验。

在这些情况下,使用一个界面外壳是一个较好的选择,外壳可以被预先定义,它的唯一目的就是告诉终端用户这些部分即将展示一些数据。当滚动速度降低,并低于阈值时,刷新最终的视图并填充数据。

你可以使用与列表视图相关联的 panGestureRecognizer 属性获取速度值。

// 列表视图的速度
-(void)scrollViewDidScroll:(UIScrollView *)scrollView { CGPoint velocity = [tableView.panGestureRecognizer velocityInView:self.view]; self.velocity = velocity;
}
-(UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
if(fabs(self.velocity.y) > 2000) { //返回界面外壳} else {//返回真正的单元格 }
}

避免离屛渲染。

开启离屛渲染的代价就是需要新开辟一块新的缓冲区,在渲染的过程中还会多次的切换上下文,这些都是很消耗性能的。以下情况均会造成离屛渲染:

1. shadows(阴影)

其原因在于需要显示在所有layer内容的下方,因此必须被渲染在先。但此时阴影的本体(layer和其子layer)都还没有被组合到一起,只能另外申请一块内存,把本体内容都先画好,再根据渲染结果的形状,添加阴影到帧缓冲区frame buffer,最后把内容画上去。不过如果我们能够预先告诉CoreAnmation(通过 shadowPath属性 )阴影的几何形状,那么阴影当然可以先被独立渲染出来,不需要依赖layer本体,也就不再需要离屏渲染了。

2. 设置了组透明度为YES,并且透明度不为1的layer(不透明)

产生离屏渲染的条件是layer.opacity != 1.0并且有 子layer 或者背景图。alpha并不是分别应用在每一层之上,而是只有到 整个layer树 画完之后,再统一加上alpha,最后和底下其他layer的像素进行组合。显然也无法通过一次遍历就得到最终结果。

3. masks(遮罩)

我们知道mask是应用在layer和其所有子layer的组合之上的,而且可能带有透明度,那么其实和group opacity的原理类似,不得不在离屏渲染中完成。

4. cornerRadius+clipsToBounds

容器的子layer因为父容器有圆角,那么也会需要被裁剪,而这时它们还在渲染队列中排队,尚未被组合到一块画布上,自然也无法统一裁剪,不得已只能另开一块内存来操作。而如果只是设置cornerRadius,并不会触发离屏渲染。

5. shouldRasterize(光栅化)

如果layer不是静态的,我们更新已光栅化的layer,会造成大量的离屏渲染。 例如UITableViewCell因为复用的原因,重绘是很频繁的。如果此时设置了光栅化,会造成大量离屏渲染,降低性能。 不要过度使用,系统限制了缓存的大小为 2.5 * Screen Size。超出缓存之后,同样会造成大量的离屏渲染。 离屏渲染缓存内容有时间限制,被光栅化的图片(即缓存内容)如果超过100ms没有被使用,那么它就会丢弃,无法进行复用。(所以光栅化只能用在图像内容不变的前提下,且只对连续不断使用的图片进行缓存:用于避免静态内容的复杂特效的重绘,例如UIBlurEffect用于避免多个View嵌套的复杂View的重绘。)

6. edge antialiasing(抗锯齿)

分页加载数据,预先异步请求数据 - Prefetching API

viewDidLoad 中先请求网络数据来获取一些初始化数据,然后再利用 UITableViewPrefetching API 来对数据进行预加载,从而来实现数据的无缝加载。

UITableViewDataSourcePrefetching 协议
// this protocol can provide information about cells before they are displayed on screen.@protocol UITableViewDataSourcePrefetching <NSObject>@required// indexPaths are ordered ascending by geometric distance from the table view
- (void)tableView:(UITableView *)tableView prefetchRowsAtIndexPaths:(NSArray<NSIndexPath *> *)indexPaths;@optional// indexPaths that previously were considered as candidates for pre-fetching, but were not actually used; may be a subset of the previous call to -tableView:prefetchRowsAtIndexPaths:
- (void)tableView:(UITableView *)tableView cancelPrefetchingForRowsAtIndexPaths:(NSArray<NSIndexPath *> *)indexPaths;@end

第一个函数会基于当前滚动的方向和速度对接下来的 IndexPaths 进行 Prefetch,通常我们会在这里实现预加载数据的逻辑。

第二个函数是一个可选的方法,当用户快速滚动导致一些 Cell 不可见的时候,你可以通过这个方法来取消任何挂起的数据加载操作,有利于提高滚动性能, 在下面我会讲到。

实现这俩个函数的逻辑代码为:

swift复制代码extension ViewController: UITableViewDataSourcePrefetching {// 翻页请求func tableView(_ tableView: UITableView, prefetchRowsAt indexPaths: [IndexPath]) {let needFetch = indexPaths.contains { $0.row >= viewModel.currentCount}if needFetch {// 1.满足条件进行翻页请求indicatorView.startAnimating()viewModel.fetchImages()}for indexPath in indexPaths {if let _ = viewModel.loadingOperations[indexPath] {return}if let dataloader = viewModel.loadImage(at: indexPath.row) {print("在 \(indexPath.row) 行 对图片进行 prefetch ")// 2 对需要下载的图片进行预热viewModel.loadingQueue.addOperation(dataloader)// 3 将该下载线程加入到记录数组中以便根据索引查找viewModel.loadingOperations[indexPath] = dataloader}}}func tableView(_ tableView: UITableView, cancelPrefetchingForRowsAt indexPaths: [IndexPath]){// 该行在不需要显示的时候,取消 prefetch ,避免造成资源浪费indexPaths.forEach {if let dataLoader = viewModel.loadingOperations[$0] {print("在 \($0.row) 行 cancelPrefetchingForRowsAt ")dataLoader.cancel()viewModel.loadingOperations.removeValue(forKey: $0)}}}
}

最后,再加上俩个有用的方法该功能就大功告成了:

   // 用于计算 tableview 加载新数据时需要 reload 的 cellfunc visibleIndexPathsToReload(intersecting indexPaths: [IndexPath]) -> [IndexPath] {let indexPathsForVisibleRows = tableView.indexPathsForVisibleRows ?? []let indexPathsIntersection = Set(indexPathsForVisibleRows).intersection(indexPaths)return Array(indexPathsIntersection)}// 用于确定该索引的行是否超出了目前收到数据的最大数量func isLoadingCell(for indexPath: IndexPath) -> Bool {return indexPath.row >= (viewModel.currentCount)}

滑动TableView时,按需加载内容

有些情况下我们可能会去快速的滑动列表,这时候其实会有大量的cell对象被创建、被重用,但其实我们可能只是去浏览列表停止的那一页的上下一定范围内的信息,前面快速划过的那些信息对我们来说都是无用的。此时我们可以通过ScrollView的代理方法

scrollViewWillEndDragging: withVelocity: targetContentoffset:

来按需加载内容。

#pragma mark - UIScrollViewDelegate  
//按需加载 - 如果目标行与当前行相差超过指定行数,只在目标滚动范围的前后指定3行加载。  
- (void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(inout CGPoint *)targetContentOffset {  NSIndexPath *targetPath = [_myTableView indexPathForRowAtPoint:CGPointMake(0, targetContentOffset->y)];  NSIndexPath *firstVisiblePath = [[_myTableView indexPathsForVisibleRows] firstObject];  NSInteger skipCount = 8;  if (labs(firstVisiblePath.row - targetPath.row)>  skipCount) {  NSArray *temp = [_myTableView indexPathsForRowsInRect:CGRectMake(0, targetContentOffset->y, _myTableView.frame.size.width, _myTableView.frame.size.height)];  NSMutableArray *arr = [NSMutableArray arrayWithArray:temp];  if (velocity.y<0) {  NSIndexPath *indexPath = [temp lastObject];  if (indexPath.row+33) {  [arr addObject:[NSIndexPath indexPathForRow:indexPath.row-3 inSection:0]];  [arr addObject:[NSIndexPath indexPathForRow:indexPath.row-2 inSection:0]];  [arr addObject:[NSIndexPath indexPathForRow:indexPath.row-1 inSection:0]];  }  }  [_dataList addObjectsFromArray:arr];  }  
}  

targetContentOffsetTableView 减速到停止的地方, velocity 表示速度向量。

如何避免滚动时的卡顿: 异步化UI,不要阻塞主线程

当你遇到滚动卡顿的应用程序时,通常是由于任务长时间运行阻碍了 UI 在主线程上的更新,想让主线程有空来响应这类更新事件,第一步就是要将消耗时间的任务交给子线程去执行,避免在获取数据时阻塞主线程。

苹果提供了很多为应用程序实现并发的方式,例如 GCD,我在这里对 Cell 上的图片进行异步加载使用的就是它。 代码如下:

swift复制代码class DataLoadOperation: Operation {var image: UIImage?var loadingCompleteHandle: ((UIImage?) -> ())?private var _image: ImageModelprivate let cachedImages = NSCache<NSURL, UIImage>()init(_ image: ImageModel) {_image = image}public final func image(url: NSURL) -> UIImage? {return cachedImages.object(forKey: url)}override func main() {if isCancelled {return}guard let url = _image.url else {return}downloadImageFrom(url) { (image) inDispatchQueue.main.async { [weak self] inguard let ss = self else { return }if ss.isCancelled { return }ss.image = imagess.loadingCompleteHandle?(ss.image)}}}// Returns the cached image if available, otherwise asynchronously loads and caches it.func downloadImageFrom(_ url: NSURL, completeHandler: @escaping (UIImage?) -> ()) {// Check for a cached image.if let cachedImage = image(url: url) {DispatchQueue.main.async {print("命中缓存")completeHandler(cachedImage)}return}URLSession.shared.dataTask(with: url as URL) { data, response, error inguardlet httpURLResponse = response as? HTTPURLResponse, httpURLResponse.statusCode == 200,let mimeType = response?.mimeType, mimeType.hasPrefix("image"),let data = data, error == nil,let _image = UIImage(data: data)else { return }// Cache the image.self.cachedImages.setObject(_image, forKey: url, cost: data.count)completeHandler(_image)}.resume()}
}

在willDisplayCell:forRowAtIndexPath:代理方法中绑定数据

那具体如何使用呢!别急,听我娓娓道来,这里我再给大家一个小建议,大家都知道 UITableView 实例化 Cell 的方法是:tableView:cellForRowAtIndexPath: ,相信很多人都会在这个方法里面去进行数据绑定然后更新 UI,其实这样做是一种比较低效的行为,因为这个方法需要为每个 Cell 调用一次,它应该快速的执行并返回重用 Cell 的实例,不要在这里去执行数据绑定,因为目前在屏幕上还没有 Cell。我们可以在 tableView:willDisplayCell:forRowAtIndexPath: 这个方法中进行数据绑定,这个方法在显示cell之前会被调用。

为每个 Cell 执行下载任务的实现代码如下:

swift复制代码 func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {guard let cell = tableView.dequeueReusableCell(withIdentifier: "PreloadCellID") as? ProloadTableViewCell else {fatalError("Sorry, could not load cell")}if isLoadingCell(for: indexPath) {cell.updateUI(.none, orderNo: "\(indexPath.row)")}return cell}func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {// preheat image ,处理将要显示的图像guard let cell = cell as? ProloadTableViewCell else {return}// 图片下载完毕后更新 celllet updateCellClosure: (UIImage?) -> () = { [unowned self] (image) incell.updateUI(image, orderNo: "\(indexPath.row)")viewModel.loadingOperations.removeValue(forKey: indexPath)}// 1. 首先判断是否已经存在创建好的下载线程if let dataLoader = viewModel.loadingOperations[indexPath] {if let image = dataLoader.image {// 1.1 若图片已经下载好,直接更新cell.updateUI(image, orderNo: "\(indexPath.row)")} else {// 1.2 若图片还未下载好,则等待图片下载完后更新 celldataLoader.loadingCompleteHandle = updateCellClosure}} else {// 2. 没找到,则为指定的 url 创建一个新的下载线程print("在 \(indexPath.row) 行创建一个新的图片下载线程")if let dataloader = viewModel.loadImage(at: indexPath.row) {// 2.1 添加图片下载完毕后的回调dataloader.loadingCompleteHandle = updateCellClosure// 2.2 启动下载viewModel.loadingQueue.addOperation(dataloader)// 2.3 将该下载线程加入到记录数组中以便根据索引查找viewModel.loadingOperations[indexPath] = dataloader}}}

对预加载的图片进行异步下载(预热):

swift复制代码func tableView(_ tableView: UITableView, prefetchRowsAt indexPaths: [IndexPath]) {let needFetch = indexPaths.contains { $0.row >= viewModel.currentCount}if needFetch {// 1.满足条件进行翻页请求indicatorView.startAnimating()viewModel.fetchImages()}for indexPath in indexPaths {if let _ = viewModel.loadingOperations[indexPath] {return}if let dataloader = viewModel.loadImage(at: indexPath.row) {print("在 \(indexPath.row) 行 对图片进行 prefetch ")// 2 对需要下载的图片进行预热viewModel.loadingQueue.addOperation(dataloader)// 3 将该下载线程加入到记录数组中以便根据索引查找viewModel.loadingOperations[indexPath] = dataloader}}}

取消 Prefetch 时,cancel 任务,避免造成资源浪费

swift复制代码func tableView(_ tableView: UITableView, cancelPrefetchingForRowsAt indexPaths: [IndexPath]){// 该行在不需要显示的时候,取消 prefetch ,避免造成资源浪费indexPaths.forEach {if let dataLoader = viewModel.loadingOperations[$0] {print("在 \($0.row) 行 cancelPrefetchingForRowsAt ")dataLoader.cancel()viewModel.loadingOperations.removeValue(forKey: $0)}}}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/278252.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

多分类预测 | MATLAB实现CNN-LSTM-Attention多输入分类预测

分类预测 | MATLAB实现CNN-LSTM-Attention多输入分类预测 分类效果 需要源码和数据的私信&#xff08;微微有偿取哦&#xff09;

【Jmeter】Jmeter基础5-Jmeter元件介绍之线程(用户)

2.5.1、线程组 一个线程组即一个虚拟用户组&#xff0c;线程组中的每个线程即为1个虚拟用户&#xff0c;每个线程互相隔离&#xff0c;互不影响参数说明&#xff1a; 在取样器错误后要执行的动作 继续&#xff1a;忽略错误&#xff0c;继续执行启动下一进程循环&#xff1a; 终…

Repo代码仓库搭建

使用rockchip sdk二次开发&#xff0c;代码十几个G&#xff0c;都放在一个git仓库的话&#xff0c;每次git status要等好久&#xff0c;决定拆分一下&#xff0c;官方是用repo做代码管理的&#xff0c;我打算也搭建个类似开发环境。 1.首先在git服务器上创建一个manifest仓库&…

架构设计系列之常见架构(二)

五、DDD&#xff08;领域驱动设计&#xff09; 领域驱动设计&#xff08;Domain-Driven Design&#xff0c;DDD&#xff09;是一种开发思想&#xff0c;强调将软件系统的注意力集中在业务领域上&#xff0c;将领域视为应用的核心。在架构设计中&#xff0c;DDD 提供了一种不同…

云端赋能大湾区:华为云照亮数字化转型之路

编辑&#xff1a;阿冒 设计&#xff1a;沐由 在中国的经济版图上&#xff0c;大湾区是极其重要的增长引擎。这块富有活力和创新力的经济区域里&#xff0c;荟聚了大量的高新技术企业&#xff0c;以及一批创新孵化器和科研机构&#xff0c;产业升级和技术创新的氛围格外浓烈。 1…

深眸科技聚焦AI+机器视觉产业化建设,加速智能制造国产替代升级

随着科技的不断发展&#xff0c;传统的制造生产已经无法满足现代制造业的需求&#xff0c;智能制造应运而生&#xff0c;以智能化、柔性化等生产优势&#xff0c;大幅提升制造效率和生产质量。智能制造是指具有信息自感知、自决策、自执行等功能的先进制造过程、系统与模式的总…

Django 表单处理:从前端到后台的全流程指南

概要 Django作为一个高级Python Web框架&#xff0c;它的表单处理能力强大&#xff0c;可以有效地处理用户输入&#xff0c;进行数据验证以及错误处理。本文将详细介绍如何在Django中创建、处理和使用表单。 1. Django表单系统的核心 Django的表单系统处理表单的生命周期&…

Unity | Shader基础知识(第六集:语法<如何加入外部颜色资源>)

目录 一、本节介绍 1 上集回顾 2 本节介绍 二、语法结构 1 复习 2 理论知识 3 Shader里声明的写法 4 Properties和SubShader毕竟不是一家人 三、 片元着色器中使用资源 四、代码实现 五、全部代码 六、下集介绍 相关阅读 Unity - Manual: Writing Surface Shaders…

多条件三元表达式如何写?

在某些业务需求情况下&#xff0c;如何书写多条件三元表达式&#xff1f;&#xff08;例如&#xff0c;父组件传值给子组件&#xff0c;子组件根据不同的值去响应不同的颜色变化该如何实现&#xff1f;&#xff09; 父组件&#xff1a; 父组件传testData的值给子组件&#xff…

一个适用于搭建企业内部培训平台的开源系统

大家好&#xff0c;我是 Java陈序员。 问君能有几多愁&#xff0c;唯有开源项目解千愁&#xff01; 最近领导给了个任务&#xff0c;搭建一个企业内部培训平台&#xff01;好不容易刚完成上个任务&#xff0c;又来一个活&#xff0c;这不又得加班了&#xff01; 还好&#x…

解决Chrome同一账号在不同设备无法自动同步书签的问题

文章目录 一、问题与原因&#xff1f;2. 解决办法 一、问题与原因&#xff1f; 1.问题 使用谷歌Chrome浏览器比较头疼的问题就是&#xff1a;使用同一个Google账号&#xff0c;办公电脑与家用电脑的数据无法同步。比如&#xff1a;办公电脑中的书签、浏览记录等数据&#xff0…

Python计算圆的面积,几何学技法大解析!

更多Python学习内容&#xff1a;ipengtao.com 大家好&#xff0c;我是彭涛&#xff0c;今天为大家分享 Python计算圆的面积&#xff0c;几何学技法大解析&#xff0c;全文3800字&#xff0c;阅读大约15分钟。 在本文中&#xff0c;将深入探讨如何使用 Python 计算圆的面积&…