UITableView性能优化-中级篇

老实说,UITableView性能优化 这个话题,最经常遇到的还是在面试中,常见的回答例如:

  • Cell复用机制
  • Cell高度预先计算
  • 缓存Cell高度
  • 圆角切割
  • 等等. . .
made in 小蠢驴的配图

进阶篇

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

  1. 要求 tableView 滚动的时候,滚动到哪行,哪行的图片才加载并显示,滚动过程中图片不加载显示;
  2. 页面跳转的时候,取消当前页面的图片加载请求;

以最常见的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 = imageUrlvalue = 下载的image图片
  • 读取的时候判断YYCache中是否有该url,有的话,直接读取缓存图片数据,没有的话,走图片下载逻辑,并缓存图片

问题所在:

如上设置,如果我们cell一行有20行,页面启动的时候,直接滑动到最底部,20个cell都进入过了界面,- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath 被调用了20次,不符合 需求1的要求

解决办法:

  1. cell每次被渲染时,判断当前tableView是否处于滚动状态,是的话,不加载图片;
  2. cell 滚动结束的时候,获取当前界面内可见的所有cell
  3. 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一次拖拽的代理方法执行流程:
ScrollView流程图.png

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


demo.gif
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]];
}
runloopDemo.gif

效果如上

  • 滚动的时候不加载图片,滚动结束加载图片-满足
  • 滚动结束,之前滚动过程中的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

思路:

  1. 创建一个可变字典,以indexPath:manager的格式,将当前的图片下载操作存起来
  2. 每次下载之前,将当前下载线程存入,下载成功后,将该线程移除
  3. viewWillDisappear的时候,取出当前线程字典中的所有线程对象,遍历进行cancel操作,完成需求


话外篇:面试题赠送

最近网上各种互联网公司裁员信息铺天盖地,甚至包括各种一线公司 ( X东 X乎 都扛不住了吗-。-)iOS本来就是提前进入寒冬,iOS小白们可以尝试思考下这个问题

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

答:

  1. 让服务器直接传圆角图片;
  2. 贝塞尔切割控件layer;
  3. 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小伙伴门能熬过这个冬天?


Demo源码


参考资料
iOS 保持界面流畅的技巧
VVeboTableViewDemo
YYKitDemo
UIScrollView 实践经验

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

推荐阅读更多精彩内容

  • Swift1> Swift和OC的区别1.1> Swift没有地址/指针的概念1.2> 泛型1.3> 类型严谨 对...
    cosWriter阅读 11,089评论 1 32
  • 五年级英语作业本上的作业 1、英语书的第7页的第九部分抄写两次。 2、英语书第三页的1b部分抄写两次。 3、英语书...
    我心我愿秀阅读 903评论 0 1
  • 邪生 秋智是纯朴农民的儿子,他的家中有五个孩子,俩个姐姐,俩个哥哥,唯独他是最小的那一个,秋智的爸爸看到自己的一双...
    Mr海鲜君的故事阅读 496评论 1 3
  • 一个人如果身体生病了我们会说他身体不健康,那么就需要去医院打针吃药,尽快治愈病症。可如果一个人感觉不到幸福呢?是不...
    耕耘生活阅读 379评论 4 10
  • 我们的生命不是一个人的! 早上突然接到亲人去世的噩耗,心理好是难受一阵。 离去的是我的姐夫,年仅四十出头,家里还有...
    飞跃银河阅读 486评论 3 0