iOS开发之多种Cell高度自适应实现方案的UI流畅度分析

本篇博客的主题是关于UI操作流畅度优化的一篇博客,我们以TableView中填充多个根据内容自适应高度的Cell来作为本篇博客的使用场景。当然Cell高度的自适应网上的解决方案是铺天盖地呢,今天我们的重点不是如何讨论Cell高度的自适应,而是给出几种Cell高度自适应的解决方案,然后对比起UI流畅度,从而得出一些UI优化的一些常规做法。今天博客中主要用涉及的第三方库是YYKitAsyncDisplayKit

关于YYKit和AsyncDisplayKit这两个库,本篇博客只是简单的涉及到一些基本用法,主要是针对我们本篇博客的Demo来使用的,其中好多功能并未使用。因为之前在项目中没怎么使用过这两个框架,所以本篇博客就不着重介绍着两个第三方框架了,如果你对其感兴趣,Github上有你想要的内容,请自行搜索。废话少说,进入今天的主题。

 

一、总述

本篇博客主要给出了5种Cell自适应高度的解决方案,并对比了每种实现方案的流畅度。也可以说是从UI最不流畅的一种我们慢慢优化,从而实现了这5种解决方案。当然我们是观察屏幕的FPS来判断屏幕在操作时是否卡顿。关于对FPS的实时监测,我参考了YYKit-Demo中的做法,并将其单独提取了一个组件,便于我们项目的使用,关于这个提取的FPS组件,下方使用时会具体介绍。当然本篇博客所涉及的所有代码,依然会分享到Github上,文章后方会给出相应的链接,有需要的小伙伴请自行clone。

下方这个截图是我们今天demo的菜单列表页面,点击每个Cell都会跳转左边这个内容列表页面。不过每个Cell所对应的内容页面的Cell自适应高度的实现方式不同,我们在对其滑动操作时,可以根据下方这个FPS组件来观察屏幕的流畅度。当然,每个内容列表页的布局和显示内容都是相同的,不过不同的Cell自适应解决方案所对应的UI流畅度也是不同的。下面我们先大体的聊一下每种Cell自适应的实现方案。

  • Autolayout + AutomaticDimension: 该解决方案对应着,下方第一个Cell, 点击该Cell进入的页面完全由AutoLayout进行布局,Cell自适应的高度也不用我们自己计算,而是使用系统提供的解决方案UITableViewAutomaticDimension来解决。当然,使用UITableViewAutomaticDimension要依赖于你添加的约束,稍后会介绍到。这种实现方案用起来简单,不过UI流畅度方面不太理想。当TableView快速滑动时,就会出现掉帧,卡的不要不要的。
  • Autolayout + CountHeight: 这种解决方案依然是采用AutoLayout的方式来对Cell的内容进行布局,不过Cell的高度我们是自己计算的,当然我们这个计算Cell高度的过程是放在子线程中进行的,所以这种实现方式要优于第一种实现方式,稍后会详细介绍。
  • FrameLayout + CountHeight: 为了进一步提高流畅度,我们采用了纯Frame布局,之前好像在哪儿看过,说Autolayout最终也是会被转换成Frame进行布局的,所以我们索性就使用Frame对整个Cell中的元素进行布局。当然Cell高度已经Cell中可变内容的高度都是在子线程中进行计算的,这也是优化很重要的一步。这种实现方式还是比较流畅的,可以作为折中的方案。
  • YYKit + CountHeight: 这种解决方案用到了YYKit中的控件,并且使用Frame布局与Cell高度的计算。这种方式要由于上面的解决方案,比较YYKit中的一些控件做了优化。
  • AsyncDisplayKit + CountHeight: 则是使用了AsyncDisplayKit中提供的相关Note代替系统的原生控件,这种实现方式是这5种实现方式中最为流畅的。稍后会详细介绍。

上面这五种实现方式将是下方介绍的具体内容,当然会涉及一些其他的技术实现细节。

     

 

 

二、博客所涉及的自定义工具介绍

在进入主题之前,先进行预热。先对本片博客中所涉及的一些小工具进行介绍。当然这些工具是自己封装的,是本篇博客中所涉Demo的基础,本部分将进行统一介绍,在使用时我们就一笔带过即可。

 

1.工具一:FPSDisplay

上述Demo中使用到了一个小的组件是FPSDisplay, 用于实时显示屏幕的刷新频率的。我们知道现在iPhone的FPS是60。也就是每秒刷新60帧,如果低于60帧的话那就是掉帧了,如果掉帧掉的多的话就会明显的看出卡顿。上述截图中右下方的黑色图标就是我们封装的FPSDisplay工具。当然该工具是参考着YYKit-Demo中所实现的,对其进行的简化和封装,将其提取成了一个单独的组件,便于在我们的应用中引入。

