文章来源
iOS性能优化--UITableView
优化方案
一、善用重用标识
二、设置预估行高,预先缓存动态行高
三、减少SubViews
层级、异步绘制、避免离屏渲染、使用Hidden
隐藏图层
四、分屏加载数据,预先异步请求数据
五、滑动TableView
时,按需加载内容
六、cell
类中应该避免请求网络加载数据
七、在willDisplayCell:forRowAtIndexPath:
代理方法中的绑定数据
一、善用重用标识
这个属于基础知识范畴,就不再过度的讲解了。
只需要了解使用static
修饰重用标识名称能够保证这个标识只会创建一次,提高性能。接着就是调用dequeueReusableCellWithIdentifier:
方法获取缓存池中的Cell
。如果没有就调用initWithStyle:ReusIdentifier:
方法创建一个新的Cell
。注意事先需要调用registerNib/registerClass
方法为TableView
注册一下标识。
二、设置预估行高,预先缓存动态行高
1. 设置预估行高
我们知道UITableView
是通过UITableView
代理方法heightForRowAtIndexPath:
方法来设置行高。自从iOS8.0
之后,苹果新增了self-sizing cell
的概念,也是cell
可以自己计算行高,使用需要满足是三个条件:
(1) 使用
Autolayout
进行UI布局约束
(2) 指定TableView
的estimatedRowHeight
属性的默认值
(3) 指定TableView
的rowHeight
的属性为UITableViewAutomaticDimension
。
TableView在加载数据时会先通过estimatedRowHeight:AtIndexPath
处理全部数据,此时我们只需要提供一个粗略的高度,待到cell
对象创建之后再去设置cell
的真实高度。而且只会处理当前屏幕范围内的cell
,这样子会显著的提升加载的性能。
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
return 50.0;
}
- (CGFloat)tableView:(UITableView *)tableView estimatedHeightForRowAtIndexPath:(NSIndexPath *)indexPath {
return 30.0;
}
2.预先计算并缓存行高
自从iOS8.0
之后,TableView
的数据源的调整时序也发生了变化,下图左边为iOS7.0
以及之前的时序,右边为iOS8.0
以及之后的时序:
从上图可以很容易的分析出,iOS8.0
之后在获取cell对象之后会再次调用heightForRowAtIndexPath:
方法获取行高,这也就意味着我们其实可以先创建cell
对象,之后再提供行高。具体方法我们可以在cell
类中添加layoutAttribute
属性,记录相应的UIEdgeInsets
,然后在设置cell
真实高度的时候返回。iOS7.0
之前则必须在cell
对象处啊给你讲爱你之前先获得所有cell
的高度。
三、减少SubViews
层级、异步绘制、避免离屏渲染、使用Hidden
隐藏图层
1. 减少图层层级数量
当我们自定义某个
cell
,并在cell
上添加大量的系统控件后,在创建该cell
对象时系统会调用底层接口进行绘制,大量的添加操作会消耗很大的资源同时会影响渲染的性能。
2. 异步绘制
解决因图层层级多造成的性能问题,我们可以通过
drawRect:
方法,调用Core Graphics
框架中的API
进行异步绘制,提高效率。drawRect:
本身是移步的。另外drawRect:
中大量的绘制也会造成内存的增长,可以使用CAShapeLayer
来代替。
3. 减少多于的绘制操作
在实现
drawRect:
方法的时候,他的rect参数就是我们需要绘制的区域,在rect范围之外的区域不要绘制,否则会消耗相当大的资源。
4. 图片加载时机选择
首先在cell中添加图片应该尽量避免使用
imageWithNamed:
方法,因为该方法会将图片缓存到内存中。而且应该使用imageWithContentsOfFile:
方法来替换,该方法在图片使用完后系统会自动释放资源,并不会缓存下来。另外结合SDWebImage
框架的使用可以显著提高图片加载的性能。
5. 避免动态添加图层
在cell中应该尽量避免动态创建图层。在初始化
cell
的时候一并将所有的图层预先创建好,通过hidden
属性控制子图层的显示或隐藏,因为单纯的显示操作要比创建快得多。
6. 避免离屏渲染
什么是离屏渲染?我们知道iOS底层的渲染框架使用的是OpenGL ES
。OpenGL
中,GPU
渲染屏幕方式有两种:当前屏幕渲染(On-Screen Rendering
)和离屏渲染(Off-Screen Rendering
)。它们的区别是当前屏幕渲染操作是在当前屏幕缓冲区完成,而离屏渲染会在另外一个新开辟的缓冲区完成渲染操作,开启离屏渲染的代价就是需要新开辟一块新的缓冲区,在渲染的过程中还会多次切换上下文,这些都是很消耗性能的。
- 为图层设置遮罩(
layer.mask
) - 设置图层的
layer.masksToBounds/view.clipsToBounds
属性为True
- 设置图层的
layer.allowsGroupOpacity
的属性为True
和layer.opacity
小于1.0 - 设置图层阴影(
layer.shadow
) - 设置图层的
layer.shouldRasterize
的属性为True
- 具有
layer.cornerRadius
,layer.edgeAntialiasingMask
,layer.allowsAntialiasing
的图层 - 文本(任何种类,包括
UILabel
、CATextLayer
、Core Text
等) - 使用
CGContext
在drawRect:
方法中绘制
上述情况均会造成离屏渲染。
7. 图片圆角优化
使用贝塞尔曲线 + Core Graphics
框架设置圆角
- (void)setImageCircularEdge:(UIImageView *)imageView {
//开始对imageView进行画图
UIGraphicsBeginImageContextWithOptions(imageView.bounds.size, NO, 1.0);
//使用贝塞尔曲线画出一个圆形图
[[UIBezierPath bezierPathWithRoundedRect:imageView.bounds cornerRadius:imageView.frame.size.width] addClip];
[imageView drawRect:imageView.bounds];
imageView.image = UIGraphicsGetImageFromCurrentImageContext();
//结束画图
UIGraphicsEndImageContext();
}
使用贝塞尔曲线 + CAShapeLayer
设置圆角
- (void)setImageCircularEdge2:(UIImageView *)imageView {
UIBezierPath *maskPath = [UIBezierPath bezierPathWithRoundedRect:imageView.bounds byRoundingCorners:UIRectCornerAllCorners cornerRadii:imageView.bounds.size];
CAShapeLayer *maskLayer=[[CAShapeLayer alloc] init];
//设置大小
maskLayer.frame = imageView.bounds;
//设置图形样子
maskLayer.path = maskPath.CGPath;
imageView.layer.mask = maskLayer;
}
8. 图片阴影优化
- (void)setImageShadow:(UIImageView *)imageView {
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;
}
四、分屏加载数据,预先异步请求数据
在我们的项目开发中列表视图的应用很多,有时数据比较多的时候我们不可能一次加载所有数据,这样子会导致内存的暴涨,同时用户不一定会浏览所有的信息,造成资源浪费。这时我们可以通过分屏加载来解决这个问题,比如第一次加载10条数据,当我向上滑动列表的时候通常我们会再次去请求数据接口获取下一个10条数据。这个时候如果我们不做任何的处理,那么我会发现每次划过10条数据的时候列表都需要停顿一下,等待数据加载。这样子我们的列表就表现的不是很流畅了,那么怎么解决这个问题呢?
提前异步预加载数据!第一次加载完10条数据之后可以再预先加载下10条数据,当划过第10条数据时,再请求下10条数据。这样子我们的列表就表现的很流畅了。
五、滑动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];
}
}
targetContentOffset
是TableView
减速到停止的地方,velocity
表示速度向量。
六、cell类中应该避免请求网络加载数据
如果确实有需求不可避免,可以将网络加载任务添加到Runloop
中,设置DefaultRunloopModule
模式。这样子可以起到延迟加载的作用。
七、在willDisplayCell:forRowAtIndexPath:代理方法中的绑定数据
初学iOS的时候,各类教程以及书籍中都喜欢在cellForRowAtIndexPath:
方法中绑定数据,然后此时的cell
其实还未显示,该方法中包含了大量的布局、绘制相关的操作。我们应该在该方法中尽量简化我们自身的逻辑操作。这时我们可以使用在willDisplayCell:forRowAtIndePath:
方法中绑定数据。
#pragma mark - UITableViewDataSource
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
static NSString *cellIdentifier = @"MyTableViewCell";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:cellIdentifier];
if (!cell) {
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:cellIdentifier];
}
return cell;
}
#pragma mark - UITableViewDelegate
- (void)tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath {
NSDictionary *dict = self.dataList[indexPath];
[cell updateData:dict];
}
性能优化