iOS开发之多种Cell高度自适应实现方案的UI流畅度分析
laiyow
8年前
<p>我们以 TableView 中填充多个根据内容自适应高度的Cell来作为本篇博客的使用场景。当然Cell高度的自适应网上的解决方案是铺天盖地呢,今天我们的重点不是如何讨论Cell高度的自适应,而是给出几种Cell高度自适应的解决方案,然后对比起UI流畅度,从而得出一些UI优化的一些常规做法。今天博客中主要用涉及的第三方库是 YYKit 和 AsyncDisplayKit 。</p> <p>关于YYKit和AsyncDisplayKit这两个库,本篇博客只是简单的涉及到一些基本用法,主要是针对我们本篇博客的Demo来使用的,其中好多功能并未使用。因为之前在项目中没怎么使用过这两个框架,所以本篇博客就不着重介绍着两个第三方框架了,如果你对其感兴趣, Github 上有你想要的内容,请自行搜索。废话少说,进入今天的主题。</p> <h2><strong>一、总述</strong></h2> <p>本篇博客主要给出了 5种Cell 自适应高度的解决方案,并对比了每种实现方案的流畅度。也可以说是从UI最不流畅的一种我们慢慢优化,从而实现了这5种解决方案。当然我们是观察屏幕的 FPS 来判断屏幕在操作时是否卡顿。关于对FPS的实时监测,我参考了 YYKit-Demo中的做法 ,并将其单独提取了一个组件,便于我们项目的使用,关于这个提取的FPS组件,下方使用时会具体介绍。当然本篇博客所涉及的所有代码,依然会分享到Github上,文章后方会给出相应的链接,有需要的小伙伴请自行clone。</p> <p>下方这个截图是我们今天demo的菜单列表页面,点击每个Cell都会跳转左边这个内容列表页面。不过每个Cell所对应的内容页面的Cell自适应高度的实现方式不同,我们在对其滑动操作时,可以根据下方这个FPS组件来观察屏幕的流畅度。当然,每个内容列表页的布局和显示内容都是相同的,不过不同的Cell自适应解决方案所对应的UI流畅度也是不同的。下面我们先大体的聊一下每种Cell自适应的实现方案。</p> <ul> <li><strong>Autolayout + AutomaticDimension: </strong> 该解决方案对应着,下方第一个Cell, 点击该Cell进入的页面完全由 AutoLayout 进行布局,Cell自适应的高度也不用我们自己计算,而是使用系统提供的解决方案 UITableViewAutomaticDimension 来解决。当然,使用 UITableViewAutomaticDimension 要依赖于你添加的约束,稍后会介绍到。这种实现方案用起来简单,不过UI流畅度方面不太理想。当TableView快速滑动时,就会出现掉帧,卡的不要不要的。</li> </ul> <ul> <li><strong>Autolayout + CountHeight: </strong> 这种解决方案依然是采用 AutoLayout 的方式来对Cell的内容进行布局,不过Cell的高度我们是自己计算的,当然我们这个计算Cell高度的过程是放在子线程中进行的,所以这种实现方式要优于第一种实现方式,稍后会详细介绍。</li> </ul> <ul> <li><strong>FrameLayout + CountHeight: </strong> 为了进一步提高流畅度,我们采用了纯Frame布局,之前好像在哪儿看过,说Autolayout最终也是会被转换成Frame进行布局的,所以我们索性就使用 Frame 对整个Cell中的元素进行布局。当然Cell高度已经Cell中可变内容的高度都是在子线程中进行计算的,这也是优化很重要的一步。这种实现方式还是比较流畅的,可以作为折中的方案。</li> </ul> <ul> <li><strong>YYKit + CountHeight: </strong> 这种解决方案用到了 YYKit 中的控件,并且使用Frame布局与Cell高度的计算。这种方式要由于上面的解决方案,比较YYKit中的一些控件做了优化。</li> </ul> <ul> <li><strong>AsyncDisplayKit + CountHeight: </strong> 则是使用了 AsyncDisplayKit 中提供的相关 Note 代替系统的原生控件,这种实现方式是这5种实现方式中最为流畅的。稍后会详细介绍。</li> </ul> <p>上面这五种实现方式将是下方介绍的具体内容,当然会涉及一些其他的技术实现细节。</p> <p style="text-align: center;"><img src="https://simg.open-open.com/show/25058cf88d1072e2b96fc0dce815e219.png"> <img src="https://simg.open-open.com/show/8fb2986d0e77ffc4def6f3a15b6f7768.png"></p> <h2><strong>二、博客所涉及的自定义工具介绍</strong></h2> <p>在进入主题之前,先进行预热。先对本片博客中所涉及的一些小工具进行介绍。当然这些工具是自己封装的,是本篇博客中所涉Demo的基础,本部分将进行统一介绍,在使用时我们就一笔带过即可。</p> <p><strong>1.工具一:FPSDisplay</strong></p> <p>上述Demo中使用到了一个小的组件是 FPSDisplay , 用于实时显示屏幕的刷新频率的。我们知道现在iPhone的FPS是60。也就是每秒刷新60帧,如果低于60帧的话那就是掉帧了,如果掉帧掉的多的话就会明显的看出卡顿。上述截图中右下方的黑色图标就是我们封装的FPSDisplay工具。当然该工具是参考着YYKit-Demo中所实现的,对其进行的简化和封装,将其提取成了一个单独的组件,便于在我们的应用中引入。</p> <p>下方就是 FPSDisplay 引入并初始化的过程,下方是在AppDelegate中的 didFinishLaunchingWithOptions 中添加的。因为FPSDisplay是添加在 KeyWindow 上的,所以在FPSDisplay初始化时要保证你的App已经有了 KeyWindow 了。进行下方初始化后,在你的App的右下方就会出现一个图标来实时的显示FPS。</p> <p><img src="https://simg.open-open.com/show/164c09b9edecf9d396ec1992624780bf.png"></p> <p>FPSDisplay 的实现并不麻烦,主要是 CADisplayLink 的使用,将创建 CADisplayLink创建的对象添加到 MainRunLoop 中,就可以以此来计算FPS了。下方是FPSDisplay的核心代码。在每次进行屏幕刷新时都会执行下方的tink方法,我们可以来计算1秒内刷新的次数,也就是所谓的FPS。代码比较简单,在此就不做过多的赘述了,详细的代码在Github上已经分享。</p> <p><img src="https://simg.open-open.com/show/5bca0d7645a17161b662d3d882b202ca.png"></p> <p><strong>2.工具二:数据提供者</strong></p> <p>除了上述的 FPSDislay 工具外,我们还需要一个模块,那就是为Demo提供模拟数据的模块。因为我们没有网络模块,我们就模拟网络请求来生成数据,然后对数据进行处理生成Model。当然这个生成测试数据的过程没有用到主线程,为了 不阻塞Main线程 ,我们需要将数据生成的部分在子线程中异步的执行。当然此处主要涉及多线程的东西。下方代码段就是数据提供者 DataSupport 的核心代码。</p> <p>下方代码段主要用到了 并行队列的异步执行 , 任务组的使用 ,已经任务锁的添加。下方首先创建了一个并行队列 concurrentQueue 和队列的任务组group,并且为了数据同步,我们使用信号量创建了一个任务锁lock。在for循环中我们异步的执行并行队列来创建我们需要的数据模型Model。每循环一次创建一个Model,为了Model数据的独立性,在创建Model时,我们要为其添加 信号量同步锁 。</p> <p>当50条数据异步创建完毕后,我们需要将其提供给数据提供者的使用放,也就是在任务组中的任务都执行完毕后,会执行下方的notify方法。</p> <p><img src="https://simg.open-open.com/show/d7192eeb803db7671da78f1e2e024719.png"></p> <p>在Model创建时,我们会对Model中可变的文字,也就是Cell中高度变化的内容的高度进行计算。当然该计算是在子线程中异步执行的。所以不会占用主线程的时间来计算Cell的高度以及Cell中可变文字的高度。我们Model中有两个字段就是来存储Cell的高度以及可变文本的高度的,如下所示。这样做的好处就是提高UI的流畅度。</p> <p style="text-align: center;"><img src="https://simg.open-open.com/show/b6abd665de94292663eca47254f98ab5.png"></p> <p><strong>3.工具三:UIImage对象的Memory缓存</strong></p> <p>第三个工具也是为了提高数据流畅度而生的,就是图片的对象缓存。 我们将已经初始化过的图片进行缓存,等下次再使用该图片时直接从缓存中读取,从而节省了在主线程中创建对象和销毁对象的时间,从而可以提高UI的流畅度 。当然此处我实现的图片的内存缓存比较简单,也就是在本Demo中适用。不过原理还是OK的,全面的MemoryCache请参考YYKit中的 YYMemoryCache 。其中用到了 双向链表 以及 CFMutableDictionaryRef 来实现的MemoryCache,其源码并不是很难理解,有兴趣的小伙伴可以进行阅读呢。</p> <p>本篇博客所实现的Memory缓存就比较简单了,就使用了一个字典,字典的Key是图片的名称,字典的Value是已经创建的字典的对象。代码比较简单,下方是核心代码。大体原理就是在获取时,如果缓存字典中没有相应的对象就进行创建并加入缓存,然后返回该对象。如果缓存中已经有该对象,则直接返回。核心代码如下。</p> <p style="text-align: center;"><img src="https://simg.open-open.com/show/e2c2198e436c163a36639ce3904aee76.png"></p> <h2><strong>三、Autolayout + AutomaticDimension</strong></h2> <p>上一部分已经为Demo的开发做好了准备,接下来就开始进入今天真正的主题。首先我们来介绍 Autolayout + AutomaticDimension 的实现方式。使用这种方式来是Cell高度的自适应比较简单,但不高效。下方是我们所使用的Cell的布局,当然是使用AutoLayout来实现的。因为下方test的内容的长度是不定的,所以我们为test所对应的TextView添加的约束为( top, left right, bottom )。这样test的高度就可以随着Cell的高度而改变了。</p> <p style="text-align: center;"><img src="https://simg.open-open.com/show/0225995c53cc73e79cde2a15f79f79bb.png"></p> <p>约束添加完毕后,我们的工作基本上就已经完成了,接下来需要进行简单的配置,我们的Cell高度自适应就OK了。下方就是我们添加完约束后要做的事情,需要给我们的tableView设置一个预估值( estimatedRowHeight ), 然后在TableViewDelegate的 heightForRowAtIndexPath 方法中返回 UITableViewAutomaticDimension 该属性即可。这样Cell就可以根据可变的文字高度来自适应了。当然该方法在iOS8以上的系统上才可以使用。</p> <p><img src="https://simg.open-open.com/show/faf62362a1d5985fe7e3526b4d4c3f7b.png"></p> <p>经过上述这两步,我们的Cell就可以进行自适应了,下方是该解决方案所对应的运行效果。可以看出来卡顿还是比较明显的,掉帧比较严重,在Cell高度自适应时最好不要采用此方法。也就是说这种方法,并不适用在我们Cell列表中来预估每个Cell的高度。那这种方式是不是就没用了呢?当然不是,填写内容的Cell上是可以使用这种方法进行预估的,也就是说,当根据用户输入的内容来实时改变Cell的高度,是可以使用该方法的。</p> <p style="text-align: center;"><img src="https://simg.open-open.com/show/60fe0842b00afcc850b34eb72fd4d7ca.gif"></p> <h2><strong>四、Autolayout +CountHeight</strong></h2> <p>接下来我们对上述的效果进行优化,不使用 TableView 的预估值了,而是直接使用我们在子线程中计算的文本高度。当然依然是使用 AutoLayout 的方式,将上述返回高度的方法 heightForRowAtIndexPath 中的内容进行替换,直接返回当前Model中Cell的高度,如下所示:</p> <p><img src="https://simg.open-open.com/show/3d9b9e3f03d758b84e610a51ad043144.png"></p> <p>经过上面这么一修改,我们就可以将之前Cell高度计算的内容移到子线程中了,上述的卡顿问题会得到些微的解决。下方是该方式的运行效果,可以看出来比上述的实现方式稍微好一些,不过还是有些掉帧,掉帧也是比较严重的。</p> <p style="text-align: center;"><img src="https://simg.open-open.com/show/5bcaac2cf38febf545ae7a1eb039a502.gif"></p> <h2><strong>五、FrameLayout + CountHeight</strong></h2> <p>上述结果仍然不理想,我们接着优化。我们不使用AutoLayout布局,我们直接使用 Frame 来布局,这样就减少了由AutoLayout转换到FrameLayout的时间。本部分我们就使用纯代码的方式,以Autolayout进行布局。在给Cell配置数据的时候我们根据Model中计算的高度来修改可变文字内容的高度,如下所示:</p> <p><img src="https://simg.open-open.com/show/715d7fd43fc3f2a32260c2ad7d370d84.png"></p> <p>下方是使用这种方式最终的运行效果,从该效果中可以看出,效果还是蛮OK的。虽然有些掉帧,但是还是非常流畅的,这种流畅度是可以接受的。如果你不想使用第三方库的话,这种方式还是一个比较好的解决方案的。</p> <p style="text-align: center;"><img src="https://simg.open-open.com/show/7e313e069fc99bcc8739516608464b29.gif"></p> <h2><strong>六. YYKit + CountHeight</strong></h2> <p>接下来我们进一步进行优化,引入第三方UI组件YYKit。将Cell上的组件替换成YYKit所提供的组件。然后使用Frame进行布局,当然也是在子线程中对Cell的高度进行计算了。当然此处只是对YYKit简单的使用,应该还有更好的优化方式,只是此处没有给出,欢迎相互交流。</p> <p style="text-align: center;"><img src="https://simg.open-open.com/show/99d6e7480acf25a8e9c6506d13d255bf.png"></p> <p>看来将进行系统的基础控件换成了YYKit中的控件,下方是此解决方案的运行效果。单从效果上来看,还是比较流畅的,但是为达到完全不掉帧的效果。不过整体看来还是比较流畅的。</p> <p style="text-align: center;"><img src="https://simg.open-open.com/show/e6b2f9d88136185165a6abdc6e216d0b.gif"></p> <h2><strong>七、AsyncDisplayKit + CountHeight</strong></h2> <p>接下来我们要用非死book提供的第三方库来进行基础组件的替换,将我们使用到的组件替换成 AsyncDisplayKit 相应的Note,如下所示。这些Note是对系统组件的重组,对组件的显示进行了优化,让其渲染更为流畅。</p> <p style="text-align: center;"><img src="https://simg.open-open.com/show/9a73e5a5cd7cf1e25060d9ecf0c5191d.png"></p> <p>下方就是使用 AsyncDisplayKit 重构后运行的效果。从下方的效果上来看,几乎不掉帧,那个流畅呢。如果你对UI流畅度要求比较高的话,那么AsyncDisplayKit是一个比较好的选择。不过会严重依赖 AsyncDisplayKit ,如果AsyncDisplayKit停止维护了,后期对AsyncDisplayKit进行替换的话,工作量还是比较大的。因为这种布局框架不像网络框架,我们可以对网络框架的调用进行提取,网络层统一对外接口,很方便切换到其他网络请求库。但是像AsyncDisplayKit这种框架会散布于UI层的各个角落,封装提取不易,更不用说轻而易举的替换了。所以像这种页面的实现,个人还是偏向于Framelayout + CountHeight的方式来实现。</p> <p style="text-align: center;"><img src="https://simg.open-open.com/show/0ea7ee98638d4bbf936d1415d5cea416.gif"></p> <h2><strong>八、Demo中用到的设计模式</strong></h2> <p>经过上面这7步,我们Demo的功能以及效果已经介绍完毕,不同实现方式优缺点一目了然。该部分也是本篇博客最后一部分,我们就来聊一下本篇博客中所使用的设计模式。我们可以看出上述几个列表的页面是完全一样的,只是Cell的实现方式不同。所以我们可以将TableView提取成基类,TableView中所使用的Cell类型由子类来确定。说的官方一些,这就是 策略模式 。具体的Cell使用策略由具体的TableView来定,而父类TableView值负责根据子类提供的策略来进行Cell的初始化。</p> <p>我们就以 AsyncDisplayKitTableViewController 和 FrameCountTableViewController 这两个类为例,下方就是这两个TableViewController的相关代码。下方这两个类的基类都是 SuperTableViewController 。大部分工作都在基类中去实现了,而子类中只提供了使用Cell的策略。这就是策略模式的好处,便于扩充,如果有类似的页面,子类只提供Cell的类型即可。下方这两个类中的 getReuseIdentifier 方法就是为父类提供策略的方法。</p> <p style="text-align: center;"><img src="https://simg.open-open.com/show/4df508af853b86a3fc1606fe03bb21a4.png"> <img src="https://simg.open-open.com/show/1cbe712ac00552da454e389ba3ec731e.png"> </p> <p>当然不知上述类有父类,具体Cell的基类也得有父类,因为在 TableViewController 中声明Cell时用的是Cell的父类,如下所示。此处用到了 面向对象的多态性 ,并且也用到了 面向接口原则 。此处 SuperTableViewCell 虽然是一个基类,但是它也担负着定义子类接口的责任。好处就不多说了吧。 <img src="https://simg.open-open.com/show/2732284aae7de5ec8d0da92d515fb0da.png"></p> <p> </p> <p> </p> <p>来自:http://www.cnblogs.com/ludashi/p/5895725.html</p> <p> </p>