下方就是FPSDisplay引入并初始化的过程,下方是在AppDelegate中的didFinishLaunchingWithOptions中添加的。因为FPSDisplay是添加在KeyWindow上的,所以在FPSDisplay初始化时要保证你的App已经有了KeyWindow了。进行下方初始化后,在你的App的右下方就会出现一个图标来实时的显示FPS。

  

 

FPSDisplay的实现并不麻烦,主要是CADisplayLink的使用,将创建CADisplayLink创建的对象添加到MainRunLoop中,就可以以此来计算FPS了。下方是FPSDisplay的核心代码。在每次进行屏幕刷新时都会执行下方的tink方法,我们可以来计算1秒内刷新的次数,也就是所谓的FPS。代码比较简单,在此就不做过多的赘述了,详细的代码在Github上已经分享。

  

 

2.工具二:数据提供者

除了上述的FPSDislay工具外,我们还需要一个模块,那就是为Demo提供模拟数据的模块。因为我们没有网络模块,我们就模拟网络请求来生成数据,然后对数据进行处理生成Model。当然这个生成测试数据的过程没有用到主线程,为了不阻塞Main线程,我们需要将数据生成的部分在子线程中异步的执行。当然此处主要涉及多线程的东西。下方代码段就是数据提供者DataSupport的核心代码。

下方代码段主要用到了并行队列的异步执行任务组的使用,已经任务锁的添加。下方首先创建了一个并行队列concurrentQueue和队列的任务组group,并且为了数据同步,我们使用信号量创建了一个任务锁lock。在for循环中我们异步的执行并行队列来创建我们需要的数据模型Model。每循环一次创建一个Model,为了Model数据的独立性,在创建Model时,我们要为其添加信号量同步锁

当50条数据异步创建完毕后,我们需要将其提供给数据提供者的使用放,也就是在任务组中的任务都执行完毕后,会执行下方的notify方法。

  

在Model创建时,我们会对Model中可变的文字,也就是Cell中高度变化的内容的高度进行计算。当然该计算是在子线程中异步执行的。所以不会占用主线程的时间来计算Cell的高度以及Cell中可变文字的高度。我们Model中有两个字段就是来存储Cell的高度以及可变文本的高度的,如下所示。这样做的好处就是提高UI的流畅度。

  

 

3.工具三:UIImage对象的Memory缓存

第三个工具也是为了提高数据流畅度而生的,就是图片的对象缓存。我们将已经初始化过的图片进行缓存,等下次再使用该图片时直接从缓存中读取,从而节省了在主线程中创建对象和销毁对象的时间,从而可以提高UI的流畅度。当然此处我实现的图片的内存缓存比较简单,也就是在本Demo中适用。不过原理还是OK的,全面的MemoryCache请参考YYKit中的YYMemoryCache。其中用到了双向链表以及CFMutableDictionaryRef来实现的MemoryCache,其源码并不是很难理解,有兴趣的小伙伴可以进行阅读呢。

本篇博客所实现的Memory缓存就比较简单了,就使用了一个字典,字典的Key是图片的名称,字典的Value是已经创建的字典的对象。代码比较简单,下方是核心代码。大体原理就是在获取时,如果缓存字典中没有相应的对象就进行创建并加入缓存,然后返回该对象。如果缓存中已经有该对象,则直接返回。核心代码如下。

  

 

 

三、Autolayout + AutomaticDimension

上一部分已经为Demo的开发做好了准备,接下来就开始进入今天真正的主题。首先我们来介绍Autolayout + AutomaticDimension的实现方式。使用这种方式来是Cell高度的自适应比较简单,但不高效。下方是我们所使用的Cell的布局,当然是使用AutoLayout来实现的。因为下方test的内容的长度是不定的,所以我们为test所对应的TextView添加的约束为(top, left right, bottom)。这样test的高度就可以随着Cell的高度而改变了。

    

约束添加完毕后,我们的工作基本上就已经完成了,接下来需要进行简单的配置,我们的Cell高度自适应就OK了。下方就是我们添加完约束后要做的事情,需要给我们的tableView设置一个预估值(estimatedRowHeight), 然后在TableViewDelegate的heightForRowAtIndexPath方法中返回UITableViewAutomaticDimension该属性即可。这样Cell就可以根据可变的文字高度来自适应了。当然该方法在iOS8以上的系统上才可以使用。

  

 

经过上述这两步,我们的Cell就可以进行自适应了,下方是该解决方案所对应的运行效果。可以看出来卡顿还是比较明显的,掉帧比较严重,在Cell高度自适应时最好不要采用此方法。也就是说这种方法,并不适用在我们Cell列表中来预估每个Cell的高度。那这种方式是不是就没用了呢?当然不是,填写内容的Cell上是可以使用这种方法进行预估的,也就是说,当根据用户输入的内容来实时改变Cell的高度,是可以使用该方法的。

  

 

 

四、Autolayout +CountHeight

