UITableView 列表滑动优化心得

这篇文章是一个UITableView的优化过程,即优化过程工具的使用。环境是Xcode11.6版本。

基本需求:

1、可以根据 JSON 文件配置设置 cell 的显示类型和 cell 的一些基本样式的修改,这样需求下对 cell实现,没有想到好的方法,采用最笨的方法,就是 cell.contentView 上添加的 view的方式。而view根据 cellModeltemplateNamedatastyle传到 factory(工厂) 的形式生成。 cell.contentView 添加之前需要先上移除所有子视图,再重新创建 View 添加到 cell.contentView 上。
下面是view根据 cellModeltemplateNamedatastyle传到 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即新增的 cellIndexPath;
新增的 cellIndexPath统计方法

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 自带的工具 InstrumentsComd + i 快捷键),检查3步走,如图1、2、3所示

图1

图2

图3

查看结果如图4所示,可以双击点击泄漏的方法进行查看代码,结果如图5。
图4

图5

不过个人感觉不是很好用(可能是不会用 - _ -!!! ),推荐个好用的第三方库 ,腾讯的 MLeaksFinder , 可以直接 pod 'MLeaksFinder' 但是这个库因为很久没有维护了,里面使用了 UIAlertView ,导致每次有泄漏就崩溃, 因此我就自己修改了,自己上传了一下 pod 名字为 HJ_MLeaksFinder(MLeaksFindercopy 版本,不喜勿喷), 可以使用下面的进行 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;

得出结论:滑动中,tableViewcontentSize 突然变化会出现跳动,这个时候就需要设置 estimatedRowHeight,而且这个值越接近 真实高度越好。

解决方案:设置 estimatedRowHeight,在tableViewwillDisplayCell方法,进行设置,这个方法还包括预加载的操作判断

- (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的问题
【如果您有更好的解决方法,请留下解决方法,非常感谢。】

  • 检查性能(耗时),针对性优化。一点小小学习心得和使用体验

使用 InstrumentsComd + i快捷键打开) 检查,

图6

图7

图8

这个检查耗时比检查内存泄漏好用多了,也是比较简单的。因为可以直接定位到代码,方便快捷。

  • 在性能和执行速度上有一个小的建议提供给小伙伴;就是大的方法,没有拆分为小的函数方法性能好,所以不要写大的方法,尽量的拆分为小的方法块吧。

  • 下面还有一个检查代码耗时的小方法,可以检查某一行代码或几行代码的耗时时间。

CFAbsoluteTime startTime = CFAbsoluteTimeGetCurrent();
/// 需要检查的耗时代码
CFAbsoluteTime endTime = (CFAbsoluteTimeGetCurrent() - startTime);
NSLog(@"linkTime %f ms", endTime * 1000.0);   // 打印耗时时间

好了,本次分享就到这里了,因为平时很少写文章,如果哪里写的不对,不好,就请大家多多指教,多多留言。非常感谢!!!。

^ 0_0 ^ -- Bright:祝大家开心快乐每一天。

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

推荐阅读更多精彩内容

  • 一、简介 <<UITableView(或简单地说,表视图)的一个实例是用于显示和编辑分层列出的信息的一种手段 <<...
    无邪8阅读 10,427评论 3 3
  • 一、初始化方法 1:TableView风格设置。 - (instancetype)initWithFrame:(C...
    iOS_SXH阅读 1,815评论 1 10
  • 背景 UITableView作为iOS开发中最重要和常用的控件之一,其中的实现原理值得深入研究一下。苹果软件在这块...
    疯狂的Cracker阅读 673评论 2 4
  • 这篇文章是我和我们团队最近对 UITableViewCell 利用 AutoLayout 自动高度计算和 UITa...
    kuangkai阅读 1,568评论 0 6
  • UITableView的简单认识 UITableView最核心的思想就是UITableViewCell的重用机制。...
    reallychao阅读 1,032评论 0 7