iOS开发针对对Masonry下的FPS优化讨论
今天博客的内容就系统的讨论一下Masonry对FSP的影响,以及如何更好的使用Masonry。如果你对iOS开发足够熟悉的话,那么对Masonry框架应该不陌生。简单的说,Masonry的诞生让AutoLayout的使用更为优雅,让控件的布局更为方便。使用辩证的观点来看一个事物的话,凡事都有两面性,Masonry的使用也不例外。Masonry框架的使用不当会直接影响当UI的FPS。今天我们就来讨论一下在使用Masonry时的一些误区,看一下那些影响性能的使用方式。本篇博客我们依然会依托于Demo来叙述的一些东西。
之前写过一篇文章是专门来介绍Masonry框架的,并且对该框架的源码进行了相关解析,详细内容请移步于《iOS开发之Masonry框架源码解析》。
一、Demo综述
1.运行效果
先入为主,本篇博客的内容依然是依托于我们特意为本篇博客所打造的Demo的,首先我们先来看一下Demo运行起来是怎样的效果,通过Demo我们可以看到那些问题,以及这些问题是如何被解决的。下方就是我们本篇博客所涉及Demo的运行效果。
从下方的运行效果不难看出,我们是分了6种情况来观察和判断Masonry的各种使用方式对FPS的影响如何。上方通过六个SegmentControl可以去切换Cell的布局方式。当然每种布局方式所呈现出来的Cell是相同的。这样也是做实验时保持实验项改变其他项保持一致的原则。我们可以通过右下方FPS指示器来直观的感受一下FPS的变化趋势。下方这个FPS显示控件是从我们之前的Demo中拿过来的。之前的Demo也是关于FPS优化的,只不过是关于Cell高度动态计算的FPS优化,详情请移步于《iOS开发之多种Cell高度自适应实现方案的UI流畅度分析》。
下方Cell中所显示的数据时随机生成的,左边的Image也是随机取的。右边的Title和Detail都是NSAttributedString并且下方的有些Detail有可能为空。如果某一条的Detail为空,那么该条Detail下方的所有内容的布局上移。稍后会详细的介绍该Demo以及其中的技术点。
2、模拟网络请求
上面Cell中显示的数据是通过模拟网络数据来获取的,下方就是我们的模拟网络层的相关代码。毕竟是Demo,并且Demo的重点不在网络层上,下方就简单的写了一下,代码比较简单。就是一个单例+一个模拟数据随机生成的方法,然后把这个随机生成的数据通过Block回调到网络层的使用者上。具体代码如下所示:
3、上述Cell的基类XBaseTableViewCell
上面每种Segment中的Cell都是一种独立的Cell类型,不过这些Cell有一个共同的父类。而这个父类就负责来处理这些Cell所共用的逻辑。下方的这个XBaseTableViewCell就是上述显示的所有Cell的基类。其中声明并初始化了Cell上的所有控件。并且提供了相关的设置值的方法。
从下方的代码中不难看出,有两个方法是需要子类进行重写的,一个是给控件添加布局的方法addLayoutSubviews, 另一个就是更新布局的方法updateLayoutSubviews方法。每个子类中都会对这两个方法进行重写并给出不同的布局方式。
4、Segment中切换Cell的代码
下方是在相应的VC中的点击SegmentControl的逻辑代码, 点击不同的Segment会选择不同的Cell然后刷新TableView,代码比较简单就不做过多的赘述了。
二、对上述各种的布局方式进行分析
接下来要做的事情就是分析一下每种布局方式对FSP的影响,下方会对不同的布局情况使用Instrument进行分析并看一下具体的数据。下方分别讨论了只使用Masonry的Update操作、Remake操作、先Make后Update、Frame操作以及先Make后Frame操作。
1、update
首先我们来看一下update操作。也就是使用update直接给控件赋值,这是比较偷懒的一种操作。因为在我们的Demo中在设置cell的值时会更新一些控件的UI布局,所有我们索性就直接使用Masonry的update,直接给控件添加约束。在Masonry中的update操作有个特点,就是update一个约束会先在已添加的约束数组中找到该约束,然后更新该约束,如果找不到就install添加相应的约束。从这个update的功能来看其效率是比较低的。
我可以先看一下代码实现,在子类XUpdateLayoutTableViewCell中,重写了addLayoutSubviews和updateLayoutSubviews两个方法。在updateLayoutSubviews方法中,为所有的控件使用update的方式添加约束。下方这样写会在每次设置值的时候都会调用下方的updateLayoutSubviews方法,这样就会更新cell上的控件的所有布局,当然,不建议这样去做,因为这样会更新那些不需要更新的约束。之所以今天罗列出来,是因为在开发中下方的问题确实存在,也许是因为时间紧张,也许是因为其他原因导致的下方这种代码实现。
我们先来使用Instruments跑一下上述的Demo,然后直观的感受一下该Demo的Core Animation的直观表现。下方就是我们将SegmentControl切换到Update时所对应的FPS数据。从下方的数据我们不难看出,直接用Update添加更新约束这种做法是比较影响FPS的。当然,Cell中还会使用到属性字符串,这个我们稍后会讨论一下的。
我们可以来跑一下Update状态下的Time Profile。如下所示,从下方的结果中不难看出,在Cell更新数据时,有两块的操作比较耗时。一个是Masonry的update操作,另一个则是Label设置NSAttributedString的操作。因为我们使用的每个Label都会赋值一个属性字符串,这个是比较耗时的操作。还有一个要明确一点的是,属性字符串的创建和生成并不会占用多少时间,而属性字符串的赋值和渲染所占用的时间是比较多的,这一点从下方的Time Profile中也是不难看出的。
2、remake
接下来我们在来看一下Remake操作,从下方的Core Animation的结果中不难看出,其所表现出来的效果还不如使用Update操作呢。下方的FPS比Update要低一些,这也与remake自身的操作有关系,remake从字面意思来看就是重新制作,如果之前已经添加过约束的话就先移除掉,然后再添加新的约束。
下方是Remake所对应的Time Profile,从结果中我们可以看出布局更新占用了66.6%的耗时,而且33%的install耗时中uninstall占用了10%左右的开销。在Masonry中remake效率是*低的。稍后我们会继续进行讨论。
3、make + update
讨论完update和remake, 我们来讨论一下使用Masonry的常规做法。就是使用make来初始化控件的布局,使用update来更新所需要更新的约束。因为代码比较简单,就不一一往上贴了,但是跑一下使用Instrument跑一下还是很有必要的。下方是make + update 的方式的Core Animation所跑出来的结果。但从下方的FSP结果来看,还是要比之前只使用update或者remake的效果要好一些的,不过下方的FPS还是不高,稍后我们会将下方的数据进行细化。
该部分的Time Profile就不跑了,因为设置值的时候我们依然采用的Update来更新的约束,只不过不是更新所有的约束,而是更新那些只需要更新的约束。因为更新的约束的量会少一些,所有FPS的表现效果会比之前更新所有的约束会更好一些。make + update的方式会是FPS稍微改善一些,但是从下方的图中我们可以看出,改善的并不是特别好。
4、frame + frame
接下来,我们就不用Masonry来布局了,我们直接使用Frame布局。因为Autolayout*终仍然会转换为Frame布局的,很显然Frame布局在性能方面是优于Autolayout布局的。接下来我们就来使用Frame布局然后使用Frame更新。下方的FPS还算说得过去,不过还不是满格,其大部分原因就是因为NSAttrubitedString的原因了。
我们可以看一下更新Frame的Time Profile,如下所示。从下方的截图中,我们不难看出update frame的时间占比只占到了2.5%,之前更新约束能占到60%左右,可以看到使用Frame布局的好处。从下方的分析结果中不难看出,现在影响FPS主要因素已经从更新布局转化到了AttributeString的设置。这也是上述FPS没有满格的原因。
5、make + frame
Masonry的诞生就是为了方便控件布局的,Frame布局不够灵活,适配起来比较繁琐,所以才有了AutoLayout。不过虽然AutoLayout可以很方便的适配屏幕,可是其性能方面表现的不是特别好。那么我们可不可以将两者进行结合呢。也就是说使用make来初始化控件的布局,使用Frame来更新布局。当然这一过程不是简单的在设置值的时候更新一下Frame就可以的,因为在Cell设置值的时候去更新Frame是没用的,因为更新完Frame后,在渲染显示的时候,还是会按照AutoLayout的布局来显示的。我们需要做的是将Frame布局放到Autolayout布局之后,此处我们要做的就是将更新Frame的相关代码放到下一个Runloop中来执行。更新Frame代码如下:
在cell中是make初始化控件布局,使用Frame更新布局,和Frame+Frame的方式差不多,只不过是使用Masonry布局时,在首屏加载的时候不如Frame布局,以后更新是一样的。下方是使用Masonry+Frame的形式的Core Animation的结果。效果虽然比上一部分会稍微差一些,但是*终的效果还是满Ok的。
三、总结
本篇博客只讨论Masonry的布局方式对FPS的影响,至于上述的NSAttributeString的问题不做过多赘述了。如果根据业务需要,有许多富文本的展示影响了FPS的话,那么可以采用其他的方式来进行优化,比如使用AsynDisplayKit所提供的相关Node进行显示等等。在博客的结尾,还是有必要做一个总结的。
下方是我们在代码中更为细化的数据,从数据中不难看出Remake的性能是*差的,所以我们在使用Masonry时尽量要少使用Remake。对控件的更新只一味的选择使用Update也不是一个好的选择,如果要使用Masonry框架还要对控件进行布局更新的话,*好是把那些不变的约束和需要更新的约束分开。使用make来添加相关约束,使用update来修改需要更新的约束。当然使用Frame布局的性能会好一些,不过布局过程过于繁琐不便于进行屏幕的适配。当然也可以使用Masonry进行布局使用Frame进行布局的更新,当然需要注意的是Frame布局更新的时机,需在Autolayout加载的时机后进行。
下方是进行了统一的数据统计,当然是针对本篇博客所对应的Demo的。下方表格中统计了一次更新cell布局所采用的不同方式的平均时间,从下方的数据中我们不难看粗Remake的更新布局用时*多,消耗了12+ms, 而Update所有的约束用时也是不少,一次更新布局使用了9+ms。而只更新需要更新的布局用时7+ms, 稍微要比更新所有的布局要好一些。当然直接修改Frame的用时*少,只用了0.06+ms的时间,从该数据可以直观的感受到Frame布局的效率性。
而右边还给出了一个属性字符串的创建和赋值的用时,其中我们可以看到,属性字符串的创建耗时并不是太多,而比较耗时的是属性字符串的赋值,每次赋值占用了0.7ms, 如果是10个的话,那么赋值时间就是7ms, 如果属性字符串的内容再复杂一些,那么用时肯定会比这个高。当然我们可以使用第三方提供的一些控件和方法将这部分时间给优化掉,这个可以放到以后再讨论。
今天的博客就到这儿吧,目的是在使用Masonry时要合理的进行使用,有必要时,可以使用Frame进行布局。