这篇文章是一个UITableView
的优化过程,即优化过程工具的使用。环境是Xcode11.6版本。
基本需求:
1、可以根据 JSON
文件配置设置 cell
的显示类型和 cell
的一些基本样式的修改,这样需求下对 cell
实现,没有想到好的方法,采用最笨的方法,就是 cell.contentView
上添加的 view
的方式。而view
根据 cellModel
的 templateName
、data
和style
传到 factory(工厂)
的形式生成。 cell.contentView
添加之前需要先上移除所有子视图,再重新创建 View
添加到 cell.contentView
上。
下面是view
根据 cellModel
的 templateName
、data
和style
传到 factory(工厂)
的形式生成核心代码(不是重点)
- (UIView *)setContentViewByModel:(CellBaseModel *)model indexPath:(NSIndexPath *)indexPath{
NSDictionary *dic = @{@"templateName":model.templateName,@"data":model};
__weak typeof(self) weakSelf = self;
CellBaseView *view = (CellBaseView *)[[UITemplateManager sharedInstance] viewWithTemplateData:dic constraintWidth:self.view.frame.size.width];
view.closeBlock = ^(UIView * _Nonnull actionView) {
[weakSelf setHomePopoViewFromActionView:actionView indexPath:indexPath model:model];
};
return view;
}
2、需要预加载,就是在滑动 tableView
列表时,即将显示完的时候,提前加载数据,体验无缝加载,没有等待。
cell
高度处理
1、tableView 使用的 UITableView+FDTemplateLayoutCell
来计算高度,并缓存高度处理。
CGFloat height = [self fd_heightForCellWithIdentifier:cellId
cacheByIndexPath:indexPath
configuration:^(id cell) {
XXXXXXX // cell的配置和赋值
}];
return height;
使用
UITableView+FDTemplateLayoutCell
优点是使用Layout
来计算高度并缓存,不用每次都计算,虽然系统有UITableViewAutomaticDimension
也是Layout
计算高度的,但是并没有缓存机制,所以性能上也就略有缺陷。
使用UITableView+FDTemplateLayoutCell
时,如果界面比较复杂,而且高度变化比较多,建议使用frame
计算并存储在dataModel
中,因为UITableView+FDTemplateLayoutCell
计算高度主要是用系统的方法[cell.contentView systemLayoutSizeFittingSize:UILayoutFittingCompressedSize].height;
来计算的。
UITableView+FDTemplateLayoutCell
也可以使用frame
计算高度,只需要设置属性fd_enforceFrameLayout=YES
UITableView+FDTemplateLayoutCell
有一个缺点,如何布局有些异步操作,那计算高度将会不准确。需要注意
2、加载数据使用AFNetworking
加载数据。
tableView
常见的卡顿原因
卡顿最常见的网上的文章比较多,就不一一列举了,列举几个常见的原因:
- 计算高度比较耗时
- 主线程加载图片或进行耗时操作
- 视图布局比较复杂,有大量的离屏渲染或设置
alpha
值 - 内存暴涨
- ……
tableView
滑动卡顿
- 问题分析1:
我们就逐步分析,首先看计算高度,因为使用
UITableView+FDTemplateLayoutCell
计算高度,应该不会存在重复计算问题,但是防止预防万一,还是在
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
代理方法中UITableView+FDTemplateLayoutCell
计算高度的方法打印log
日志,发现打印日志每次都会重indexPath.row=0
开始,也就是说每次加载更多,更新数据都会从 0 行重新计算,那这样计算肯定越来越多,那我们就想,难道和加载数据,更新数据有关吗?为什么每次都从0开始计算呢?经过多次查阅资料,发现[self.tableView reloadData]
这个方法刷新造成的。因为这个方法是重新加载数据,既然重新加载了所有数据,那么UITableView+FDTemplateLayoutCell
就不知道数据是否有变更,为了保证高度的准确,只能从0再全部计算一遍。
得出结论:加载更多时,不能使用
[self.tableView reloadData]
因为reloadData
会导致UITableView+FDTemplateLayoutCell
每次都重 0 行开始计算,意味着计算高度的时间越来越久,而且多次重复计算,严重浪费性能。从而引发卡顿。
解决方案:加载更多不使用
[self.tableView reloadData]
,使用另外一个[self.tableView insertRowsAtIndexPaths:indexpaths withRowAnimation:UITableViewRowAnimationNone]
[self.tableView insertRowsAtIndexPaths:indexpaths withRowAnimation:UITableViewRowAnimationNone];
其中
indexpaths
即新增的cell
的IndexPath
;
新增的cell
的IndexPath
统计方法NSMutableArray *indexPaths = @[].mutableCopy; for (int i = 0; i < models.count; i++) { NSIndexPath *indexPath = [NSIndexPath indexPathForRow:self.dataArray.count + i inSection:0]; [indexPaths addObject:indexPath]; }
[self.tableView insertRowsAtIndexPaths:indexpaths withRowAnimation:UITableViewRowAnimationNone]
这句话可能会有动画效果, 虽然用了 ·UITableViewRowAnimationNone· 但是还是可能有动画,如何不需要动画可以用下面的方法[UIView performWithoutAnimation:^{ [self.tableView insertRowsAtIndexPaths:indexpaths withRowAnimation:UITableViewRowAnimationNone]; }];
这样写可以没有动画,如果需要有动画,不是系统的动画,可以使用
[self.tableView beginUpdates]; /// 添加自己的动画 code [self.tableView insertRowsAtIndexPaths:indexpaths withRowAnimation:UITableViewRowAnimationNone]; /// 添加自己的动画 code [self.tableView endUpdates];
- 注意
[self.tableView beginUpdates]
,[self.tableView endUpdates]
成对出现,中间是动画代码。reloadData
方法不能使用该方法。
- 问题分析2:
首先首页布局比较简单常见,图片数量不多,并且加载图片是采用
SDWebImage
异步加载,不会出现加载大图片卡主线程问题,图片的切圆角和阴影也不多(有几个),离屏渲染也不多,也不会有性能问题。然后看一下内存。使用xcode
查看即可。
得出结论:通过查看内存,发现内存增加比较快,简直就是暴涨,断定内存。
解决方案:查找内存泄漏,解决内存泄漏。
网上查找内存泄漏的方法方案比较多,我们就选两个,第一个是xcode
系统工具Instruments
Instruments
功能巨大,可以查看耗时
,CPU
,内存分配
,内存泄漏
,僵尸对象
等等,有很多文章,这里就不一一列举了。
打开查看内存泄漏的工具
xcode
自带的工具Instruments
(Comd + i
快捷键),检查3步走,如图1、2、3所示
查看结果如图4所示,可以双击点击泄漏的方法进行查看代码,结果如图5。
不过个人感觉不是很好用(可能是不会用 - _ -!!! ),推荐个好用的第三方库 ,腾讯的 MLeaksFinder
, 可以直接 pod 'MLeaksFinder'
但是这个库因为很久没有维护了,里面使用了 UIAlertView
,导致每次有泄漏就崩溃, 因此我就自己修改了,自己上传了一下 pod
名字为 HJ_MLeaksFinder
(MLeaksFinder
的 copy
版本,不喜勿喷), 可以使用下面的进行 pod
添加 ,需要使用 1.0.2 版本。
pod 'HJ_MLeaksFinder'
这个库真的真的真的很好用,因为不许要做任何操作,除了单利需要添加- (BOOL)willDealloc
不需要释放之外,其他地方头文件都不需要导入,有内存泄漏就可以直接弹窗弹出来,还可以查询循环引用(MLeaksFinder
加上 pod ‘FBRetainCycleDetector’ 进行配合使用 而 HJ_MLeaksFinder
内部集成了 FBRetainCycleDetector
)。但是有些地方泄漏可能是强引用了,并没有循环,所以会检测不出来。
经过多次的内存泄漏提示弹窗,找到了内存泄漏的地方,只是强引用,是使用
cell.contentView
上的子视图View
后,再次布局cell.contentView
需要移除cell.contentView
上的子视图View
。而使用的移除方法是- (void)removeAllSubviews { while (self.subviews.count) { [self.subviews.lastObject removeFromSuperview]; } }
大多数会使用这个方法来移除所有子视图,但是有可能移除的不够完整,不够干净。当这个
view
有子视图有内容时,可能会放在了内存中,只是没有显示出来,如果查看内存,可以发现内存会有大量的增加,这个时候就需要移除所有的,包括子视图上的子视图。- (void)removeAllSubviews:(UIView *)supView { while (supView.subviews.count) { UIView *subView = supView.subviews.lastObject; if (subView.subviews.count > 0) { [self removeAllSubviews:subView]; } else { [subView removeFromSuperview]; subView = nil; } } }
类似这样的方法,递归移除。(这是个人想的,可能不是很友好,大家有好的想法可以留言,谢谢)
- 上面的问题都解决完之后,还有加载更多数据加载完成刷新页面是时会出现卡顿,或是闪动。
问题分析:
计算高度已经没有了问题,内存也没有了问题,现在为什么还会卡顿(闪动)呢,
网上搜索问题,大多数解决方法是self.tableView.estimatedRowHeight = 0; self.tableView.estimatedSectionHeaderHeight = 0; self.tableView.estimatedSectionFooterHeight = 0;
得出结论:滑动中,
tableView
的contentSize
突然变化会出现跳动,这个时候就需要设置estimatedRowHeight
,而且这个值越接近 真实高度越好。
解决方案:设置
estimatedRowHeight
,在tableView
的willDisplayCell
方法,进行设置,这个方法还包括预加载的操作判断- (void)tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath { CGFloat cellHeight = 0.0; if ([self.fd_indexPathHeightCache existsHeightAtIndexPath:indexPath]) { cellHeight = [self.fd_indexPathHeightCache heightForIndexPath:indexPath]; } else { if (cellHeight == 0) { if (indexPath.row > 1) { NSIndexPath *index = [NSIndexPath indexPathForRow:indexPath.row - 1 inSection:indexPath.section]; cellHeight = [tableView rectForRowAtIndexPath:index].size.height; } } } self.estimatedRowHeight = cellHeight; if (!self.isLoading) { // 防止多次联系加载更多 if (self.dataArray.count - number <= indexPath.row) { // number 还有多少条就可开始预加载 [self loadMore]; // 在加载更多 loadMore 中 isLoading 设置为 YES ,再加载完成之后再设置为NO } } }
在滑动过程中由于cell的高度变化导致出现的跳动,实现这个方法可以让计算高度方法不用一次性全部数据计算(一页的数据或全部数据),只有到了即将显示的
cell
,才计算cell
的高度
- 新的问题:设置了
tableView.estimatedRowHeight
缓慢滑动可能会出现tableView
突然的跳动
问题分析:
网上大多数解释说,设置了estimatedRowHeight
,使tableView
不知道具体的contentsize
,需要滚动完才会知道。
得出结论:因为设置了
estimatedRowHeight
不是 0 ,使tableView
不能完美的知道contentsize
,从而是tableView
滚动闪动或跳动。
解决方案:没有想到好的解决方案。就使用
scrollView
滚动代理方案设置,可是并不完美,
监听scrollView
滚动方法- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate { self.tableView.isSetEstimatedHeight = scrollView.isDecelerating; }
这个方法,
self.tableView.isSetEstimatedHeight
这个是自定义的是否设置预设高度开关,当快速滑动时scrollView.isDecelerating
是 1 (YES)缓慢滑动是 0 (NO),来解决缓慢滑动不需要estimatedRowHeight
和快速滑动需要estimatedRowHeight
的问题
【如果您有更好的解决方法,请留下解决方法,非常感谢。】
- 检查性能(耗时),针对性优化。一点小小学习心得和使用体验
使用
Instruments
(Comd + i
快捷键打开) 检查,
这个检查耗时比检查内存泄漏好用多了,也是比较简单的。因为可以直接定位到代码,方便快捷。
在性能和执行速度上有一个小的建议提供给小伙伴;
就是大的方法,没有拆分为小的函数方法性能好,所以不要写大的方法,尽量的拆分为小的方法块吧。
下面还有一个检查代码耗时的小方法,可以检查某一行代码或几行代码的耗时时间。
CFAbsoluteTime startTime = CFAbsoluteTimeGetCurrent(); /// 需要检查的耗时代码 CFAbsoluteTime endTime = (CFAbsoluteTimeGetCurrent() - startTime); NSLog(@"linkTime %f ms", endTime * 1000.0); // 打印耗时时间
好了,本次分享就到这里了,因为平时很少写文章,如果哪里写的不对,不好,就请大家多多指教,多多留言。非常感谢!!!。
^ 0_0 ^ -- Bright:祝大家开心快乐每一天。