iOS系列开发-UITableView性能优化
在我们的日常开发中,很多开发人员最常接触的就是UITableView或者UICollectionView来布局某些列表等界面.
这里我们就拿UITableView来作为说明内容
绝大部分的时候,一个UITableView的内容不会很多,cell的样式\高度也不会很多元化,其仅仅作为一个展示用的UITableView来说,很多时候其性能都是很不错的.
但是也会有小众的时候,一个作为列表展示的界面会有很多很多数据,而且是实时的会加载很多新的内容,表格的样式也不唯一,有的仅有文字,有的仅有图片,有的高度很长,在复杂点的很多cell虽然总体上差不多,但是却会有很多或多或少的布局上的不一样或者组件上的差距
当然,并不是说有了这些复杂的内容,我们的UITableView就会性能变差,但是我们却可以说性能上比较差的UITableView,很多原因都是因为这些不定的因素,复杂的逻辑判断,复杂的数据处理,复杂的图形渲染,复杂的高度计算等等导致的.
而且如果表格的性能真的已经变差了,那么其调优的步骤是一定需要进行的,否则作为开发人员,我们过不了自己的关,过不了测试的关,过不了产品的关...
但是说白了UITableView也就那么些方法
我们能够利用的有哪些?
@property (nonatomic) CGFloat rowHeight;
是的,你会发现当rowHeight时唯一固定的时候,往往性能不会太差
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath;
我们都知道,当我们使用了代理的方法之后,上面的行高属性就会失效了,但是相对应的,我们需要给每一行都返回一个行高,这个是无法避免的,那么我们能否在这里做些优化呢?
答案是肯定的,而且是显著的.
为什么这么说呢?
当我们的UITableView是一个动态行高的表格的话,我们以往的算法就是动态计算,什么意思呢?
大致是这样的
-(CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath{
DetaileModel *model = _listArray[indexPath.row];//取出模型
return [DetailCell heigthForModel:model];//计算所需高度并返回
}
但是响应的,因为内容的多样化,可能有,这部分的计算本事虽然不会很耗时,但是因为我们reloadData的时候或者滚动的时候,其都需要不停的计算高度并返回,这样就会造成很多的展示表格或者创建或者重用表格cell的时候都需要耗时在计算高度的这部分上,那么我们如何调节呢?这部分计算肯定是需要的,此时你可能会说,iOS现在支持自适应高度,
@property (nonatomic) CGFloat estimatedRowHeight NS_AVAILABLE_IOS(7_0); // default is 0, which means there is no estimate
是的,UITableView中,我们可以写一个预估行高,然后使用xib或者storyboard或者手动layout的时候编写好约束的时候,即可动态自动的返回行高,但这样的性能其实更差,时间全部都在调节约束上面了,然后硬生生的依靠约束来调节行高,这样的性能只会更差,不推荐.
所以我们可以采取的是
-(CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath{
DetaileModel *model = _listArray[indexPath.row];//取出模型
return model.cellHeight;//获得所需高度并返回
}
如果我们把cellHeight当做一个属性,提前缓存好,之后直接获取怎么样呢?
此时你就会说,这样有什么用吗?
简单的说几点
1.使用内存换性能
2.不需要重复计算
为什么这么说呢?
首先我们会发现,往往我们计算行高和设置cell的时候都是根据模型数据来判断哪些需要显示哪些不需要显示,显示的话显示的位置和大小分别是多少,我们都是根据数据模型的内容来判断的.那么我们如果这设置这个模型数据的时候(网络请求回来json转模型的时候),我们就根据这个模型的数据添加一个cellHeight的属性,并且直接计算好保存在模型中,当做模型的一个字段,那么我们在reloadData的时候我们就会发现我们把计算高度的事情提前一步做好了,我们只需要返回model的cellHeight即可.而且因为其存储在该模型数据中,那么其跟模型一样,一直是存在的,我们即可以在重新刷新表格或者滚动视图的时候都不需要再一次计算了(只要改模型存在,那么该cellHeight属性就一直存在).
我们会发现仅仅是一个提前计算并存储起来,一个表格的性能就会大大提高,一些稍微不是特别复杂的表格此时已经就能够很流畅了.
至此我们会发现我们使用了类似设置cell的方式
-(UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{
static NSString *cellid = @"cellid";
DetailCell *cell = [tableView dequeueReusableCellWithIdentifier:cellid];
if (!cell) {
cell = [[DetailCell alloc]initWithStyle:UITableViewCellStyleDefault reuseIdentifier:cellid];
}
cell.model = _listArray[indexPath.row];
return cell;
}
很多的开发者都使用类似的方式,在cell中有一个model属性,或者设置model的方法,以此来通过传递一个model来设置数据,可能在自定义的设置方法中,可能在model的setter方法里面,此时我们又会一到一个新的问题,某些model本身并不深很复杂,但是我们需要对这些数据做一些处理之后在做展示,比如图文混排,比如拼接分割转换等处理字符的显示,这些在以往的时候,我们都是放在设置cell的数据源方法里面,通过模型的传递过来,然后通过进一步的转化或者修改来决定如何显示数据. 但是同样的,此时,我们就需要不停的在表格更新显示的时候来计算.
此时你肯定会想到,处理方法和上面一样,提前计算好! 是的,我们在设置模型的时候就提前计算好,在模型中添加新的字段,存储这些需要计算的数据,在cell展示的时候直接拿过来用即可.
但是问题来了,新的数据模型仿佛不是数据模型了,参与了很多的计算(内容计算,行高计算...)
那么我们能不能优化一下呢?
这是我们之前的逻辑
这是我们现在的逻辑
我们能够轻松的优化了红色部分的性能了
原因是我们把所有的需要提前计算的东西都提前了,将原来的需要计算的部分全部转化成了一个内存,使用内存来代替之前的计算,在需要的时候只需要从内存中直接拿出来使用即可.优点上面简单的说明了,只需要在赋值的时候计算好即可,之后就不需要计算了,以后的刷新都是直接从内存中取出所需要的内容或者行高即可
但是,相对应的,模型就变的不在像模型了,(此时的模型中有了计算行高的部分,有了计算新的内容的部分),并且跟网络返回回来的模型已经不是同样的一个模型数据了.比原来有了更多的字段,或许在以后的时候中会有或多或少的问题,比如字段重复却不知道,比如模型传递,传递多次之后,已经不知道哪部分是后来自己添加的属性了...
那么我们使用新的数组来替代呢?我们使用一个新的数组来替代原来的模型数组
使用一个自建的模型类来代替原来的模型类,里面只有需要的内容,比如行高,比如所有需要显示的内容,这样的一个模型类来组成新的模型数组.
然后UITableView只需要处理新的模型数组来呈现即可.
但是这样的的数组你会发现,丢失了原来的模型.比如我们点击某一个cell的时候我们需要将模型传递到下一个界面,此时拿到的数组就不是该模型了,而是我们自己定义的一个新的模型,这样会不会就不太方便呢?
那么我们进一步扩展,我们创建一个DetailModelManager的类,这个类里面有这样的一个属性,model, 我们在其set方法中做几件事,第一件,就是普通的set,第二件是计算好一个cellHeight并保存为DetailModelManager的属性,而不是model的一个属性,计算好(或者赋值)所有需要显示的内容的字段,并保存为一个个的属性
现在的逻辑变了
其实熟悉MVVM框架的同学此时就肯定笑话我了,这个不就是MVVM的VM嘛
是的,这其实就是MVVM的VM部分的一块优势了,现在的列表数组已经变成了DetailModelManager(ViewModel)数组了,所有的需要复杂计算的部分我们都可以以类似的方式来处理,比如我项目的一个列表
此时的列表不是单纯的模型数组
tableView中写法稍变
原来所有的model部分转化成viewModel部分
那么viewModel部分做了什么?
是的在之前的setModel的方法中我就做了很多事情,把后续需要计算的很多东西都计算结束并保存为属性了
至于cell就简单了,原来的model换成viewModel,原来的model的属性,现在变成了viewModel的属性了,而且完全不需要任何计算.直接都是拿过来使用即可,
后期如果显示的内容不符合需求,只需要修改viewModel的那个字段的计算即可.其他任何地方都不需要修改.
呃,不知不觉说了部分的MVVM的内容,虽然和UITableView的性能优化没有关系,但是毕竟是性能优化的一个实现方式,所以就一并说了一点
- 总之就是行高一定要缓存
另外,我们肯能会遇到这样的需求,一个稍微复杂的cell中可能有九宫格展示图片(如新浪微博),可能有滚动视图展示图片(如很多视频类app),这个时候,我们会发现,用来展示图片的cell展示的内容是动态的,即数量是不定的,于是我们很多人就都会想到,九宫格啥的或者滚动视图啥的,我们都可以用coolectionView来实现嘛,完全动态保证,多好...
但是其实这样做仍然是有很多麻烦的.
首先是层级关系,一个cell中包含collectionView等虽然不会有很多影响,但是这个cell的代码就会变的很庞大,很麻烦.而且动态添加的方式会很消耗性能,原因上面也差不多说了,我们上面一直就是在做表格的性能优化,你在cell中又加一个表格,虽然不复杂,但是该遇到的性能问题一个都不会丢的全部触发,而且或许是double.
如何做一点优化呢?首先我们会发现诸如新浪微博的九宫格图片展示样式其实也是有规律的,其最多展示9张图片.所以我们其实没有必要使用动态创建的方式,我们在创建cell的时候就创建9个imageView即可.在使用或者复用的时候,根据图片个数或者展示需求来保证其显示或者隐藏即可.这样的cell你会发现在层次上和你创建collectionview是一样的,但是在展示上,其非动态获取数据源展示和创建cell,其就是简单的设置图片的显示和隐藏即可,然后根据图片的格式,调整好约束就好了.一个简单的处理我们会发现效果是很明显的,这样能尽可能的减少cell创建或从缓存池取时因为布局子控件所消耗的时间.
所以说
所有的子视图都要预先创建
如果不需要显示可以设置hidden
另外我们是否会发现我们在开发app的时候,A做push动作到B.如果B没有设置背景色的话,会出现一个明显的卡顿?所以避免颜色导致问题,我们最好都给子视图设置好背景色.虽然不会有太大的影响,但是还是有点效果的
此外,栅格化也是我们可以关注的一点,首先栅格化是设计中的术语,但是用在我们的开发同样适用,当我们遇到表格中的cell层级很多的时候,我们是可以做个栅格化的操作,就是将 cell 中的所有内容,生成一张独立的图像,在屏幕滚动时,只显示图像 设置属性 self.layer.shouldRasterize = YES;虽然代码很简单,但是你会发现滚动的效果会很明显,当然栅格化的同时必须指定分辨率,否则默认使用 1倍的scale 生成图像! 需要设置 self.layer.rasterizationScale = [UIScreen mainScreen].scale;
当然既然有栅格化优化滚动,那么还有操作来优化其他的,比如绘制.
我们在开发中都会用到SDWebImage等类似的图片异步加载的.来保证每个cell中的网络图片在加载数据的时候不会占用主线程,从而来保证cell的流畅性,当然,其实我们不仅仅能做到这点,我们还可以设置cell的异步绘制,如果 cell 比较复杂,可以设置cell图层的属性 self.layer.drawsAsynchronously = YES;代码同样简单的可以忽略,但是效果也是显著的.
所以
- 异步加载是很需要的
所以总的来说我们在开发表格的时候,我们需要关注的其实无非也就是简单的几点
- 对行高的缓存------不要频繁的动态计算行高,少使用系统自动预估和动态行高,主动负担cell的布局,而不是依靠系统自动'撑开',在这块处理上我们可以使用内存换计算的方式,把内存在模型构建完成的时候就算出来,提前保存好.之后直接使用即可.比如把行高计算放入model、viewmodel的初始化中或者设置数据的方法中,优势是不需要重复计算,减少计算行高时间花销
- 子视图提前创建,不要动态创建------对于视图的创建,每个人都有自己的方式,但是对于cell中来说,因为cell本身就是一个动态的,所以在对其复用或者创建的时候我们就要尽量避免使用动态的方式创建子视图,无论是先removeSubViews再addSubView 亦或者是使用collectionView等动态视图,这些都要在复用的时候避免,如果可以,提前创建好需要的子视图.在需要显示的时候显示,在不需要显示的时候隐藏,这样其创建的耗时工作仅在第一次创建的时候,之后的复用都不会再次发生创建和删除视图等操作,一个简单的隐藏就可以达到我们的需求,何乐不为?
- 图片等展示异步加载------对于这一点,大家都很有心得.或者说大家都会在开发的时候很直接的做到.如果讲cell中的网络图片的加载使用主线程,那么何止是cell会卡顿,简直都是太小白的操作了.
- 尽量减少cell的层级------子视图的层级不易过多,(不仅仅是cell的层级,整个应用的视图中过多)层级如果太多,系统对视图的渲染压力就会相应的变大,毕竟很明显的道理是越少越好蛮.至于如何减少cell的层级,很多方式,比如我们使用drewRect的方式来绘制文字或者图片,而不使用label等,比如我们使用UItextField来充当label,这样既可以在某些时候仅展示文字,某些时候用来编辑文字,做到一个控件两种使用方式(在类似需要填写或者选择等信息的界面中我都会这么用),当然如果你对性能要求很高,且技术足够,你可以完全手工绘制cell,对于此你可以简单的学习setNeedsDisplay
- 图层颜色的选择------透明图层对渲染性能会有一定的影响,系统必须将透明图层与下面的视图混合起来计算颜色,并绘制出来。减少透明图层并使用不透明的图层来替代它们,可以极大地提高渲染速度。
- 为代理方法瘦身-----自从我们学会的block的时候我们会发现我们在任何地方都习惯使用block的方式来代替代理,在cell的创建中也不例外.比如cell中包含按钮的点击事件,我们现在都会使用block的方式来传递出来,但是不知不觉中cell的代理方法我们会发现很庞大,其实这样做是否真的好呢?其实不是,首先庞大的代码就会影响阅读,其次,每一个block都会开辟一个空间,所以说你别看只写了一个block,但是相对应的你却付出了block*个数的内存,但其实他们的内容完全一致,代理之所以存在是有其意义的,一味的使用block来替换代理其实是不对的,我们要在合适的时候用合适的方法.cell中的按钮点击,诸如此类的,仍然建议还是使用代理传递出来.你不会发现性能上的优化,你却能发现代码上的优化.
- 减少离屏渲染------离屏渲染,指的是GPU在当前屏幕缓冲区以外新开辟一个缓冲区进行渲染操作。设置了以下属性时,都会触发离屏绘制:shouldRasterize(光栅化)masks(遮罩)shadows(阴影)edge antialiasing(抗锯齿)group opacity(不透明)复杂形状设置圆角等 渐变... ,我们的cell开发虽然不在,但是不会太过分,我们用到的比较多的就是几种,如圆角图片,其会造成很明显的离屏渲染,从而造成性能上的变差.所以肯定是要对其优化的,当然网络上有很多教你设置圆角图片的贴文,不仿看看
- cell栅格化------当shouldRasterize设成true时,layer被渲染成一个bitmap,并缓存起来,等下次使用时不会再重新去渲染了。实现圆角本身就是在做颜色混合(blending),如果每次页面出来时都blending,消耗太大,这时shouldRasterize = yes,下次就只是简单的从渲染引擎的cache里读取那张bitmap,节约系统资源。如果在滚动tableView时,每次都执行圆角设置,肯定会阻塞UI,设置这个将会使滑动更加流畅。
- 异步加载------不仅仅是网络图片的异步加载.drawsAsynchronously属性也是我们需要关注的.
- 性能优化不是唯一的固定的,如果你的性能足够,那么你不优化也行,如果你的性能不够,如何优化,优化哪些都是可选的.非固定的
性能上的优化不仅仅如此,不仅仅是表格,
在这里推荐一部iOS开发必看书籍,如果融会贯通,你的开发经验足以让别人仰视对待,
ios核心动画高级技巧(https://www.gitbook.com/book/zsisme/ios-/details)