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

一、总述

本篇博客主要给出了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的初始化。

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

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

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

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,294评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,493评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,790评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,595评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,718评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,906评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,053评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,797评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,250评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,570评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,711评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,388评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,018评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,796评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,023评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,461评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,595评论 2 350

推荐阅读更多精彩内容