UITableViewCell重用导致的图片错乱问题

之前做过一个视频信息列表展示的模块,cell很简单就是左边图片,右边文字信息。当时用的SDWebImage加载图片并没有看到图片错乱的情况。但是,如果是自己写的图片下载器,不注意处理是会导致图片错乱的。

今天写了个Demo,验证及解决这个问题。
实验环境:cell依然是左边图片,右边文字信息。图片两张,一张大图片A(风景),一张小图片B(人物),采用自己实现的原始图片下载器异步下载,block里回调设置cell的图片。要求偶数行的图片是风景,奇数行的图片是人物。
整个界面期望如下:

期望tableview界面.jpg

但实际可能出现bug,如下图:


bug界面.jpg

数据错乱原因分析

cell上的数据错乱显然是由于cell的重用导致的。由于图片是异步下载的,下载完成才给cell设置,但是在这个过程中用户可能会上下滑动,滑动的时候会导致cell的重用,比如第0行是设置大图片的,第11行是设置小图片的,用户在滑动的过程中,因为cell的重用第11行的cell可能使用的是第0行的cell,这时第0行的block回调设置的cell和第11行的block回调设置的cell是同一个,即cell的重用导致两个block回调时设置的其实是同一个cell上的imageView。这就是问题的关键。

因为图片是异步下载的,你也不知道哪个block会先回调,如果小图片的block先回调那么这个cell的图片就先被设置为小图片,如果后来大图片的block回来了,那么你会看到图片被替换成大图片,这种情况还算比较好,但如果大图片下载失败或者小图片的block最后回调,那么你看到的将是小图片加大图片的文字信息,这时数据就错乱了。

如何解决

如果不重用cell,当然是可以解决该问题的,但是内存肯定会浪费不少。

解决的方案有两种:

方案一:在下载完成的回调里进行区分,如果不一致则不设置imageView。

方案二:每次下载前都先取消掉上一次的下载,这样就不会同时有两个block回调,这是很多第三方图片加载库的做法。

如果采用方案一,那么有两种办法进行区分:

1. 通过indexPath来区分

block里截获的indexPath对象是cell在下载前的indexPath,假设为t1时刻的indexPath。而通过[tableView indexPathForCell:cell]; 则可以获得cell当前的indexPath,假设为t2时刻的indexPath。如果t1-t2这段时间内cell发生了重用的话,那么这两个indexPath将不一致。

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    MyTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"Cell" forIndexPath:indexPath];
    cell.imgView.image = [UIImage imageNamed:@"pl"];
    NSString *urlStr = self.urlArr[indexPath.row];
    __weak typeof(MyTableViewCell *) weakCell = cell;
    [[XQImageDownloader defaultImageDownloader] downloadImageWithUrlString:urlStr completion:^(NSString *imgUrl, UIImage *image, NSError *error) {
        if (!error) {
            NSIndexPath *currentIndexPath = [tableView indexPathForCell:weakCell];
            if (currentIndexPath == nil) { //表明cell没在屏幕上显示。
              return;
            }
            NSInteger currentRow = currentIndexPath.row;
            NSInteger originalRow = indexPath.row; //这里的indexPath是block截获的.
            if (originalRow != currentRow) {
                NSLog(@"数据错乱,应该设置的是第%ld个cell上的ImageView,但当前设置的是%ld个cell上的ImageView,cell:%p", (long)originalRow, currentRow, weakCell);
                NSLog(@"urlStr:%@, imgUrl:%@", urlStr, imgUrl); //这里的urlStr==imgUrl,想一想为什么
                return ;
            }
            weakCell.imgView.image = image;
        } else {
            NSLog(@"下载图片:%@,error:%@", urlStr, [error localizedDescription]);
        }
    }];
    cell.contentLabel.text = [NSString stringWithFormat:@"这是第%ld个cell:%p", indexPath.row, cell];
    return cell;
}

这种办法的缺点是如果cell的配置方法是在别处,那么需要传递tableView和indexPath两个参数,对现有代码改动较大,不是很方便。

注意:上述代码中和currentIndexPath比较的indexPath,必须是block截获的,不能直接使用 cell.indexPath (这里假设cell里有一个indexPath属性并在dequeue后就赋值为代理方法的indexPath参数)否则总是相等的。

2. 通过下载的图片URL来区分

