这段时间也看了很多关于tableview优化的文章,加上前段时间自己也做了一个同时仿微博和支付宝的项目,思考了一些关于UITableView的优化技巧。UITableView是iOS开发中最常用的控件之一。

UITableview的简单认识

1.重用机制

UITableView最核心的思想就是UITableViewCell的重用机制。UITableView只会创建一屏的UITableViewCell,其他都是从中取出来重用的。每当Cell滑出屏幕时,就会放入到一个集合中,当要显示某一位置的Cell时,会先去集合中取,如果有,就直接重用显示。如果没有,才会创建,这样能极大的减少内存开销。

2.代理方法

UITableView最主要的两个代理方法tableView:cellForRowAtIndexPath:和tableView:heightForRowAtIndexPath:。我最开始接触UITableView的时候,认为UITableView会先调用前者,再调用后者,因为这跟我们创建控件的思路是一样的,先创建它,然后设置它的布局。但实际上并非如此,UITableView是继承自UIScrollView的,需要先确定它的contentSize及每个Cell的位置,然后才会把重用的Cell放置到对应的位置。所以事实上,UITableView的回调顺序是先多次调用tableView:heightForRowAtIndexPath:以确定contentSize及Cell的位置,然后才会调用tableView:cellForRowAtIndexPath:,来显示在当前屏幕的cell。

举个例子来说:如果现在要显示100个Cell,当前屏幕显示5个。刷新(reload)UITableView时,UITableView会先调用100次tableView:heightForRowAtIndexPath:方法,然后调用5次tableView:cellForRowAtIndexPath:方法;滚动屏幕时,每当Cell滚入屏幕,都会调用一次tableView:heightForRowAtIndexPath:、tableView:cellForRowAtIndexPath:方法。通过上述的讲解后,首先想到的UITableView优化的方案是优化上面的UITableView两个代理方法。

优化技巧

1.将赋值和计算布局分离,并根据数据源计算出对应的布局,并缓存到数据源中。

这样能让tableView:cellForRowAtIndexPath:方法只负责赋值,tableView:heightForRowAtIndexPath:方法只负责计算高度。示例代码如下:

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{VVeboTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"cell"];if (cell==nil) {cell = [[VVeboTableViewCell alloc] initWithStyle:UITableViewCellStyleDefaultreuseIdentifier:@"cell"];}[self drawCell:cell withIndexPath:indexPath];return cell;
}- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath{NSDictionary *dict = datas[indexPath.row];float height = [dict[@"frame"] CGRectValue].size.height;return height;
}

2.预渲染

微博的头像在某次改版中换成了圆形,当头像下载下来后。利用后台线程将头像预先渲染为圆形并保存到一个ImageCache中去。示例代码如下

