UITableView性能优化-中级篇

简介: 老实说,UITableView性能优化 这个话题,最经常遇到的还是在面试中,常见的回答例如:Cell复用机制Cell高度预先计算缓存Cell高度圆角切割等等. . .

进阶篇
最近遇到一个需求,对tableView有中级优化需求

要求 tableView 滚动的时候,滚动到哪行,哪行的图片才加载并显示,滚动过程中图片不加载显示;
页面跳转的时候,取消当前页面的图片加载请求;
本人5年iOS开发经验,曾就职于阿里巴巴。 善于把艰涩的iOS知识转化为通俗易懂的白话文字,同时也欢迎大家加入小编的iOS交流群 642363427,群里会提供相关面试资料,书籍欢迎大家入驻!

以最常见的cell加载webImage为例:

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{

    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"cell"];
    if (!cell) {
        cell = [[UITableViewCell alloc]initWithStyle:UITableViewCellStyleDefault reuseIdentifier:@"cell"];
    }

    DemoModel *model = self.datas[indexPath.row];
    cell.textLabel.text = model.text;

    [cell.imageView setYy_imageURL:[NSURL URLWithString:model.user.avatar_large]];

    return cell;
}

解释下cell的复用机制:
如果cell没进入到界面中(还不可见),不会调用- (UITableViewCell )tableView:(UITableView )tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath去渲染cell,在cell中如果设置loadImage,不会调用;
而当cell进去界面中的时候,再进行cell渲染(无论是init还是从复用池中取)
解释下YYWebImage机制:
内部的YYCache会对图片进行数据缓存,以key:value的形式,这里的key = imageUrl,value = 下载的image图片
读取的时候判断YYCache中是否有该url,有的话,直接读取缓存图片数据,没有的话,走图片下载逻辑,并缓存图片
问题所在:
如上设置,如果我们cell一行有20行,页面启动的时候,直接滑动到最底部,20个cell都进入过了界面,- (UITableViewCell )tableView:(UITableView )tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath 被调用了20次,不符合 需求1的要求

解决办法:

cell每次被渲染时,判断当前tableView是否处于滚动状态,是的话,不加载图片;
cell 滚动结束的时候,获取当前界面内可见的所有cell
在2的基础之上,让所有的cell请求图片数据,并显示出来
步骤1:

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{

    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"cell"];
    if (!cell) {
        cell = [[UITableViewCell alloc]initWithStyle:UITableViewCellStyleDefault reuseIdentifier:@"cell"];
    }

    DemoModel *model = self.datas[indexPath.row];
    cell.textLabel.text = model.text;

    //不在直接让cell.imageView loadYYWebImage
    if (model.iconImage) {
        cell.imageView.image = model.iconImage;
    }else{
        cell.imageView.image = [UIImage imageNamed:@"placeholder"];

        //核心判断:tableView非滚动状态下,才进行图片下载并渲染
        if (!tableView.dragging && !tableView.decelerating) {
            //下载图片数据 - 并缓存
            [ImageDownload loadImageWithModel:model success:^{

                //主线程刷新UI
                dispatch_async(dispatch_get_main_queue(), ^{
                    cell.imageView.image = model.iconImage;
                });
            }];
        }
}

步骤2:

- (void)p_loadImage{

    //拿到界面内-所有的cell的indexpath
    NSArray *visableCellIndexPaths = self.tableView.indexPathsForVisibleRows;

    for (NSIndexPath *indexPath in visableCellIndexPaths) {

        DemoModel *model = self.datas[indexPath.row];

        if (model.iconImage) {
            continue;
        }

        UITableViewCell *cell = [self.tableView cellForRowAtIndexPath:indexPath];

        [ImageDownload loadImageWithModel:model success:^{
            //主线程刷新UI
            dispatch_async(dispatch_get_main_queue(), ^{

                cell.imageView.image = model.iconImage;
            });
        }];
    }
}

步骤3:

//手一直在拖拽控件
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView{

    [self p_loadImage];
}

//手放开了-使用惯性-产生的动画效果
- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate{

    if(!decelerate){
        //直接停止-无动画
        [self p_loadImage];
    }else{
        //有惯性的-会走`scrollViewDidEndDecelerating`方法,这里不用设置
    }
}

dragging:returns YES if user has started scrolling. this may require some time and or distance to move to initiate dragging
可以理解为,用户在拖拽当前视图滚动(手一直拉着)

deceleratingreturns:returns YES if user isn't dragging (touch up) but scroll view is still moving
可以理解为用户手已放开,试图是否还在滚动(是否惯性效果)

ScrollView一次拖拽的代理方法执行流程:

当前代码生效的效果如下:

