UITableView的优化

作者: 星___尘 | 来源:发表于2015-12-18 17:37 被阅读2910次

    最近看到有些面试题中会问到TableView的优化,特定花了几天时间研究了一下各种优化技巧,主要就分为几个主要方向:

    • 从重用Cell的方面去优化
    • 从图层属性,圆角/阴影等方面去优化
    • 从UIView的绘制方面去优化
    • 从预计算和缓存高度,按需加载的方面去优化

    从一个初学者写的卡到爆的TableView例子说起(引用自这里)

    • (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
      ContacterTableCell *cell = [tableView dequeueReusableCellWithIdentifier:@"ContacterTableCell"];
      if (!cell) {
      cell = (ContacterTableCell *)[[[NSBundle mainBundle] loadNibNamed:@"ContacterTableCell" owner:self options:nil] lastObject];
      }
      NSDictionary *dict = self.dataList[indexPath.row];
      [cell setContentInfo:dict];
      return cell;
      }
    如果没有在xib中设置重用的标识,上面的cell就不会发生重用,只要TableView发生滚动,cellForRowAtIndexPath就会被调用,每一次cellForRowAtIndexPath被调用都会从NSBundle中获取View的示例再赋给cell。众所周知从NSBundle中读取数据是非常的慢的,所以这样写出的代码必定会卡到爆(我当初刚学iOS的时候就是这样写的(-_-))。
    
    ## 既然卡到爆就重用啰
    首先注册cell
    
    
        table = UITableView(frame: self.view.bounds, style: UITableViewStyle.Plain)
        table?.registerClass(UITableViewCell.self, forCellReuseIdentifier: "mainViewControllerCellId")
    
    
    然后重用
    
    
    func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCellWithIdentifier("mainViewControllerCellId")
        // config cell data
        cell?.textLabel?.text = titles[indexPath.row]
        cell?.selectionStyle = .None
        return cell!
    }
    
    可以看到,使用了基本的重用机制后,整个TableView就变得流畅起来了,而且按照这种做法,cell的生产和使用就分开了,cellForRowAtIndexPath就只专注于设置cell的数据,不关心cell的初始化。一般的TableView界面采用这种写法都能获得不错的性能,而且有一定经验的iOS老手都是这样写的。
    
    ## 万一Cell中有圆角蒙版阴影透明这些图层属性呢
    在UIView中,透明效果的渲染,圆角的渲染,蒙版的渲染,阴影的渲染会消耗很大部分的性能。在静态的View之中,这部分只消耗一次,不会造成太大的影响,但是在想UITableViewCell这些重用的动态的View中,每一次的重用都会消耗一次,所以总的性能消耗会很大。当然,要想在cell中使用这些属性,还是有优化的技巧来减少性能的消耗的。
    
    #### 首先,尽量减少透明View。
    UIView有一个属性```opaque ```,当不需要透明时可以将其设置为true,这样可以减少性能消耗。
    
    #### 其次,减少离屏渲染
    通常图层的以下属性将会触发离屏渲染:
    * 阴影(UIView.layer.shadowOffset/shadowRadius/...)
    * 圆角(当 UIView.layer.cornerRadius 和 UIView.layer.maskToBounds 一起使用时)
    * 图层蒙板
    
    对于以上这些,要减少离屏渲染比较好的方法是使用shadowPath来设置阴影,使用裁剪过的图片代替圆角而不是使用cornerRadius,图层模板能不用就不用。
    
    #### 最后,合理使用光栅化
    当然,除了第二点减少离屏渲染外,还有一点就是要合理使用光栅化。使用光栅化能够将能够有效提高性能,[把layer的shouldRasterize设为YES后,CALayer会被光栅化为bitmap,layer的阴影等效果也会被保存到bitmap中作为缓存。在使用了shadow或cornerRadius等效果时,缓存使性能得到提升](http://zhijiang.me/2015/08/03/%E5%BD%B1%E5%93%8D%E5%9B%BE%E5%BD%A2%E6%80%A7%E8%83%BD%E7%9A%84%E5%9B%A0%E7%B4%A0%E5%92%8C%E4%BD%BF%E7%94%A8Instrument%E8%BF%9B%E8%A1%8C%E6%A3%80%E6%B5%8B/)。
    
    但是,使用光栅化要注意几点:
    
    - 更新已经光栅化的CALayer会造成离屏渲染 (这最重要)
    - 被光栅化的bitmap如果超过100ms没有被使用则会被移除
    - 系统限制缓存的大小为2.5 x screen size
    
    对于经常改动的,不易采用光栅化,否则会增加离屏渲染,增加性能消耗。对于相对静态的,建议采用光栅化。特别是界面比较复杂,动画比较复杂的,都建议使用光栅化。
    
    ## 糟糕我遇到了很复杂的Cell
    由于绘制渲染复杂View的过程是非常耗时的,有时真的不排除遇到很复杂CellView的情况。由于本人对Core Graphics不太熟练,而且最近有点小忙,所以就不提供代码,只提供一些优化思路。
    核心优化就两点:
    
    1. 使用Core Graphics进行重绘,更进一步就是将所有subViews绘制成一张图片,通过消除View树的层级来提高效率。通俗来说就是所谓的扁平化。。。
    通过重绘来压缩View树层级,能大大提高View的绘制效率,特别对于一些复杂界面效果特别明显。当然缺点就是使用重绘比较复杂,开发一些相对简单的界面可能会影响开发效率。
    
    2.异步绘制渲染。
    这个就是将View的绘制渲染放到后台线程中,完成后再通知主线程更新UI。这个思路跟Facebook的一个非常出名的开源框架[AsyncDisplayKit](https://github.com/facebook/AsyncDisplayKit)类似。
    
    ## 我想我的TableView更动人
    如何更进一步优化TableView的体验?答案就是就两点:
    - cell高度缓存和预计算
    - 按需加载
    
    对于第一点cell高度缓存和预计算,简单来说就是计算每一个cell的高度并缓存起来,避免重复计算。这里有个更合理的方法是[在用户不滑动TableView时,通过监听runloop来启动后台cell高度计算和缓存](http://blog.sunnyxx.com/2015/05/17/cell-height-calculation/)。这样做的好处时将用户交互和耗时计算任务进行合理的分时,既不影响用户交互,也不浪费CPU计算性能。
    
    对于第二点,用户滚动TableView的时候,希望能尽快看到当前屏幕显示的内容,而不关心其他还没有滚动到当前屏幕的内容。所以,一个比较有效的优化思路是优先绘制加载当前的内容,暂时忽略其他内容。根据这个思路,可以使用UIScrollView的协议方法,监听滚动的offset,然后当用户暂停滚动时的屏幕,就是需要优先加载的屏幕内容,这时候可以再去加载当前内容,而不是一开始按照滚动顺序全部加载。这样的话就不用特地设置一个网络请求的队列来管理网络请求,而且发出去了的请求也是cancel不了的。
    
    
    ## 总结
    感觉对TableView的优化有了点思路,但是优化方法太多以至于还没消化过来,但是大的优化方向也是很清晰的:
    
    - 能重用就重用
    - 尽量减少view层级
    - 使用异步绘制
    - 使用缓存
    - 按需加载
    - 对于某些layer属性尽量采用替代方案
    
    总之,这几天对这个专题的研究,收获很多,特定分享出来,写的不好,还望指教。
    
    ## 参考
    
    [优化UITableViewCell高度计算的那些事](http://blog.sunnyxx.com/2015/05/17/cell-height-calculation/)
    
    [[iOS 程序性能优化](http://www.samirchen.com/ios-performance-optimization)](http://www.samirchen.com/ios-performance-optimization/)
    
    [UITableView优化技巧](http://longxdragon.github.io/2015/05/26/UITableView%E4%BC%98%E5%8C%96%E6%8A%80%E5%B7%A7/)
    
    [如何加强 iOS 里的列表滚动时的顺畅感?](http://www.zhihu.com/question/20382396)
    
    [影响图形性能的因素和使用Instrument进行检测](http://zhijiang.me/2015/08/03/%E5%BD%B1%E5%93%8D%E5%9B%BE%E5%BD%A2%E6%80%A7%E8%83%BD%E7%9A%84%E5%9B%A0%E7%B4%A0%E5%92%8C%E4%BD%BF%E7%94%A8Instrument%E8%BF%9B%E8%A1%8C%E6%A3%80%E6%B5%8B/)

    相关文章

      网友评论

      • 尘絮缘12138:赞同@毛毛可 的说法。

        ContacterTableCell *cell = [tableView dequeueReusableCellWithIdentifier:@"ContacterTableCell"];
        if (!cell) {
        cell = (ContacterTableCell *)[[[NSBundle mainBundle] loadNibNamed:@"ContacterTableCell" owner:self options:nil] lastObject];
        }

        这个确实是重用,只要在xib或storyboard中注册identifier即可;这个方法顶多说是初始化比较慢,不能说"一般不用xib重用",看个人喜好。 UITableview 本身就有重用机制,所以开篇 “可以看出TableViewCell是完全没有使用重用的” 这句话很不贴切。一般我们写Scrollview时,会自己写重用机制,当然也是模仿tableview。
        星___尘:@尘絮缘12138 文章已更新,感谢指出错误。 :smile: 。对“"一般不用xib重用"”的意思是个人建议不在xib中设置重用,而尽量用代码写界面。原因有二:其一解析xib增加系统负担,降低系统系能,其二构建复杂界面时xib力不从心,views一多交互就难控制。xib的好处就是对于静态简单的view来说是非常方便的。所以只能说“一般不用xib重用”,而不是“不用xib重用”,个人建议和喜好而已。 :grin:
      • 毛毛可: ContacterTableCell *cell = [tableView dequeueReusableCellWithIdentifier:@"ContacterTableCell"];
        if (!cell) {
        cell = (ContacterTableCell *)[[[NSBundle mainBundle] loadNibNamed:@"ContacterTableCell" owner:self options:nil] lastObject];
        }
        这个方法也是重用吧,只是写法比较老
        星___尘:@毛毛可 一般不用xib来实现重用😄
        毛毛可:@星___尘 一般都不会忘了xib吧😊
        星___尘:@毛毛可 要在xib中指定重用的标识,很容易就忽略
      • 14b08396b57f:不错 学到了 多谢分享
      • 左眼见到肠:学习了,继续关注,楼主加油
        星___尘:@左眼见到肠 谢谢

      本文标题:UITableView的优化

      本文链接:https://www.haomeiwen.com/subject/irxphttx.html