一、Cell 复用

在可见的页面会重复绘制页面,每次刷新显示都会去创建新的 Cell,非常耗费性能。 

解决方案:创建一个静态变量 reuseID,防止重复创建(提高性能),使用系统的缓存池功能。

static NSString * CELL_RUID = @"CELL";  // 调用次数太多,static 保证只创建一次 reuseID,提高性能

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
// 缓存池中取已经创建的 cell
UITableViewCell * cell = [tableView dequeueReusableCellWithIdentifier:CELL_RUID
forIndexPath:indexPath];
return cell;
}

通过 identifier 标识不同类型的 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

复用 Cell 时 不会调用 awakeFromNib。

  • 获取方法的区别

dequeueReusableCellWithIdentifier:forIndexPath 如果没有注册复用 identifier,执行这句时会崩溃,提示:

reason: 'unable to dequeue a cell with identifier CELL - must register a nib or a class for the identifier or connect a prototype cell in a storyboard'

dequeueReusableCellWithIdentifier 如果没有注册复用 identifier,语句返回 nil,继续执行会崩溃。提示:

failed to obtain a cell from its dataSource

判断 nil 后可以自己创建 cell。

{
MyCell * cell = [tableView dequeueReusableCellWithIdentifier:@"Cell"];
if (cell == nil) {
cell = [[MyCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:@"Cell"];
}
}
  • 为什么需要 forIndexPath:

因为在返回 cell 之前,会调用委托 tableView:heightForRowAtIndexPath:来确定 cell 尺寸(如果已经定义该函数)。

我们经常在 tableView:cellForRowAtIndexPath: 中为每一个 cell 绑定数据,实际上在调用 cellForRowAtIndexPath: 的时候 cell 还没有被显示出来,为了提高效率我们应该把数据绑定的操作放在 cell 显示出来后再执行,可以在 tableView:willDisplayCell:forRowAtIndexPath: 方法中绑定数据。

注意 willDisplayCell 中 cell 在 tableview 展示之前就会调用,此时 cell 实例已经生成,所以不能更改 cell 的结构,只能是改动 cell 上的 UI 的一些属性,如 label 的内容、控件的隐藏等。

二、定义一种(尽量少)类型的 Cell 及善用 hidden 隐藏(显示)subviews

分析 Cell 结构,尽可能的将相同内容的抽取到一种样式 Cell 中。UITableView 真正创建出的 Cell 可能只比屏幕显示的多一点。虽然 Cell 的"体积"可能会大点,但是因为 Cell 的数量不会很多,完全可以接受的。

好处:

①、减少代码量,减少 Nib 文件的数量,在一个 Nib 文件定义 Cell,容易修改、维护;(多个 Cell 不是更容易维护?)

②、基于复用机制,真正运行时铺满屏幕所需的 Cell 数量大致是固定的,设为 N 个。如果只有一种 cell,那就是只有 N + c 个 cell 的实例;但是如果有 M 种 cell,那么运行时最多可能会是 M * (N + c) 个 cell 的实例,虽然这可能并不会占用太多内存,但能少一些更好。

既然只定义一种 Cell,那么需要把所有不同类型的 view 都定义好,放在 Cell 里面,通过 hidden 属性控制,来显示不同类型的内容。毕竟,在用户快速滑动中,只是单纯的显示/隐藏 subview 比实时创建要快得多。

尽量少用 [cell addSubview:] 动态添加 View,可以初始化时就添加,然后通过 hidden 属性来控制。

三、提前计算并缓存 Cell 的高度

3.1 固定高度的 cell

self.tableView.rowHeight = 88;

直接采用上面方式给定高度,不需要实现 tableView:heightForRowAtIndexPath: 以节省不必要的计算和开销。

3.2 动态高度的 cell

实现代理方法后,上面的 rowHeight 属性的设置将会变成无效。

tableView:estimatedHeightForRowAtIndexPath: -> tableView:heightForRowAtIndexPath: 获取每个 Cell 即将显示的高度,从而确定表格视图的布局,实际是要获取滚动视图的 contentSize,然后调用 tableView:cellForRowAtIndexPath:,获取每个 Cell,进行赋值。如果有很多个 Cell 要显示,那么方法会执行很多次。

解决方案:在 Model(Entity)中计算并保存 Cell 的高度。其实 Model 中保存 UI 的参数是很奇怪的,最好放在 MVVM 模式的 ViewModel(视图模型)中,让 Model(数据模型)只负责处理数据。

@interface Model : NSObject

@property (nonatomic, assign) CGFloat cellHeight;  // Cell 高度

/**
* @brief 计算高度
*/
- (void)calculateCellHeight; @end

在 tableView:heightForRowAtIndexPath: 中尽量不使用 cellForRowAtIndexPath: 方法来获取 cell,如果你需要用到它,只用一次然后缓存结果。

还可以继续进行优化,提前创建真正显示的、需要加工的数据并缓存。如:接口返回 NSString 而展示 NSAttributeString。

四、异步绘制(自定义 Cell 绘制)

遇到比较复杂的界面时(复杂点的图文混排),上面缓存行高的方式可能就不能满足要求了。详细整理:UITableView 优化技巧

/**
* @brief cell 添加 draw 方法
*/
- (void)draw
{
// 异步绘制
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ });
} /**
* @brief 重写 drawRect: 方法
*/
- (void)drawRect:(CGRect)rect
{
// 不需要用 GCD 异步线程,因为 drawRect: 本来就是异步绘制的。
}