RunLoop小操作

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{

    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"cell"];
    if (!cell) {
        cell = [[UITableViewCell alloc]initWithStyle:UITableViewCellStyleDefault reuseIdentifier:@"cell"];
    }

    DemoModel *model = self.datas[indexPath.row];
    cell.textLabel.text = model.text;

    if (model.iconImage) {
        cell.imageView.image = model.iconImage;
    }else{
        cell.imageView.image = [UIImage imageNamed:@"placeholder"];

        /**
         runloop - 滚动时候 - trackingMode,
         - 默认情况 - defaultRunLoopMode
         ==> 滚动的时候,进入`trackingMode`,defaultMode下的任务会暂停
         停止滚动的时候 - 进入`defaultMode` - 继续执行`trackingMode`下的任务 - 例如这里的loadImage
         */
        [self performSelector:@selector(p_loadImgeWithIndexPath:)
                   withObject:indexPath
                   afterDelay:0.0
                      inModes:@[NSDefaultRunLoopMode]];
}

//下载图片,并渲染到cell上显示
- (void)p_loadImgeWithIndexPath:(NSIndexPath *)indexPath{

    DemoModel *model = self.datas[indexPath.row];
    UITableViewCell *cell = [self.tableView cellForRowAtIndexPath:indexPath];

    [ImageDownload loadImageWithModel:model success:^{
        //主线程刷新UI
        dispatch_async(dispatch_get_main_queue(), ^{
            cell.imageView.image = model.iconImage;
        });
    }];
}

效果与demo.gif的效果一致

runloop - 两种常用模式介绍: trackingMode && defaultRunLoopMode

默认情况 - defaultRunLoopMode
滚动时候 - trackingMode
滚动的时候,进入trackingMode,导致defaultMode下的任务会被暂停,停止滚动的时候 ==> 进入defaultMode - 继续执行defaultMode下的任务 - 例如这里的defaultMode
大tips:这里,如果使用RunLoop,滚动的时候虽然不执行defaultMode,但是滚动一结束,之前cell中的p_loadImgeWithIndexPath就会全部再被调用,导致类似YYWebImage的效果,其实也是不满足需求,

提示会被调用的代码如下:

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{

    //p_loadImgeWithIndexPath一进入`NSDefaultRunLoopMode`就会执行
    [self performSelector:@selector(p_loadImgeWithIndexPath:)
               withObject:indexPath
               afterDelay:0.0
                  inModes:@[NSDefaultRunLoopMode]];
}

效果如上

滚动的时候不加载图片,滚动结束加载图片-满足
滚动结束,之前滚动过程中的cell会加载图片 => 不满足需求
版本回滚到Runloop之前 - git reset --hard runloop之前
解决: 需求2. 页面跳转的时候,取消当前页面的图片加载请求;

- (void)p_loadImgeWithIndexPath:(NSIndexPath *)indexPath{

    DemoModel *model = self.datas[indexPath.row];

    //保存当前正在下载的操作
    ImageDownload *manager = self.imageLoadDic[indexPath];
    if (!manager) {

        manager = [ImageDownload new];
        //开始加载-保存到当前下载操作字典中
        [self.imageLoadDic setObject:manager forKey:indexPath];
    }

    [manager loadImageWithModel:model success:^{
        //主线程刷新UI
        dispatch_async(dispatch_get_main_queue(), ^{
            UITableViewCell *cell = [self.tableView cellForRowAtIndexPath:indexPath];
            cell.imageView.image = model.iconImage;
        });

        //加载成功-从保存的当前下载操作字典中移除
        [self.imageLoadDic removeObjectForKey:indexPath];
    }];
}

- (void)viewWillDisappear:(BOOL)animated{
    [super viewWillDisappear:animated];

    NSArray *loadImageManagers = [self.imageLoadDic allValues];
    //当前图片下载操作全部取消
    [loadImageManagers makeObjectsPerformSelector:@selector(cancelLoadImage)];
}

@implementation ImageDownload
- (void)cancelLoadImage{
    [_task cancel];
}
@end

思路:

创建一个可变字典,以indexPath:manager的格式,将当前的图片下载操作存起来
每次下载之前,将当前下载线程存入,下载成功后,将该线程移除
在viewWillDisappear的时候,取出当前线程字典中的所有线程对象,遍历进行cancel操作,完成需求
话外篇:面试题赠送
最近网上各种互联网公司裁员信息铺天盖地,甚至包括各种一线公司 ( X东 X乎 都扛不住了吗-。-)iOS本来就是提前进入寒冬,iOS小白们可以尝试思考下这个问题

问:UITableView的圆角性能优化如何实现
答:

让服务器直接传圆角图片;
贝塞尔切割控件layer;
YYWebImage为例,可以先下载图片,再对图片进行圆角处理,再设置到cell上显示
问:YYWebImage 如何设置圆角? 在下载完成的回调中?如果你在下载完成的时候再切割,此时 YYWebImage 缓存中的图片是初始图片,还是圆角图片?(终于等到3了!!)
答: 如果是下载完,在回调中进行切割圆角的处理,其实缓存的图片是原图,等于每次取的时候,缓存中取出来的都是矩形图片,每次set都得做切割操作;