+ (YYWebImageManager *)avatarImageManager {static YYWebImageManager *manager;static dispatch_once_t onceToken;dispatch_once(&onceToken, ^{NSString *path = [[UIApplication sharedApplication].cachesPath stringByAppendingPathComponent:@"weibo.avatar"];YYImageCache *cache = [[YYImageCache alloc] initWithPath:path];manager = [[YYWebImageManager alloc] initWithCache:cache queue:[YYWebImageManager sharedManager].queue];manager.sharedTransformBlock = ^(UIImage *image, NSURL *url) {if (!image) return image;return [image imageByRoundCornerRadius:100]; // a large value};});return manager;
}

对于TableView来说,Cell内容的离屏渲染会带来较大的GPU消耗。为了避免离屏渲染,你应当尽量避免使用layer的border、corner、shadow、mask 等技术,而尽量在后台线程预先绘制好对应内容。

3.当滚动停止时才加载可见cell的图片

- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate
{  if (!decelerate)  {  [self loadImagesForOnscreenRows];  }
}  

4.异步绘制

第三方库YYKit(GitHub地址:https://github.com/jingwanli6666/YYKit)在显示文本的控件上用到了异步绘制的功能。YYKit参考了FaceBook的AsyncDisplayKit库,它是用于保持iOS界面流畅的库。关于这块代码作者单独提取出来,放到了这里:YYAsyncLayer。YYAsyncLayer 是 CALayer 的子类,当它需要显示内容(比如调用了 [layer setNeedDisplay])时,它会向 delegate,也就是 UIView 请求一个异步绘制的任务。在异步绘制时,Layer 会传递一个BOOL(^isCancelled)() 这样的 block,绘制代码可以随时调用该 block 判断绘制任务是否已经被取消。
当 TableView 快速滑动时,会有大量异步绘制任务提交到后台线程去执行。但是有时滑动速度过快时,绘制任务还没有完成就可能已经被取消了。如果这时仍然继续绘制,就会造成大量的 CPU 资源浪费,甚至阻塞线程并造成后续的绘制任务迟迟无法完成。我的做法是尽量快速、提前判断当前绘制任务是否已经被取消;在绘制每一行文本前,我都会调用 isCancelled() 来进行判断,保证被取消的任务能及时退出,不至于影响后续操作。

当我们在Cell上添加系统控件时,实质上系统都需要调用底层的接口进行绘制。当需要大量添加控件时,对资源的开销也会很大,如果我们直接绘制,就能提高效率。第三方微博客户端(VVebo)通过给自定义的Cell添加draw方法,来异步绘制Cell的系统控件。相关实现见这个项目:VVeboTableViewDemo,部分代码如下所示:

//将主要内容绘制到图片上
- (void)draw{if (drawed) {return;}NSInteger flag = drawColorFlag;drawed = YES;//异步绘制dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{CGRect rect = [_data[@"frame"] CGRectValue];UIGraphicsBeginImageContextWithOptions(rect.size, YES, 0);CGContextRef context = UIGraphicsGetCurrentContext();[[UIColor colorWithRed:250/255.0 green:250/255.0 blue:250/255.0 alpha:1] set];CGContextFillRect(context, rect);if ([_data valueForKey:@"subData"]) {[[UIColor colorWithRed:243/255.0 green:243/255.0 blue:243/255.0 alpha:1] set];CGRect subFrame = [_data[@"subData"][@"frame"] CGRectValue];CGContextFillRect(context, subFrame);[[UIColor colorWithRed:200/255.0 green:200/255.0 blue:200/255.0 alpha:1] set];CGContextFillRect(context, CGRectMake(0, subFrame.origin.y, rect.size.width, .5));}{float leftX = SIZE_GAP_LEFT+SIZE_AVATAR+SIZE_GAP_BIG;float x = leftX;float y = (SIZE_AVATAR-(SIZE_FONT_NAME+SIZE_FONT_SUBTITLE+6))/2-2+SIZE_GAP_TOP+SIZE_GAP_SMALL-5;[_data[@"name"] drawInContext:context withPosition:CGPointMake(x, y) andFont:FontWithSize(SIZE_FONT_NAME)andTextColor:[UIColor colorWithRed:106/255.0 green:140/255.0 blue:181/255.0 alpha:1]andHeight:rect.size.height];y += SIZE_FONT_NAME+5;float fromX = leftX;float size = [UIScreen screenWidth]-leftX;NSString *from = [NSString stringWithFormat:@"%@  %@", _data[@"time"], _data[@"from"]];[from drawInContext:context withPosition:CGPointMake(fromX, y) andFont:FontWithSize(SIZE_FONT_SUBTITLE)andTextColor:[UIColor colorWithRed:178/255.0 green:178/255.0 blue:178/255.0 alpha:1]andHeight:rect.size.height andWidth:size];}[@"•••" drawInContext:contextwithPosition:CGPointMake(SIZE_GAP_LEFT, 8+countRect.origin.y)andFont:FontWithSize(11)andTextColor:[UIColor colorWithRed:178/255.0 green:178/255.0 blue:178/255.0 alpha:.5]andHeight:rect.size.height];if ([_data valueForKey:@"subData"]) {[[UIColor colorWithRed:200/255.0 green:200/255.0 blue:200/255.0 alpha:1] set];CGContextFillRect(context, CGRectMake(0, rect.size.height-30.5, rect.size.width, .5));}}//将绘制的内容以图片的形式返回,并调用主线程显示UIImage *temp = UIGraphicsGetImageFromCurrentImageContext();UIGraphicsEndImageContext();dispatch_async(dispatch_get_main_queue(), ^{if (flag==drawColorFlag) {postBGView.frame = rect;postBGView.image = nil;postBGView.image = temp;}});});[self drawText];[self loadThumb];
}//将文本内容绘制到图片上
- (void)drawText{if (label==nil||detailLabel==nil) {[self addLabel];}label.frame = [_data[@"textRect"] CGRectValue];[label setText:_data[@"text"]];if ([_data valueForKey:@"subData"]) {detailLabel.frame = [[_data valueForKey:@"subData"][@"textRect"] CGRectValue];[detailLabel setText:[_data valueForKey:@"subData"][@"text"]];detailLabel.hidden = NO;}
}

各个部分都是根据之前算好的布局进行绘制。这里需要异步绘制,但如果在重写drawRect方法就不需要用GCD异步线程了,因为drawRect本身就是异步绘制。

5.按需加载

当滑动时,松开手指后,立刻计算出滑动停止时Cell的位置,并预先绘制那个位置附近的几个Cell,而忽略当前滑动中从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+3<datas.count) {[arr addObject:[NSIndexPath indexPathForRow:indexPath.row+1 inSection:0]];[arr addObject:[NSIndexPath indexPathForRow:indexPath.row+2 inSection:0]];[arr addObject:[NSIndexPath indexPathForRow:indexPath.row+3 inSection:0]];}} else {NSIndexPath *indexPath = [temp firstObject];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]];}}[needLoadArr addObjectsFromArray:arr];}
}