绘制的各个信息都是根据之前算好的布局进行绘制的。这里是需要异步绘制。

五、滑动时,按需加载

自定义 Cell 的种类千奇百怪,但它本来就是用来显示数据的,差不多 100% 带有图片,这个时候就要考虑,下滑的过程中可能会有点卡顿,尤其网络不好的时候,异步加载图片是个程序员都会想到,但是如果给每个循环对象都加上异步加载,开启的线程太多,一样会卡顿。这个时候利用 UIScrollViewDelegate 两个代理方法就能很好地解决这个问题。

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
if (needLoadArr.count > 0 && [needLoadArr indexOfObject:indexPath] == NSNotFound) {
[cell clear]; // 清掉内容
}
return cell;
} // 按需加载 - 如果目标行与当前行相差超过指定行数,只在目标滚动范围的前后指定 3 行加载。
- (void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(inout CGPoint *)targetContentOffset
{
NSIndexPath * ip = [self.tableView indexPathForRowAtPoint:CGPointMake(0, targetContentOffset->y)];
NSIndexPath * cip = [[self.tableView indexPathsForVisibleRows] firstObject]; NSInteger skipCount = 8; // -8 < 当前位置 - 目标位置 < 8
if (labs(cip.row - ip.row) > skipCount) { // 目标区域的 cell 的 indexPaths
NSArray * temp = [self.tableView indexPathsForRowsInRect:CGRectMake(0, targetContentOffset->y, self.tableView.frame.size.width, self.tableView.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]];
}
}
[needLoadArr addObjectsFromArray:arr];
}
}

思想:识别 UITableView 拖拽即将结束的时候,进行异步加载图片,快滑动过程中,只加载目标范围内的 Cell,这样按需加载,极大的提高流畅度。而 SDWebImage 可以实现异步加载,与这条性能配合就完美了,尤其是大量图片展示的时候。而且也不用担心图片缓存会造成内存警告的问题。

六、缓存 View

当 Cell 中的部分 View 是非常独立且不便于重用的,"体积"非常小,在内存可控的前提下,完全可以将这些 view 缓存起来。

七、尽量显示“大小刚好合适的”图片资源

避免大量的图片缩放、颜色渐变等。

八、避免同步的从网络、文件获取数据

Cell 内实现的内容来自 web,使用异步加载,缓存请求结果。

九、渲染

  1. 减少 subviews 的个数和层级

    子控件的层级越深,渲染到屏幕上所需要的计算量就越大;如多用 drawRect 绘制元素,替代用 view 显示。

  2. 少用 subviews 的透明图层

    渲染最耗时的操作之一就是混合(blending)了。对于不透明的 View,设置 opaque = YES,这样在绘制该 View 时,避免 GPU 对 View 覆盖的其他内容也进行绘制。

  3. 背景色不要使用 clearColor

  4. 避免 CALayer 特效(shadowPath)

    给 Cell 中 View 加阴影会引起性能问题,如下面代码会导致滚动时有明显的卡顿:

    view.layer.shadowColor   = color.CGColor;
    view.layer.shadowOffset = offset;
    view.layer.shadowOpacity = 1;
    view.layer.shadowRadius = radius;
  5. 当有图像时,预渲染图像,在 bitmap context 先将其画一遍,导出成 UIImage 对象,然后再绘制到屏幕,这会大大提高渲染速度。具体内容可以自行查找“利用预渲染加速显示 iOS 图像”相关资料。

十、总结

UITableView 的优化主要从四个方面入手:

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

大概需要关注的:

  1. cell 复用
  2. cell 高度的计算
  3. 渲染(混合问题)
  4. 减少视图的数目(重写 drawRect:)
  5. 减少多余的绘制操作
  6. 不要给 cell 动态添加 subView
  7. 异步化 UI,不要阻塞主线程
  8. 滑动时按需加载对应的内容

十一、资料

图片加载优化官方 Demo:LazyTableImages

文章:提升 UITableView 性能-复杂页面的优化

代码:VVeboTableViewDemo

优化UITableViewCell高度计算的那些事
UITableView+FDTemplateLayoutCell

