1.介绍UITableView
UITableView 是 iOS 开发中我们经常使用到的一个控件之一 , 它是用来展示列表数据的 , 我们使用的App中,基本上随处都是UITableView。UITableView只有行没有列,每一行都是一个UITableViewCell,如果我们查看UITableViewCell的声明文件可以发现在内部有一个UIView控件(contentView,作为其他元素的父控件)、两个UILable控件(textLabel detailTextLabel)、一个UIImageView控件,分别用于容器、显示内容、详情和图片。这些控件并不一定要全部显示,可以根据需要设置。但我们一般不会使用系统自带的UITableViewCell,一般我们都会自定义UITableViewCell,因为系统自带的UITableViewCell往往很难满足产品的需求。
2.性能优化的必要性
基于UITableView使用的频繁程度,优化就显得非常重要了。他关乎到用户体验,大家试想一下,用户拿着一个界面很卡的App,他会不会有一种想秒删的想法?反正我有。
3.优化开始
1.UITableViewCell的重用机制
我们知道UITableView是以UITableViewCell为单元展示的,当我们的数据量很大的时候就需要创建很多的UITableViewCell,如果一次性创建那么多的UITableViewCell是很消耗CPU和GPU的。因此重用UITableViewCell就显得十分必不可少了,这是保持UITableView流畅最核心的思想。简单理解就是UITableView刚出现的时候只会创建一屏幕(或者一屏幕多一点)的UITableViewCell,其他的Cell都是从重用池中重用的。每当Cell滑出屏幕时,就会把这个滑出屏幕的Cell放入重用池中。当要显示某一位置的Cell时,会根据重用标识去重用池中取,如果有,就直接拿出来显示,如果没有,才会创建新的Cell。这样做的好处可想而知,极大的减少了内存的开销,因为我们循环重复使用这些cell,而不需要消耗内存去创建大量的Cell,并且也达到了效果和实现了数据和显示的分离。
1.1注册Cell(纯码)
[self.tableView registerClass:[UITableViewCell class] forCellReuseIdentifier: identifier] ;
1.2创建Cell(纯码)
-(UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
// 重用队列中取单元格 由于上面已经注册过单元格,系统会帮我们做判断,不用再次手动判断单元格是否存在
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier: identifier forIndexPath:indexPath] ;
return cell ;
}
1.3 产品方面的性能优化
产品在设计的时候尽量使用统一规格的Cell,不仅仅是能减少设计不同Cell所需要的代码量和xib文件,更重要的是能提高Cell的重用率,提升TableView整体性能。能设计等高的Cell就设计等高的Cell,避免CUP计算量,动态计算高度的Cell也应该统一设计。
1.4 接口
如果数据量过多, 接口返回过来的数据尽量不要一次性的全部返回,而是分批次的返回。项目中可以使用MJRefresh来加入上拉刷新和下拉刷新操作。
2.UITableViewCell赋值和计算分离
如果现在要显示100个Cell,当前屏幕显示5个。那么刷新(reload)UITableView时,UITableView会先调用100次tableView:heightForRowAtIndexPath:方法,然后调用5次tableView:cellForRowAtIndexPath:方法;滚动屏幕时,每当Cell滚入屏幕,都会调用一次tableView:heightForRowAtIndexPath:、tableView:cellForRowAtIndexPath:方法。基于这种调用顺序,所以tableView:cellForRowAtIndexPath:方法只负责赋值,tableView:heightForRowAtIndexPath:方法只负责计算Cell高度。如果Cell的高度都是一样的,就不要重载tableView:heightForRowAtIndexPath:代理方法,直接在初始化TableView的地方赋值给rowHeight属性,同理各组头或各组尾如果高度一样的话,应该用sectionFooterHeight和sectionHeaderHeight,而不是代理方法。基于这种分离我们可以在获取数据后,直接先根据数据源计算出Cell的高度,并缓存到数据源中,而不是当Cell滚入屏幕时才计算,这个时候如果用户滚动速度很快的话就会造成界面卡顿,影响体验。
3.自定义Cell的异步绘制
如果UITableViewCell是像朋友圈那样的图文混排,因为界面很复杂,就需要使用自定义Cell的异步绘制。因为我们在Cell上添加系统控件的时候,实际上系统都需要调用底层的接口进行绘制,当我们大量添加控件时,对资源的开销也会很大,所以我们索性直接绘制,提高效率。给自定义的Cell添加draw方法或者也可以重写drawRect,如果在重写drawRect方法就不需要用GCD异步线程了,因为drawRect本来就是异步绘制的。代码较复杂,自行Google。
4.滑动UITableView时,按需加载对应的内容
4.1 滚动很快时,只加载目标范围内的Cell,这样按需加载极大的提高流畅度。
//按需加载 - 如果目标行与当前行相差超过指定行数,只在目标滚动范围的前后指定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.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];
}}
记得在tableView:cellForRowAtIndexPath:方法中加入判断
if (needLoadArr.count>0&&[needLoadArr indexOfObject:indexPath]==NSNotFound) {
[cell clear];
return;
}
4.2 SDWebImage已经实现异步加载图片,若用户滚动太快, 未下载下来的图片先用一个占位图占位,因此使用SDWebImage下载图片,有效的提高UITableView滚动的流畅性。
5.尽量使用所有View的不透明(opaque),包括Cell自身。尽量少用或者不用透明图层。因为渲染出透明图层其实是非常耗费性能的。
6.减少subviews的数量和层级数,子控件的层级数越深,渲染到屏幕上所需要的计算量就会越大;
7.尽量少用addView给Cell动态添加View,可以初始化时就添加,然后通过hide来控制是否显示。试想一下,TableView 在快速滚动的时候,还需要腾出性能去动态的往Cell上添加子控件,是一件比较耗费性能的事。
8.复杂的需要高效的界面Cell,尽量使用纯手码。Xib,StoryBoard需要系统自动转码,其实是给系统多加了一层负担。房帮帮+ 项目 iOS 版为了追求高性能,80%以上是以纯手码的方式开展开发。
9.耗时操作一定放在子线程,刷新UI的操作一定放在主线程。避免同步的从网络、文件获取数据。
10.圆角优化
在App开发中,圆角图片还是经常出现的。如果一个界面只有少量圆角图片对性能的影响不大,但是当圆角图片比较多的时候就会对App性能产生明显的影响。
通常我们处理圆角一般使用两个方法来处理:
imageView.layer.cornerRadius = 10;
imageView.layer.maskToBounds = 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设置圆角(比较处理方式1更优)
UIImageView*imageView = [[UIImageView alloc] initWithFrame:CGRectMake(100,100,100,100)];
imageView.image = [UIImage imageNamed:@"myImg"];
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;
[self.view addSubview:imageView];
CAShapeLayer : 1)CAShapeLayer继承于CALayer,可以使用CALayer的所有属性值. 2)CAShapeLayer需要贝塞尔曲线配合使用才有效果 3)CAShapeLayer动画渲染直接提交到手机的GPU当中,相较于view的drawRect方法使用CPU渲染而言,其效率极高,能大大优化内存使用情况。3)总而言之,CAShapeLayer的内存消耗少,渲染速度快。因此处理方式2较处理方式1更优。
处理方式3: 使用一个透明底圆形白边的图盖在图片上。直接避免了离屏渲染。(房帮帮+项目中使用的就是这种方式)
处理方式四:直接让UI把图片切成圆角进行显示,这是效率最高的一种方案。直接避免了离屏渲染。但切图会比较麻烦。
11.避免视图层级调整
对象的调整是会经常消耗CPU资源的地方。特别说一下CALayer:CALayer内部并没有属性,但调用属性方法时,它内部时通过运行时resolveInstanceMethod为对象临时添加一个方法,并把对应属性值保存到内部的一个Dictionary里,同时还会通知delegate,创建动画等等,非常消耗资源。当视图层次调整时,UIView、CALayer之间会出现很多方法调用与通知,所以在优化性能时,应该尽量避免调整视图层次、添加和移除视图。
12.尽量使用手动布局控件
Autolayout(自动布局) 是苹果本身提倡的技术,在大部分情况下也能很好的提升开发效率,但是Autolayout会使CPU的使用率呈指数级的上升,因此在构建比较复杂的视图时,尽量使用手动布局控件。
13.减少多余的绘制操作
在实现drawRect方法的时候,它的参数rect就是我们需要绘制的区域,在rect范围之外的区域我们不需要进行绘制,否则会消耗相当大的资源。
- (void)drawRect:(CGRect)rect {
//获得处理的上下文
CGContextRef context = UIGraphicsGetCurrentContext();
//设置线条样式
CGContextSetLineCap(context, kCGLineCapSquare);
//设置线条粗细宽度
CGContextSetLineWidth(context, 1.0);
//设置颜色
CGContextSetRGBStrokeColor(context, 1.0, 0.0, 0.0, 1.0);
//开始一个起始路径
CGContextBeginPath(context);
//起始点设置为(0,0):注意这是上下文对应区域中的相对坐标
CGContextMoveToPoint(context, 0, 0);
//设置下一个坐标点
CGContextAddLineToPoint(context, 100, 100);
//设置下一个坐标点
CGContextAddLineToPoint(context, 0, 150);
//设置下一个坐标点
CGContextAddLineToPoint(context, 50, 180);
//连接上面定义的坐标点
CGContextStrokePath(context);
}
14.图片资源使用格式
14.1 同个分辨率的图片,保存为png要比jpg大。如果图片是从网络上下载的,考虑到流量以及速度,可以考虑用jpg,因为它具有较高的压缩率。
14.2 你的图片是Xcode本地的,那就用png。本地的png优化Xcode帮你做;其他格式的需要在程序运行时做优化,更耗性能。
14.3 如果你的图片要求有较高的色彩饱和度、图像质量,那就用jpg。
15.使用SDWebImage来加载图片
在 iOS 的图片加载框架中,SDWebImage可谓是占据大半壁江山。它支持从网络中下载且缓存图片,并设置图片到对应的 UIImageView 控件或者 UIButton 控件。在项目中使用 SDWebImage 来管理图片加载相关操作可以极大地提高开发效率,让我们更加专注于业务逻辑实现。
15.1 SDWebImage 概论
1.提供了一个 UIImageView 的 category 用来加载网络图片并且对网络图片的缓存进行管理
2.采用异步方式来下载网络图片
3.采用异步方式,使用memory+disk 来缓存网络图片,自动管理缓存。
4.支持 GIF 动画
5.支持 WebP 格式 ps:WebP格式是谷歌开发的一种旨在加快图片加载速度的图片格式。图片压缩体积大约只有JPEG的2/3,并能节约大量的服务器带宽资源和数据空间。
6.同一个 URL 的网络图片不会被重复下载 ps:根据给定的URL生成一个唯一的Key,之后利用这个key到缓存中查找对应的图片缓存。读取图片缓存,根据key先从内存中读取图片缓存,若没有找到图片则读取磁盘缓存,如果磁盘缓存中有图片,那么将磁盘缓存读到内存中成为内存缓存,然后从内存缓存中拿到图片。如果磁盘缓存也没有的话,那么才用这个地址开始下载图片。
7.失效的 URL 不会被无限重试 ps:框架保存了一个失效的URL列表,如果URL失效了就会被加入这个表,保证不会重复多次请求失效的URL。
8.耗时操作都在子线程,确保不会阻塞主线程
9.使用 GCD 和 ARC ps:性能方面GCD更接近底层,所以速度更快。
10.支持 Arm64
15.2 SDWebImage 流程图
15.3 SDWebImage 常用方法使用
1.使用 ImageView+WebCache category 来加载 UITableView 中 cell 的图片
[cell.imageView sd_setImageWithURL:[NSURL URLWithString:@"http://www.domain.com/path/to/image.jpg"] placeholderImage:[UIImage imageNamed:@"placeholder.png"]];
2.使用 block,采用这个方案可以在网络图片加载过程中得知图片的下载进度和图片加载成功与否
[cell.imageView sd_setImageWithURL:[NSURL URLWithString:@"http://www.domain.com/path/to/image.jpg"] placeholderImage:[UIImage imageNamed:@"placeholder.png"] completed:^(UIImage *image, NSError *error, SDImageCacheType cacheType, NSURL *imageURL) {
//... completion code here ...
}];
3.使用 SDWebImageManager,SDWebImageManager 为UIImageView+WebCache category 的实现提供接口。
SDWebImageManager *manager = [SDWebImageManager sharedManager] ;
[manager downloadImageWithURL:imageURL options:0 progress:^(NSInteger receivedSize, NSInteger expectedSize) { // progression tracking code } completed:^(UIImage *image, NSError *error, SDImageCacheType cacheType, BOOL finished, NSURL *imageURL) {
if (image){
// do something with image
}}];
15.关于数据绑定
通常我们喜欢把数据绑定写入- (UITableViewCell *)cellForRowAtIndexPath:(NSIndexPath *)indexPath方法中,但是这里并不是十分准确的,因为这时候的cell还没有实际产生。因此可以尝试把数据绑定写入- (void)tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath;方法中
16.关于刷新tableView
1.如果只是刷新某row就可以解决问题,就使用单独刷新某row的方法
NSIndexPath *indexPath = [NSIndexPath indexPathForRow:0 inSection:0];
[self.tableView reloadRowsAtIndexPaths: [NSArray arrayWithObjects:indexPath,nil] withRowAnimation:UITableViewRowAnimationFade];
2.如果只是刷新某section就可以解决问题,就使用单独刷新某section的方法
NSIndexSet *indexSet = [[NSIndexSet alloc] initWithIndex:0];
[self.tableView reloadSections:indexSet withRowAnimation:UITableViewRowAnimationFade];