6.全局并发控制

用 concurrent queue 来执行大量绘制任务时,偶尔会遇到这种问题:

大量的任务提交到后台队列时,某些任务会因为某些原因(此处是 CGFont 锁)被锁住导致线程休眠,或者被阻塞,concurrent queue 随后会创建新的线程来执行其他任务。当这种情况变多时,或者 App 中使用了大量 concurrent queue 来执行较多任务时,App 在同一时刻就会存在几十个线程同时运行、创建、销毁。CPU 是用时间片轮转来实现线程并发的,尽管 concurrent queue 能控制线程的优先级,但当大量线程同时创建运行销毁时,这些操作仍然会挤占掉主线程的 CPU 资源。ASDK 有个 Feed 列表的 Demo:SocialAppLayout,当列表内 Cell 过多,并且非常快速的滑动时,界面仍然会出现少量卡顿,我谨慎的猜测可能与这个问题有关。
使用 concurrent queue 时不可避免会遇到这种问题,但使用 serial queue 又不能充分利用多核 CPU 的资源。我写了一个简单的工具 YYDispatchQueuePool,为不同优先级创建和 CPU 数量相同的 serial queue,每次从 pool 中获取 queue 时,会轮询返回其中一个 queue。我把 App 内所有异步操作,包括图像解码、对象释放、异步绘制等,都按优先级不同放入了全局的 serial queue 中执行,这样尽量避免了过多线程导致的性能问题。

除了上述提到的几个方面外,还有一些大家都比较熟悉的优化点:

  • 正确使用重用机制
  • 尽量少用或者不用透明图层
  • 减少subviews的数量
  • 如果Cell内部的内容来自web,使用异步加载,缓存请求结果
如何评测界面的流畅度
屏幕的刷新频率为60HZ,当列表快速滑动时仍能保持屏幕刷新频率为50~60FPS,则说明滑动比较顺畅。可以利用FPS指示器:FPSLabel来监视CPU的卡顿问题。

参考文献:

iOS 保持界面流畅的技巧