iOS UITableView优化的更多相关文章

  1. 【腾讯Bugly干货分享】微信读书iOS性能优化

    本文来自于腾讯bugly开发者社区,非经作者同意,请勿转载,原文地址:http://dev.qq.com/topic/578c93ca9644bd524bfcabe8 “8小时内拼工作,8小时外拼成长 ...

  2. iOS 滑动性能优化

    iOS 滑动性能优化 目录 一. 减少图层的Blend操作 1. UIView的背景色避免使用clearColor 2. 控件贴图避免使用带alpha的图片 3. UIImageView 使用时避免半 ...

  3. UITableView优化那点事

    forkingdog关于UITableView优化的框架其实已经能够应用在一般的场景,且有蛮多的知识点供我们借鉴,借此站在巨人的肩膀上来分析一把. 至于UITableView的瓶颈在哪里,我相信网上随 ...

  4. [转] 详细整理:UITableView优化技巧

      原文:http://www.cocoachina.com/ios/20150602/11968.html   最近在微博上看到一个很好的开源项目VVeboTableViewDemo,是关于如何优化 ...

  5. IOS 性能优化的建议和技巧

    IOS 性能优化的建议和技巧 本文来自iOS Tutorial Team 的 Marcelo Fabri,他是Movile的一名 iOS 程序员.这是他的个人网站:http://www.marcelo ...

  6. uitableview 优化

    1. http://www.cocoachina.com/ios/20150602/11968.html 最近在微博上看到一个很好的开源项目VVeboTableViewDemo,是关于如何优化UITa ...

  7. UITableView 优化总结

    最近在微博上看到一个很好的开源项目VVeboTableViewDemo,是关于如何优化UITableView的.加上正好最近也在优化项目中的类似朋友圈功能这块,思考了很多关于UITableView的优 ...

  8. iOS tableView优化

    iOS: Autolayout和UITableViewCell的动态高度 http://www.mgenware.com/blog/?p=507 优化UITableViewCell高度计算的那些事 h ...

  9. iOS UITableView Tips(2)

    #TableView Tips(2) (本来想一章就结束TableView Tips,但是发现自己还是太天真了~too young,too simple) ##架构上的优化 在Tips(1)中指出了一 ...

随机推荐

  1. sql -- 获取商品分类的最新销售情况

    表设计: 需求: 1.先找出各个分类中销售的最新日期 select prod_class,max(sales_date) as sn from prod_sales group by prod_cla ...

  2. MySQL之单表多表查询

    #1.单表查询 #单表查询语法 select <字段1,字段2....> from <表名> where <表达式> group by field 分组 havin ...

  3. RocketMQ - 基础知识

    RocketMQ简介 RocketMQ是阿里开源的消息中间件,它是纯java开发,具有低延迟.高吞吐量.高可用性和适合大规模分布式系统应用的特点.从名字可以看出Rocket火箭,代表RocketMQ主 ...

  4. docker 搭建本地私有仓库

    1.使用registry镜像创建私有仓库 安装docker后,可以通过官方提供的 registry 镜像来简单搭建一套本地私有仓库环境: docker run -d -p : registry: 这将 ...

  5. angular -——组件样式修改不成功

    angular组件样式修改不成功! 自己定义的css可以成功 组件的不行 style在模板字符串里 直接没有 class 是显示的 但是样式不生效 加上面 即可,为什么?我也不太清楚.有知道答案的请回 ...

  6. JavaScript实现集合与字典

    JavaScript实现集合与字典 一.集合结构 1.1.简介 集合比较常见的实现方式是哈希表,这里使用JavaScript的Object类进行封装. 集合通常是由一组无序的.不能重复的元素构成. 数 ...

  7. SQLi-Labs之1~6关 - 常规注入与盲注

    年初五 财神入 第一关 联合注入 1.准备 2.加'截断 3.order by 判断查询列数 4.同上 5.联合查询判断字段位置 6.查数据库名 7.1 查表名 7.2 查列名 8.查数据 第二关 不 ...

  8. 使用 ALSAlib 播放 wav

    在 ARM 2440 开发板上正常播放 16bit  44100 采样率的wav , 为了程序简单,没有判断返回值. 补充,在 ubunto 上也能正常播放. 编译方法: arm-linux-gcc ...

  9. 【题解】NOIP 2015 子串

    淦!这题我做了三个月啊 题目描述 有两个仅包含小写英文字母的字符串 \(A\) 和 \(B\). 现在要从字符串 \(A\) 中取出 \(k\) 个互不重叠的非空子串,然后把这 \(k\) 个子串按照 ...

  10. Win2012+Nginx+IIS+xxfpm(服务版)

    这次做了一个项目部署在环境为win2012+nginx1.13.5+mysql5.6+php7的环境下,服务器是阿里云的 由于之前没有这种经验,遇到了点坑(据参考文章里说的这坑还有些年份了),最开始自 ...