前言
- UITableView是我们经常会使用的控件,那么关于这块的优化还是很有必要,网上关于这块优化的资料很多,其实核心本质还是降低 CPU和GPU 的工作来提升性能
CPU:对象的创建和销毁、对象属性的调整、布局计算、文本的计算和排版、图片的格式转换和解码、图像的绘制
GPU:纹理的渲染
CPU层面优化
1.用轻量级对象
比如用不到事件处理的地方,可以考虑使用 CALayer 取代 UIView
CALayer * imageLayer = [CALayer layer];
imageLayer.bounds = CGRectMake(0,0,200,100);
imageLayer.position = CGPointMake(200,200);
imageLayer.contents = (id)[UIImage imageNamed:@"xx.jpg"].CGImage;
imageLayer.contentsGravity = kCAGravityResizeAspect;
[tableCell.contentView.layer addSublayer:imageLayer];
2.不要频繁地调用 UIView 的相关属性
比如 frame、bounds、transform 等属性,尽量减少不必要的修改
不要给UITableViewCell动态添加subView,可以在初始化UITableViewCell的时候就将所有需要展示的添加完毕,然后根据需要来设置hidden
属性显示和隐藏
3.提前计算好布局
UITableViewCell高度计算主要分为两种,一种固定高度,另外一种动态高度.
固定高度:
rowHeight
高度默认44
对于固定高度直接采用self.tableView.rowHeight = 77
比tableView:heightForRowAtIndexPath:
更高效
动态高度:
采用tableView:heightForRowAtIndexPath:
这种代理方式,设置这种代理之后rowHeight
则无效,需要满足以下三个条件
- 使用Autolayout进行UI布局约束(要求cell.contentView的四条边都与内部元素有约束关系)
- 指定TableView的
estimatedRowHeight
属性的默认值 - 指定TableView的
rowHeight
属性为UITableViewAutomaticDimension
self.tableView.rowHeight = UITableViewAutomaticDimension;
self.tableView.estimatedRowHeight = 44;
除了提高cell高度的计算效率之外,对于已经计算出的高度,我们需要进行缓存
4.直接设置frame
Autolayout 会比直接设置 frame 消耗更多的 CPU 资源
5.图片尺寸合适
图片的 size 最好刚好跟 UIImageView 的 size 保持一致
图片通过contentMode处理显示,对tableview滚动速度同样会造成影响
- 从网络下载图片后先根据需要显示的图片大小切/压缩成合适大小的图,每次只显示处理过大小的图片,当查看大图时在显示大图。
- 服务器直接返回预处理好的小图和大图以及对应的尺寸最好
/// 根据特定的区域对图片进行裁剪
+ (UIImage*)kj_cutImageWithImage:(UIImage*)image Frame:(CGRect)cropRect{
return ({
CGImageRef tmp = CGImageCreateWithImageInRect([image CGImage], cropRect);
UIImage *newImage = [UIImage imageWithCGImage:tmp scale:image.scale orientation:image.imageOrientation];
CGImageRelease(tmp);
newImage;
});
}
6.控制最大并发数量
控制一下线程的最大并发数量,当下载线程数超过2时,会显著影响主线程的性能。因此在使用ASIHTTPRequest时,可以用一个NSOperationQueue来维护下载请求,并将其最大线程数目maxConcurrentOperationCount
。
NSURLRequest可以配合 GCD进阶技巧分享 来实现,或者使用NSURLConnection的setDelegateQueue:
方法。
当然在不需要响应用户请求时,也可以增加下载线程数来加快下载速度:
- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate{
if (!decelerate) self.queue.maxConcurrentOperationCount = 5;
}
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView{
self.queue.maxConcurrentOperationCount = 5;
}
- (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView{
self.queue.maxConcurrentOperationCount = 2;
}
7.子线程处理
尽量把耗时的操作放到子线程
- 文本处理(尺寸计算、绘制)
- 图片处理(解码、绘制)
8.预渲染图像
显示图像时,解压和重采样会消耗很多CPU时间,
当有图像时,在bitmap context
先将其画一遍,导出成UIImage对象,然后再绘制到屏幕,这会大大提高渲染速度,
- (void)awakeFromNib {
if (self.image == nil) {
self.image = [UIImage imageNamed:@"xxx"];
UIGraphicsBeginImageContextWithOptions(imageSize, YES, 0);
[image drawInRect:imageRect];
self.image = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
}
}
9.异步绘制
异步绘制,就是异步在画布上绘制内容,将复杂的绘制过程放到后台线程中执行,然后在主线程显示
// 异步绘制,切换至子线程
dispatch_async(dispatch_get_global_queue(0, 0), ^{
UIGraphicsBeginImageContextWithOptions(size, NO, scale);
CGContextRef context = UIGraphicsGetCurrentContext();
// TODO:draw in context...
CGImageRef imgRef = CGBitmapContextCreateImage(context);
UIGraphicsEndImageContext();
dispatch_async(dispatch_get_main_queue(), ^{
self.layer.contents = imgRef;
});
});
这篇文章 iOS-UIView异步绘制 介绍的满详细
当然还是少不了YY大神的佳作,iOS 保持界面流畅的技巧(转载)
10.按需求加载
滑动UITableView时,按需加载对应的内容
//按需加载 - 如果目标行与当前行相差超过指定行数,只在目标滚动范围的前后指定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 = 10;
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 > 3) {
[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]];
}
}
[self.needLoadDatas addObjectsFromArray:arr];
}
}
还需要在tableView:cellForRowAtIndexPath:
方法中加入判断
if (self.needLoadDatas.count > 0 && [self.needLoadDatas indexOfObject:indexPath] == NSNotFound) {
//TODO:清理工作
return;
}
GPU层面优化
1.避免短时间内大量显示图片
尽可能将多张图片合成一张进行显示
- RunLoop小操作
当前线程是主线程时,某些UI事件,比如ScrollView正在拖动,将会RunLoop切换成NSEventTrackingRunLoopMode
模式,在这个模式下默认的NSDefaultRunLoopMode
模式中注册的事件是不会执行
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"cell"];
if (!cell) {
cell = [[UITableViewCell alloc]initWithStyle:UITableViewCellStyleDefault reuseIdentifier:@"cell"];
}
cell.selectionStyle = UITableViewCellSelectionStyleNone;
KJTestModel *model = self.datas[indexPath.row];
if (model.iconImage) {
cell.imageView.image = model.iconImage;
}else{
NSDictionary *dict = @{@"imageView":cell.imageView,@"model":model};
[self performSelector:@selector(kj_loadImageView:) withObject:dict afterDelay:0.0 inModes:@[NSDefaultRunLoopMode]];
}
cell.nameLabel.text = model.name;
cell.IDLabel.hidden = model.remarkName == nil ? YES : NO;
cell.label.text = model.remarkName;
}
/// 下载图片,并渲染到cell上显示
- (void)kj_loadImageView:(NSDictionary*)dict{
UIImageView *imageView = dict[@"imageView"];
[imageView sd_setImageWithURL:model.avatar completed:^(UIImage * _Nullable image, NSError * _Nullable error, SDImageCacheType cacheType, NSURL * _Nullable imageURL) {
KJTestModel *model = dict[@"model"];
model.iconImage = image;
}];
}
2.控制尺寸
GPU能处理的最大纹理尺寸是4096x4096
,超过这个尺寸就会占用CPU资源进行处理,所以纹理尽量不要超过这个尺寸
3.减少图层混合操作
当多个视图叠加,放在上面的视图是半透明的,那么这个时候GPU就要进行混合,把透明的颜色加上放在下面的视图的颜色混合之后得出一个颜色再显示在屏幕上,这一步是消耗GPU资源
- UIView的backgroundColor不要设置为clearColor,最好设置和superView的backgroundColor颜色一样。
- 图片避免使用带alpha通道的图片
4.透明处理
减少透明的视图,不透明的就设置opaque = YES
5.避免离屏渲染
离屏渲染就是在当前屏幕缓冲区以外,新开辟一个缓冲区进行操作
离屏渲染的整个过程,需要多次切换上下文环境,先是从当前屏幕切换到离屏;等到离屏渲染结束以后,将离屏缓冲区的渲染结果显示到屏幕上,又需要将上下文环境从离屏切换到当前屏幕
1 - 下面的情况或操作会引发离屏渲染
- 光栅化,layer.shouldRasterize = YES
- 遮罩,layer.mask
- 圆角,同时设置 layer.masksToBounds = YES 和 layer.cornerRadius > 0
- 阴影,layer.shadow
- layer.allowsGroupOpacity = YES 和 layer.opacity != 1
- 重写drawRect方法
2 - 圆角优化
这里主要其实就是解决同时设置layer.masksToBounds = YES
和layer.cornerRadius > 0
就会产生的离屏渲染
其实我们在使用常规视图切圆角时,可以只使用view.layer.cornerRadius = 3.0
,这时是不会产生离屏渲染
但是UIImageView
这家伙有点特殊,切圆角时必须上面2句同时设置,则会产生离屏渲染,所以我们考虑通过 CoreGraphics 绘制裁剪圆角,或者叫美工提供圆角图片
- (UIImage *)kj_ellipseImage{
UIGraphicsBeginImageContextWithOptions(self.size, NO, 0.0);
CGContextRef ctx = UIGraphicsGetCurrentContext();
CGRect rect = CGRectMake(0, 0, self.size.width, self.size.height);
CGContextAddEllipseInRect(ctx, rect);
CGContextClip(ctx);
[self drawInRect:rect];
UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
return image;
}
镂空圆形图片覆盖,此方法可以实现圆形头像效果,这个也是极为高效的方法。缺点就是对视图的背景有要求,单色背景效果就最为理想
3 - 阴影优化
对于shadow,如果图层是个简单的几何图形或者圆角图形,我们可以通过设置shadowPath来优化性能,能大幅提高性能
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;
4 - 强制开启光栅化
当图像混合了多个图层,每次移动时,每一帧都要重新合成这些图层,十分消耗性能,这时就可以选择强制开启光栅化layer.shouldRasterize = YES
当我们开启光栅化后,会在首次产生一个位图缓存,当再次使用时候就会复用这个缓存,但是如果图层发生改变的时候就会重新产生位图缓存。
所以这个功能一般不能用于UITableViewCell中,复用反而降低了性能。最好用于图层较多的静态内容的图形
5 - 优化建议
- 使用中间透明图片蒙上去达到圆角效果
- 使用ShadowPath指定layer阴影效果路径
- 使用异步进行layer渲染
- 将UITableViewCell及其子视图的opaque属性设为YES,减少复杂图层合成
- 尽量使用不包含透明alpha通道的图片资源
- 尽量设置layer的大小值为整形值
- 背景色的alpha值应该为1,例如不要使用clearColor
- 直接让美工把图片切成圆角进行显示,这是效率最高的一种方案
- 很多情况下用户上传图片进行显示,可以让服务端处理圆角
最后简单介绍TableViewCell的部分常用属性
功能 | API & Property |
---|---|
设置分割线颜色 | [tableView setSeparatorColor:UIColor.orangeColor] |
设置分割线样式 | tableView.separatorStyle = UITableViewCellSeparatorStyleSingleLine |
是否允许多选 | tableView.allowsMultipleSelection |
是否响应点击操作 | tableView.allowsSelection = YES |
返回选中的多行 | tableView.indexPathsForSelectedRows |
可见的行 | tableView.indexPathsForVisibleRows |