UITableView的优化技巧相关推荐

  1. (0074)iOS开发之UITableView的优化

    写的很好引用 https://www.jianshu.com/p/af6b095aaaf3 前言 这篇文章对 UITableView 的优化主要从以下3个方面分析: 基础的优化准则(高度缓存, cel ...

  2. UITableView性能优化 - 中级篇

    老实说,UITableView性能优化 这个话题,最经常遇到的还是在面试中,常见的回答例如: Cell复用机制 Cell高度预先计算 缓存Cell高度 圆角切割 等等. . . 进阶篇 最近遇到一个需 ...

  3. ios开源框架——UITableView+FDTemplateLayoutCell优化UITableViewCell高度计算

    前言 这篇文章是我和我们团队最近对UITableViewCell利用AutoLayout自动高度计算和UITableView滑动优化的一个总结.从这篇文章里,你可以读到: UITableView高度计 ...

  4. 模板 - 判断负环(超时高效优化技巧)、01分数规划

    整理的算法模板合集: ACM模板 判断负环 判正环求最长路,判负环求最短路 int n; // 总点数 int h[N], w[N], e[N], ne[N], idx; // 邻接表存储所有边 in ...

  5. sw如何缩放装配体_SolidWorks关于大型装配体的优化技巧(二)

    在之前的文章中,已经从SolidWorks的软件设置方面,阐述了关于大型装配体的优化技巧,包括:启用冻结栏,显示样式切换以及图像品质设置,今天将从工程师的日常设计规范上,给大家讲讲如何进行优化. 首先 ...

  6. php 随机在文章中添加锚文本_seo网站优化技巧之:8种优质锚文本的做法

    众所周知,质量最高的外链就是锚文本形式的外链,这种外链又称为锚链,当锚文本中嵌入了我们的目标关键词之后,这种锚链的权重传递效率最高.锚文本链接的质量高低,对关键词排名及网站权重的影响非常大. seo网 ...

  7. vue seo关键词设置_网站SEO常用优化技巧

    SEO(Search Engine Optimization)搜索引擎优化,简单来说,就是通过技术手段,帮助我们的网站上首页.不同的搜索引擎,排名算法也不尽相同,导致了各项参数的权重比值也就不一样,但 ...

  8. Java性能优化技巧

    Java性能优化技巧 参考了些书籍,网络资源整理出来,适合于大多数Java应用 在JAVA程序中,性能问题的大部分原因并不在于JAVA语言,而是程序本身.养成良好的编码习惯非常重要,能够显著地提升程序 ...

  9. 让win7系统高速运行的优化技巧

    这里要跟大家分享的是关于如何让win7系统高速运行的优化技巧,任何一款电脑系统用久了之后速度都会变慢,因此很多用户会到处寻找各种优化渠道,如果想要让自己的win7系统像新安装前一样的速度,其实我们只需 ...

最新文章

  1. python类方法和实例方法syntax errors_Python 实例方法,类方法和静态方法
  2. 使用U函数之后如何去掉index.php
  3. Java SPI 源码解析及 demo 讲解
  4. webService学习3:客户端生成webservice代码
  5. 微软推出新语言Bosque,超越结构化程序设计
  6. phpcms v9 sql数据{$r[content]},前端如何换行显示?
  7. java学习小知识集锦1
  8. Java NIO 之 I/O基本概念(二)
  9. 用python写WordCount的MapReduce代码
  10. PR/PS/AE/达芬奇免费模板素材网站分享——个人纯分享,没有公众号,没有广告!
  11. 重磅干货:30张图读懂当前中国金融体系!
  12. Neo4j CQL语法
  13. 赢在2022,面试官常问的软件测试面试题总结
  14. html5 预览图片原理,html5实现图片预览和查看原图
  15. 融云 CTO 岑裕:出海技术前沿探索和排「坑」实践
  16. 小数化分数 (思维)
  17. Depin(Linux)下安装Tibco Ems 8.5
  18. 股票技术分析--任正德主编
  19. python Note II
  20. 2021届秋招嵌入式软件开发(联发科、海康威视、浙江大华、高德红外、汇顶科技、瑞芯电子、深圳有为)

热门文章

  1. item_review - 获得shopee商品评论
  2. 金山毒霸2008安全套装通行证—最后一次更新
  3. 每日技巧(word条形图更改横坐标)
  4. TCP/IP协议栈在Linux内核中的运行时序分析
  5. 手脱压缩壳UPX的4种查OEP方法
  6. 使用scrapy爬取斗鱼直播间信息
  7. 《开源思索集》一欢迎来到异步社区!
  8. 精确率和召回率 与 置信度之间的关系
  9. bzoj-1025 [SCOI2009]游戏
  10. 读书笔记 - 实现领域驱动设计