一. cellForRowAtIndexPath:
我们经常在注意 cellForRowAtIndexPath :中为每一个 cell 绑定数据,实际上在调用
cellForRowAtIndexPath:的时候 cell 还没有被显示出来,为了提高效率我们应该把数据绑定的
操作放在 cell 显示出来后再执行,可以在tableView:willDisplayCell:forRowAtIndexPath:
(以后简称willDisplayCell)方法中绑定数据。
注意: willDisplayCell 在 tableview 展示之前就会调用,此时 cell 实例已经生成,所以不
能更改 cell 的结构,只能是改动 cell 上的 UI 的一些属性(例如 label 的内容等)。
二. cell 高度的计算
这边我们分为两种 cell,一种是定高的 cell,另外一种是动态高度的 cell。
-
定高的 cell,应该采用如下方式:
self.tableView.rowHeight = 88;
这个方法指定了所有 cell 高度都是 88 的 tableview,rowHeight 默认的值是 44。
对于定高cell,直接采用上面方式给定高度,不需要实现 tableView:heightForRowAtIndexPath:以节省不必要的计算和开销。 -
动态高度的 cell
我们需要实现它的代理,来给出高度:- (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)指定TableView的estimatedRowHeight属性的默认值。
(3)指定TableView的rowHeight属性为UITableViewAutomaticDimension。- (void)viewDidload { self.myTableView.estimatedRowHeight = 44.0; self.myTableView.rowHeight = UITableViewAutomaticDimension; }
除了提高 cell 高度的计算效率之外,对于已经计算出的高度,我们需要进行缓存,对于已经计算过的高度,没有必要进行计算第二次。
三. 渲染
为了保证 TableView 的流畅,当快速滑动的时候,cell 必须被快速的渲染出来。所以 cell 渲染的速度必须快。
如何提高 cell 的渲染速度呢?
(1)当有图像时,预渲染图像,在bitmap context先将其画一遍,导出成UIImage对象,然后再绘制到屏幕,这会大大提高渲染速度。具体内容可以自行查找“利用预渲染加速显示iOS图像”相关资料。
(2)渲染最好时的操作之一就是混合(blending)了,所以我们不要使用透明背景,将cell的opaque值设为Yes,背景色不要使用clearColor,尽量不要使用阴影渐变等。
(3)由于混合操作是使用GPU来执行,我们可以用CPU来渲染,这样混合操作就不再执行。可以在UIView的drawRect方法中自定义绘制。
四. 异步化UI,不要阻塞主线程
我们时常会看到这样一个现象,就是加载时整个页面卡住不动,怎么点都没用,仿佛死机了一
般。原因是主线程被阻塞了。
所以对于网路数据的请求或者图片的加载,我们可以开启多线程,将耗时操作放到子线程中进行,异步化操作。这个或许每个iOS开发者都知道的知识,不必多讲。
五. 滑动时按需加载对应的内容
如果目标行与当前行相差超过指定行数,只在目标滚动范围的前后指定3行加载。
- (void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(inout CGPoint *)targetContentOffset {
NSIndexPath *ip=[self indexPathForRowAtPoint:CGPointMake(0,targetContentOffset->y)];
NSIndexPath *cip = [[self indexPathsForVisibleRows] firstObject];
NSInteger skipCount=8;
if (labs(cip.row-ip.row) > skipCount) {
NSArray *temp=[self indexPathsForRowsInRect:CGRectMake(0,targetContentOffset->y,self.width,self.height)];
NSMutableArray *arr=[NSMutableArray arrayWithArray:temp];
if(velocity.y < 0){
NSIndexPath *indexPath=[temp lastObject];
if(indexPath.row + 33){
[arr addObject:[NSIndexPath indexPathForRow:indexPath.row3 inSection:0]];
[arr addObject:[NSIndexPath indexPathForRow:indexPath.row-2 inSection:0]];
[arr addObject:[NSIndexPath indexPathForRow:indexPath.row-1 inSection:0]]; }
}
[needLoadArr addObjectsFromArray:arr];
}
}
记得在 tableView:cellForRowAtIndexPath: 方法中加入判断:
if (needLoadArr.count > 0
&& [needLoadArr indexOfObject:indexPath] == NSNotFound) {
[cell clear];
return;
}
滑动很快时,只加载目标范围内的cell,这样按需加载(配合SDWebImage),极大提高流畅度。
六. 离屏渲染的问题:
-
下面的情况或操作会引发离屏渲染:
• 为图层设置遮罩(layer.mask)
• 将图层的layer.masksToBounds / view.clipsToBounds属性设置为true
• 将图层layer.allowsGroupOpacity属性设置为YES和layer.opacity小于1.0
• 为图层设置阴影(layer.shadow *)
• 为图层设置layer.shouldRasterize=true
• 具有layer.cornerRadius,layer.edgeAntialiasingMask,layer.allowsEdgeAntialiasing 的图层
• 文本(任何种类,包括UILabel,CATextLayer,Core Text等)
• 使用CGContext在drawRect :方法中绘制大部分情况下会导致离屏渲染,甚至仅仅是一个空的实现 -
优化方案
官方对离屏渲染产生性能问题也进行了优化:iOS 9.0 之前UIimageView跟UIButton设置圆角都会触发离屏渲染。
iOS 9.0 之后UIButton设置圆角会触发离屏渲染,而UIImageView里png图片设置圆角不会触发离屏渲染了,如果设置其他阴影效果之类的还是会触发离屏渲染的。
(1)圆角优化
在APP开发中,圆角图片还是经常出现的。如果一个界面中只有少量圆角图片或许对性能没有非常大的影响,但是当圆角图片比较多的时候就会APP性能产生明显的影响。我们设置圆角一般通过如下方式:
imageView.layer.cornerRadius=CGFloat(10); imageView.layer.masksToBounds=YES;
这样处理的渲染机制是GPU在当前屏幕缓冲区外新开辟一个渲染缓冲区进行工作,也就是离屏渲染,这会给我们带来额外的性能损耗,如果这样的圆角操作达到一定数量,会触发缓冲区的频繁合并和上下文的的频繁切换,性能的代价会宏观地表现在用户体验上——掉帧。
优化方案1: 使用贝塞尔曲线UIBezierPath和Core Graphics框架画出一个圆角
UIImageView *imageView = [[UIImageView alloc]initWithFrame:CGRectMake(100, 100, 100, 100)]; imageView.image = [UIImage imageNamed:@"myImg"]; //开始对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(); [self.view addSubview:imageView];
优化方案2: 使用CAShapeLayer和UIBezierPath设置圆角
UIImageView *imageView=[[UIImageView alloc]initWithFrame:CGRectMake(100,100,100,100)]; imageView.image=[UIImage imageNamed:@"myImg"]; UIBezierPath *maskPath=[UIBezierPath bezierPathWithRoundedRect:imageView.bounds byRoundingCorners:UIRectCornerAllCornerscornerRadii:imageView.bounds.size]; CAShapeLayer *maskLayer=[[CAShapeLayer alloc]init]; //设置大小 maskLayer.frame=imageView.bounds; //设置图形样子 maskLayer.path=maskPath.CGPath; imageView.layer.mask=maskLayer; [self.view addSubview:imageView];
对于方案2需要解释的是:
• CAShapeLayer继承于CALayer,可以使用CALayer的所有属性值;
• CAShapeLayer需要贝塞尔曲线配合使用才有意义(也就是说才有效果)
• 使用CAShapeLayer(属于CoreAnimation)与贝塞尔曲线可以实现不在view的drawRect(继承于CoreGraphics走的是CPU,消耗的性能较大)方法中画出一些想要的图形
• CAShapeLayer动画渲染直接ᨀ交到手机的GPU当中,相较于view的drawRect方法使用CPU渲染而言,其效率极高,能大大优化内存使用情况。总的来说就是用CAShapeLayer的内存消耗少,渲染速度快,建议使用优化方案2。
(2)shadow优化
对于shadow,如果图层是个简单的几何图形或者圆角图形,我们可以通过设置shadowPath来优化性能,能大幅提高性能。示例如下:imageView.layer.shadowColor=[UIColorgrayColor].CGColor; imageView.layer.shadowOpacity=1.0; imageView.layer.shadowRadius=2.0; UIBezierPath *path=[UIBezierPath bezierPathWithRect:imageView.frame]; imageView.layer.shadowPath=path.CGPath;
我们还可以通过设置shouldRasterize属性值为YES来强制开启离屏渲染。其实就是光栅化(Rasterization)。
既然离屏渲染这么不好,为什么我们还要强制开启呢?
当一个图像混合了多个图层,每次移动时,每一帧都要重新合成这些图层,十分消耗性能。当我们开启光栅化后,会在首次产生一个位图缓存,当再次使用时候就会复用这个缓存。
但是如果图层发生改变的时候就会重新产生位图缓存。所以这个功能一般不能用于UITableViewCell中,cell的复用反而降低了性能。最好用于图层较多的静态内容的图形。
而且产生的位图缓存的大小是有限制的,一般是2.5个屏幕尺寸。在100ms之内不使用这个缓存,缓存也会被删除。所以我们要根据使用场景而定。
(3)其他的一些优化建议
• 当我们需要圆角效果时,可以使用一张中间透明图片蒙上去
• 使用ShadowPath指定layer阴影效果路径
• 使用异步进行layer渲染(Facebook开源的异步绘制框架AsyncDisplayKit) • 设置layer的opaque值为YES,减少复杂图层合成
• 尽量使用不包含透明(alpha)通道的图片资源
• 尽量设置layer的大小值为整形值
• 直接让美工把图片切成圆角进行显示,这是效率最高的一种方案
• 很多情况下用户上传图片进行显示,可以让服务端处理圆角
• 使用代码手动生成圆角Image设置到要显示的View上,利用UIBezierPath(CoreGraphics框架)画出来圆角图片。