优化一、善用重用标识
这个属于基础知识范畴,就不再过度的讲解了。只需了解使用 static 修饰重用标识名称能够保证这个标识只会创建一次,提高性能。接着就是调用dequeueReusableCellWithIdentifier:方法获取缓存池中的Cell。如果没有就调用 initWithStyle:ReusIdentifier:方法创建一个新的Cell。注意事先需要调用registerNib/registerClass方法为TableView注册一下重用标识。
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:cellIdentifier];
if (!cell) {
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:cellIdentifier];
}
需要注意的是我们在自定义Cell的时候需要尽量将相识度高的Cell合并为一种样式的Cell。我们知道运行在当前设备屏幕中的Cell数量是有限的,设为N个。如果有M种样式,那么基于Cell的重用机制,我们知道在缓存池中运行最多时将有M*N个Cell的实例被创建,这显然会占用大量的内存。
优化二、设置预估行高,预先缓存动态行高
1. 设置预估行高
我们知道UITableView是通过设置UITableView代理方法heightForRowAtIndexPath:方法来设置行高。自从iOS8.0之后,苹果新增了self-sizing cell的概念,也是cell可以自己计算行高,使用需要满足三个条件:
(1) 使用Autolayout进行UI布局约束
(2) 指定TableView的estimatedRowHeight属性的默认值
(3) 指定TableView的rowHeight的属性为UITableViewAutomaticDimension。
TableView在加载数据时会先通过estimatedHeightForRowAtIndexPath处理全部数据,此时我们只需要提供一个粗略的高度,待到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. 异步绘制
解决因图层层级多造成的性能问题,我们可以通过重写drawReact:方法,调用Core Graphics框架中的API进行异步绘制,提高效率。drawRect:本身是异步的。另外drawRect:中大量的绘制操作也会造成内存的增长,可以使用CAShapeLayer来代替。
3. 减少多于的绘制操作
在实现drawRect:方法的时候,他的参数rect就是我们需要绘制的区域,在rect范围之外的区域不要绘制,否则会消耗相当大的资源。
4. 图片加载时机选择
首先在Cell类中添加图片应该避免使用imageWithName:方法,因为该方法会将图片缓存到内存中。而是应该使用imageWithContensOfFile:方法来替换,该方法在图片使用完后系统会自动释放资源,并不会缓存下来。另外结合SDWebImage框架的使用可以显著的提高图片加载的性能。
5. 避免动态添加图层
在cell中应该尽量避免动态创建图层。在初始化cell的时候一并将所有图层预先创建好,通过hidden属性控制子图层的显示或隐藏,因为单纯的显示操作要比创建快的多。
6. 避免离屛渲染
- 为图层设置遮罩(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在drawReact:方法中绘制
上述情况均会造成离屛渲染。
什么是离屛渲染?我们知道iOS底层的渲染框架使用的是OpenGL ES。OpenGL中,GPU渲染屏幕方式有两种:当前屏幕渲染(On-Screen Rendering)和离屛渲染(Off-Screen Rendering)。它们的区别是当前屏幕渲染操作是在当前显示的屏幕缓冲区完成,而离屛渲染会在另外一个新开辟的缓冲区完成渲染操作。开启离屛渲染的代价就是需要新开辟一块新的缓冲区,在渲染的过程中还会多次的切换上下文,这些都是很消耗性能的。
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];
}