TableView原理及深度优化

UITableView可以说是使用频率最高而且话题最多的UIKit组件了,他也是MVC的完美体现。

一. 原理篇

1.1 复用机制

UITableView是继承自UIScrollView,其最核心的思想就是UITableViewCell的复用机制。初始化的时候他会先创建cell的缓存字典和section的缓存array,以及一个用于存放复用cell的mutableSet。并且它会去创建显示的N+1个cell,其他都是从中取出来重用。

每当Cell滑出屏幕时,就会放入到set中(相当于一个重用池),当要显示某一位置的Cell时,会先去集合中取,如果有,就直接拿来显示;如果没有,才会创建。这样做的好处可想而知,极大的减少了内存的开销。

复用机制如下:

  • 维护一个重用队列

  • 当元素离开可见范围时,removeFromSuperview 并加入重用队列(enqueue)

  • 当需要加入新的元素时,先尝试从重用队列获取可重用元素(dequeue)并且从重用队列移除

  • 如果队列为空,新建元素

  • 这些一般都在 scrollViewDidScroll: 方法中完成

1.2 调用流程

初始化的时候内部会对_needsReload进行标记,之后调用setNeedsLayout方法。对于UIView的setNeedsLayout方法,在调用后Runloop会在即将到来的周期调用displayIfNeeded标记,如果为YES则会进行drawRect视图重绘。当RunLoop到来时,开始重回过程即调用layoutSubViews方法,且改方法是被重写过的。

它会在内部调用reloadData方法,在里面会对每个cell进行removeFromSuperview操作(为了指针悬挂的情况,有可能某个cell被其他视图引用),以及清除cell缓存字典和复用set。

之后会再次去更新缓存:他会先移除每个section的header和footer视图,然后根据DataSource中实现的delegate方法重新设置对应的参数,比如:titleForHeader,titleForFooter,heightForHeader,HeightForFooter,heightForRow,numberOfRows等。

重新缓存完成之后,那么就是进行布局更新了。他会先去获取容器视图相对于父视图的坐标以及偏移量,然后依次将header,cell,footer取出来添加到self上。

我们都知道,UITableView是继承自UIScrollView的,那么需要先确定它的contentSize及每个Cell的位置,然后才会把重用的Cell放置到对应的位置。所以UITableView的回调顺序是先多次调用tableView:heightForRowAtIndexPath:以确定contentSize及Cell的位置,然后才会调用tableView:cellForRowAtIndexPath:,从而来显示在当前屏幕的Cell。

比如:现在需要展示100个cell,当前屏幕显示10个。那么刷新tableview的时候,首先会调用100次tableView:heightForRowAtIndexPath:方法,然后调用10次tableView:cellForRowAtIndexPath: 方法。滑动屏幕时,每当Cell进入屏幕,都会调用一次tableView:heightForRowAtIndexPath:tableView:cellForRowAtIndexPath:方法。所以高度计算是一个很有必要优化的地方。


二. 优化篇

2.1 问题分析

  1. CPU(主要是主线程)/GPU负担过重或者不均衡(诸如mask/cornerRadius/drawRect/opaque带来offscreen rendering/blending等等)。由于所有的UIView都是由CALayer来负责显示,因此对Core Animation的了解就变得尤为重要。这里推荐Nick Lockwood的Core Animation: Advanced Techniques一书,其中有对Core Animation的性能有着非常详尽的梳理和剖析。
  2. Autolayout布局性能瓶颈,约束计算时间会随着数量呈指数级增长,并且必须在主线程执行。具体分析可以参考这篇文章:http://floriankugler.com/2013/04/22/auto-layout-performance-on-ios/。这也是为何ASDK抛弃了Autolayout而设计了自己的布局系统的重要原因之一(https://github.com/facebook/AsyncDisplayKit/issues/196)。Autolayout在单个View开发时能带来很多便利,而在一些需要高性能的场景下需要谨慎使用。
  3. 尽管从iPhone4S(A5)开始CPU已经采用多核,然而对于大多数app来说,并行效率仍然非常低下。也就是说,在app卡顿(主线程所占用的核心满负荷)时,往往CPU的其他核心几乎无事可做。一般情况下,由于主线程承担了绝大部分的工作,仅仅是把主线程的任务转移一部分给其他线程进行异步处理,就可以马上享受到并发带来的性能提升。这应该也是AsyncDisplayKit得名的原因之一。

2.2 优化思路

我们知道,当用户开始滚动或点击一个View,所有的事件都会被送到主线程等待处理。此时主线程能否抽出足够充裕的时间来处理变得极为重要,尤其是在连续操作(如UIGestureRecognizer)时,每次touchMoved事件处理都会占用主线程一定的时间(如新的UIImageView进入视图,主线程开始处理布局或者图片解码,而这些需要连续占用大量CPU时间)。如果一个操作耗时超过16ms(1000ms/60fps),那就意味着下一帧无法及时得到处理,引起丢帧。

2.3 可优化点

  1. 提前计算并缓存好高度(布局),因为heightForRowAtIndexPath:是调用最频繁的方法。

  2. 复杂界面可采用异步绘制。

  3. 在大量图片展示时,可以滑动时按需加载。

  4. 尽量少用或不用透明图层,多个透明元素重叠显示可采用合并成一张图片显示。

  5. 减少subviews的数量,如果是不需要交互可以使用CALayer 替换掉 UIView。

  6. heightForRowAtIndexPath:中尽量不使用cellForRowAtIndexPath:

  7. 根据场景合理使用imageWithContentsOfFile和imageNamed。

  8. 页面元素多的时候,减少autolayout布局,采用frame。

  9. 缓存NSDateFormatter结果,不多次创建,及时释放。

  10. 图片解码时,CALayer 被提交到 GPU 前,CGImage 中的数据才会得到解码,GPU执行,卡主线程。常见的做法是在后台线程先把图片绘制到 CGBitmapContext 中,然后从 Bitmap 直接创建图片。

  11. CALayer 的 border、圆角、阴影、遮罩(mask)触发的离屏渲染,可开启CALayer.shouldRasterize ,转嫁到CPU上或是截图或者采用图片实现。

  12. 使用RunLoop和多线程在闲时处理一些繁重的计算工作。

具体可查看:UITableView+FDTemplateLayoutCell源码解析


参考文献:

优化UITableViewCell高度计算的那些事

AsyncDisplayKit介绍(一)原理和思路

微信读书 iOS 性能优化总结

iOS 保持界面流畅的技巧

你可能感兴趣的:(TableView原理及深度优化)