问: 那是否有解决办法?
答:其实是有的,简单来说YYWebImage 可以拆分成两部分,默认情况下,我们拿到的回调,是走了 download && cache的流程了,这里我们多做一步,取出cache中该url路径对应的图片,进行圆角切割,再存储到 cache中,就能保证以后每次拿到的就都是cacha中已经裁切好的圆角图片

详情可见:

NSString *path = [[UIApplication sharedApplication].cachesPath stringByAppendingPathComponent:@"weibo.avatar"];
YYImageCache *cache = [[YYImageCache alloc] initWithPath:path];
manager = [[YYWebImageManager alloc] initWithCache:cache queue:[YYWebImageManager sharedManager].queue];
manager.sharedTransformBlock = ^(UIImage *image, NSURL *url) {
    if (!image) return image;
    return [image imageByRoundCornerRadius:100]; // a large value
};

SDWebImage同理,它有暴露了一个方法出来,可以直接设置保存图片到磁盘中,无需修改源码

“winner is coming”,如果面试正好遇到以上问题的,请叫我雷锋~
衷心希望各位iOS小伙伴门能熬过这个冬天?

目录
相关文章
|
16天前
|
设计模式 缓存 架构师
架构师必备10大接口性能优化秘技
【11月更文挑战第25天】在软件开发中,接口性能优化是架构师必须掌握的关键技能之一。一个高效的接口不仅能够提升用户体验,还能减少服务器资源消耗,提高系统稳定性。本文将介绍10大接口性能优化秘技,并通过Java示例代码展示这些技巧在实际业务场景中的应用。
31 3
|
21天前
|
缓存 监控 前端开发
前端开发中的性能优化:策略与实践
前端开发中的性能优化:策略与实践
|
4月前
|
开发者 数据库 虚拟化
Xamarin 应用性能优化策略大揭秘,从代码到界面再到数据访问,全面提升应用性能,快来围观!
【8月更文挑战第31天】在 Xamarin 跨平台移动应用开发中,性能优化至关重要。代码优化是基础,应避免不必要的计算与内存分配,如减少循环中的对象创建及合理使用数据结构。界面设计上需注意简化布局、减少特效并启用虚拟化以提升响应速度。数据访问方面,优化数据库查询和网络请求可显著改善性能。Xamarin Profiler 等工具还可帮助开发者实时监控并优化应用表现,从而打造流畅高效的用户体验。
56 0
|
4月前
|
存储 缓存 JavaScript
提升Blazor应用性能的探索之旅:深入解析关键技巧与最佳实践
【8月更文挑战第31天】在开发现代Web应用时,性能与用户体验至关重要。Blazor作为一款使用.NET构建交互式Web UI的框架,提供了诸多便利。为了充分发挥其潜力并优化体验,掌握一些性能提升技巧十分必要。本文将分享几个实用的Blazor性能优化方法,包括减少不必要的服务器端调用、使用懒加载以及优化DOM操作。通过这些技巧,可以显著提升应用性能,为用户提供更流畅的体验。以下是具体方法及示例代码。
63 0
|
7月前
|
缓存 监控 NoSQL
一次性能优化实践
【5月更文挑战第21天】为解决在线教育平台在高并发下数据库查询响应时间增加的问题,开发者采用Redis缓存策略。通过数据分层、LRU淘汰策略、异步更新及监控调优,成功提升性能,缓存命中率超90%,页面加载时间从3秒降至1秒,改善了用户体验。此实践强调了合理缓存策略、监控调优以及考虑数据访问模式在系统设计中的重要性。
75 2
|
7月前
|
缓存 移动开发 Android开发
Android应用性能优化实践
【5月更文挑战第20天】 在移动开发领域,应用的性能直接关乎用户体验。特别是对于Android平台,由于设备多样性和应用生态环境的复杂性,性能优化成为了开发者必须面对的挑战。本文将深入探讨Android应用性能优化的多个方面,包括内存管理、UI渲染、多线程处理以及电池效率等,旨在为开发者提供一系列实用的优化策略和技巧。
|
消息中间件 缓存 弹性计算
|
缓存 API 数据库
UITableView性能优化分析总结
UITableView是iOS中使用最频繁的控件之一,其性能优化是我们经常要面对的,尤其是当数据量偏大并且设备性能不足时。
300 0
|
缓存 JSON 前端开发
支付宝前端的代码结构及性能优化大总结
支付宝前端的代码结构及性能优化大总结
266 0
支付宝前端的代码结构及性能优化大总结
|
前端开发 索引
前端css性能优化
1. 避免使用@important 外部的css文件中使用@important会使得页面在加载时增加额外的延迟。最好使用link   2. 避免使用css表达式(表达式可能会造成极大的计算量)   3. 避免通配选择器 在初期使用*{margin:0;padding:0},以此来消除标签的默认布局和不同浏览器的对同一个标签的不同的渲染。
967 0