下载前先记录当前imageView应该显示的图片URL,当下载完成时再进行比较。这种办法比第一种要方便很多,我们可以写一个UIImageView的类别封装一下。

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    MyTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"Cell" forIndexPath:indexPath];
    cell.imgView.image = [UIImage imageNamed:@"pl"];
    NSString *urlStr = self.urlArr[indexPath.row];
    cell.imageView.xq_imgUrl = [NSURL URLWithString:urlStr]; //记录当前imageView应该显示的图片URL
    __weak typeof(MyTableViewCell *) weakCell = cell;
    [[XQImageDownloader defaultImageDownloader] downloadImageWithUrlString:urlStr completion:^(NSString *imgUrl, UIImage *image, NSError *error) {
        if (!error) {
            if (![weakCell.imageView.xq_imgUrl.absoluteString isEqualToString:imgUrl]) {
                NSLog(@"数据错乱,应该下载的图片url为:%@, 但当前下载的图片url为:%@", weakCell.imageView.xq_imgUrl, imgUrl);
                return ;
            }
            weakCell.imgView.image = image;
        } else {
                      NSLog(@"下载图片:%@,error:%@", urlStr, [error localizedDescription]);
        }
    }];
    cell.contentLabel.text = [NSString stringWithFormat:@"这是第%ld个cell:%p", indexPath.row, cell];
    return cell;
}

另外在下载图片之前先把cell的imageView的image置为nil。cell.imgView.image = nil;可以防止重用的cell万一图片下载失败而导致显示了以前的图片,不过一般都会有占位图片所以这一步可有可无。

如果采用方案二:每次下载前都先取消掉上一次的下载。那么你的图片下载器就需要实现取消下载功能,幸运的是SD或YY这样的图片加载器已经实现了这样的功能。

比如SD:

- (void)sd_setImageWithURL:(NSURL *)url placeholderImage:(UIImage *)placeholder options:(SDWebImageOptions)options progress:(SDWebImageDownloaderProgressBlock)progressBlock completed:(SDWebImageCompletionBlock)completedBlock {
    [self sd_cancelCurrentImageLoad]; //下载前先取消掉当前ImageView上之前的下载
    objc_setAssociatedObject(self, &imageURLKey, url, OBJC_ASSOCIATION_RETAIN_NONATOMIC);

    if (!(options & SDWebImageDelayPlaceholder)) {
        dispatch_main_async_safe(^{
            self.image = placeholder;
        });
    }
    ...
}

YY:

- (void)yy_setImageWithURL:(NSURL *)imageURL
               placeholder:(UIImage *)placeholder
                   options:(YYWebImageOptions)options
                   manager:(YYWebImageManager *)manager
                  progress:(YYWebImageProgressBlock)progress
                 transform:(YYWebImageTransformBlock)transform
                completion:(YYWebImageCompletionBlock)completion {
    if ([imageURL isKindOfClass:[NSString class]]) imageURL = [NSURL URLWithString:(id)imageURL];
    manager = manager ? manager : [YYWebImageManager sharedManager];
    
    _YYWebImageSetter *setter = objc_getAssociatedObject(self, &_YYWebImageSetterKey);
    if (!setter) {
        setter = [_YYWebImageSetter new];
        objc_setAssociatedObject(self, &_YYWebImageSetterKey, setter, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    }
    int32_t sentinel = [setter cancelWithNewURL:imageURL]; //下载前先取消掉当前ImageView上之前的下载
    ...
}

所以直接使用这些第三方库就可以了。

注意:如果你使用这些第三方库还出现图片错乱的问题,根本原因是因为重用的cell的imageView没有执行cancel下载操作,至于为啥会没有执行,大概率是因为代码中出现了有if没else的逻辑或者if里用了SD而else里没使用,千万不要写这样的代码,千万不要啊。

总结

cell上发生数据错乱的控件,大部分都是因为要显示的资源需要异步处理比如图片需要下载后才能设置到imageView上。少部分同步显示资源的控件(比如UILabel)发生数据错乱则一般是因为你的条件判断有问题导致重用的cell上还留有旧的数据,可以重写-prepareForReuse在重用前先清除掉旧数据。

俺在开发过程中遇到的一些数据错乱的场景:

cell上的imageView根据条件判断一会加载本地的图片,一会加载网络图片,这个就很典型。

cell上的imageView根据条件判断一会用SDWebImage加载,一会用YYWebImage加载。同一个cell里的imageView千万不要使用两种框架去加载。

以上,MADE BY XQ。

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

推荐阅读更多精彩内容

  • 要解决cell重用错乱的问题首先要了解重用的机制是什么,重用简单明了的来讲就是: 注册了cell后,使用了重用机制...
    健贱阅读 1,874评论 5 7
  • 2017.02.22 可以练习,每当这个时候,脑袋就犯困,我这脑袋真是神奇呀,一说让你做事情,你就犯困,你可不要太...
    Carden阅读 1,306评论 0 1
  • 发现 关注 消息 iOS 第三方库、插件、知名博客总结 作者大灰狼的小绵羊哥哥关注 2017.06.26 09:4...
    肇东周阅读 11,947评论 4 60
  • 他手上握着可怜的安静 在杏树下,等一朵花开 头顶白云,嘶嘶地生长 人浮了一下午,屋顶睡那么多瓦片 他收留了一个故事...
    我是不是蝎大人阅读 184评论 0 0
  • 协方差的定义 在统计学上,协方差用来刻画两个随机变量之间的相关性,反映的是变量之间的二阶统计特性,两个随机变量Xi...
    marine0131阅读 4,121评论 0 0