接下来我们对上述的效果进行优化,不使用TableView的预估值了,而是直接使用我们在子线程中计算的文本高度。当然依然是使用AutoLayout的方式,将上述返回高度的方法heightForRowAtIndexPath中的内容进行替换,直接返回当前Model中Cell的高度,如下所示:

  

 

经过上面这么一修改,我们就可以将之前Cell高度计算的内容移到子线程中了,上述的卡顿问题会得到些微的解决。下方是该方式的运行效果,可以看出来比上述的实现方式稍微好一些,不过还是有些掉帧,掉帧也是比较严重的。

  

 

 

五、FrameLayout + CountHeight

上述结果仍然不理想,我们接着优化。我们不使用AutoLayout布局,我们直接使用Frame来布局,这样就减少了由AutoLayout转换到FrameLayout的时间。本部分我们就使用纯代码的方式,以Autolayout进行布局。在给Cell配置数据的时候我们根据Model中计算的高度来修改可变文字内容的高度,如下所示:

  

下方是使用这种方式最终的运行效果,从该效果中可以看出,效果还是蛮OK的。虽然有些掉帧,但是还是非常流畅的,这种流畅度是可以接受的。如果你不想使用第三方库的话,这种方式还是一个比较好的解决方案的。

  

 

 

六. YYKit + CountHeight

接下来我们进一步进行优化,引入第三方UI组件YYKit。将Cell上的组件替换成YYKit所提供的组件。然后使用Frame进行布局,当然也是在子线程中对Cell的高度进行计算了。当然此处只是对YYKit简单的使用,应该还有更好的优化方式,只是此处没有给出,欢迎相互交流。

  

看来将进行系统的基础控件换成了YYKit中的控件,下方是此解决方案的运行效果。单从效果上来看,还是比较流畅的,但是为达到完全不掉帧的效果。不过整体看来还是比较流畅的。

  

 

 

七、AsyncDisplayKit + CountHeight

接下来我们要用Facebook提供的第三方库来进行基础组件的替换,将我们使用到的组件替换成AsyncDisplayKit相应的Note,如下所示。这些Note是对系统组件的重组,对组件的显示进行了优化,让其渲染更为流畅。

  

下方就是使用AsyncDisplayKit重构后运行的效果。从下方的效果上来看,几乎不掉帧,那个流畅呢。如果你对UI流畅度要求比较高的话,那么AsyncDisplayKit是一个比较好的选择。不过会严重依赖AsyncDisplayKit,如果AsyncDisplayKit停止维护了,后期对AsyncDisplayKit进行替换的话,工作量还是比较大的。因为这种布局框架不像网络框架,我们可以对网络框架的调用进行提取,网络层统一对外接口,很方便切换到其他网络请求库。但是像AsyncDisplayKit这种框架会散布于UI层的各个角落,封装提取不易,更不用说轻而易举的替换了。所以像这种页面的实现,个人还是偏向于Framelayout + CountHeight的方式来实现。

  

 

 

八、Demo中用到的设计模式

经过上面这7步,我们Demo的功能以及效果已经介绍完毕,不同实现方式优缺点一目了然。该部分也是本篇博客最后一部分,我们就来聊一下本篇博客中所使用的设计模式。我们可以看出上述几个列表的页面是完全一样的,只是Cell的实现方式不同。所以我们可以将TableView提取成基类,TableView中所使用的Cell类型由子类来确定。说的官方一些,这就是策略模式。具体的Cell使用策略由具体的TableView来定,而父类TableView值负责根据子类提供的策略来进行Cell的初始化。

我们就以AsyncDisplayKitTableViewControllerFrameCountTableViewController这两个类为例,下方就是这两个TableViewController的相关代码。下方这两个类的基类都是SuperTableViewController。大部分工作都在基类中去实现了,而子类中只提供了使用Cell的策略。这就是策略模式的好处,便于扩充,如果有类似的页面,子类只提供Cell的类型即可。下方这两个类中的getReuseIdentifier方法就是为父类提供策略的方法。

   

当然不知上述类有父类,具体Cell的基类也得有父类,因为在TableViewController中声明Cell时用的是Cell的父类,如下所示。此处用到了面向对象的多态性,并且也用到了面向接口原则。此处SuperTableViewCell虽然是一个基类,但是它也担负着定义子类接口的责任。好处就不多说了吧。  

 

关于设计模式相关的内容,请查看之前发布的关于设计模式的系列博客设计模式系列,重构的内容的话请查看之前发布重构系列的博客《重构系列》。当然这两个系列的博客全是使用Swift语言实现的Demo,不过思想都是相同的。好了今天博客篇幅也挺长的,就先到这儿吧。

github分享链接:https://github.com/lizelu/DisplayTestDemo

 

posted @ 2016-10-08 09:33  青玉伏案  阅读(18700)  评论(7编辑  收藏  举报