Mar 18

一个TableView(并没有)循环刷新的现象与正确的做法

Lrdcq , 2019/03/18 01:07 , 程序 , 閱讀(2361) , Via 本站原創
在一次CodeReview中,我们发现在一个普通的列表加载高度不定的图片的时候,有如下写法:

- 初始化高度固定为默认值。reloadData。
- 在cellForRow中通过sd加载图片。
- 在图片加载成功后,获取图片高度并塞到本地缓存。
- 再刷新当前的cell。


代码看起来如下:

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    VCCell *cell = [tableView dequeueReusableCellWithIdentifier:@"VCCell"];
    [cell.img sd_setImageWithURL:[NSURL URLWithString:@"https://p0.meituan.net/klcmspic/616bb9a1d1b17226a75682cf155718c293474.png"] completed:^(UIImage * _Nullable image, NSError * _Nullable error, SDImageCacheType cacheType, NSURL * _Nullable imageURL) {
        if (image) {
            self.imgHeight = image.size.height;//记录高度缓存
            [tableView reloadRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationFade];//刷新当前cell
        }
    }];
    return cell;
}

这个写法咋看起来挺自然流畅的,但是仔细想想就发现不对:reloadRowsAtIndexPaths会让当前cell重新执行cellForRow,难道这个代码不会导致无限循环刷新么?不过事实上确实没有。

Log分析

要分析是否真的循环了,我们只需要插桩一些log就好:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    VCCell *cell = [tableView dequeueReusableCellWithIdentifier:@"VCCell"];
    
    NSInteger flag = arc4random() % 100;//生成当前cellForRow的标记
    NSLog(@"Call %ld cellForRowAtIndexPath", flag);//执行cellForRow log
    
    [cell.img sd_setImageWithURL:[NSURL URLWithString:@"https://p0.meituan.net/xxxx/616bb9a1d1b17226a75682cf155718c293474.png"] completed:^(UIImage * _Nullable image, NSError * _Nullable error, SDImageCacheType cacheType, NSURL * _Nullable imageURL) {
        if (image) {
            self.imgHeight = image.size.height;
            
            NSLog(@"Call %ld reloadRow", flag);//开始reloadRows log
            
            [tableView reloadRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationFade];
            
            NSLog(@"Call %ld reloadRow end", flag);//结束reloadRows log
        }
    }];
    return cell;
}

执行的结果如下:
play[16969:1820508] Call 36 cellForRowAtIndexPath
play[16969:1820508] Call 36 reloadRow
play[16969:1820508] Call 13 cellForRowAtIndexPath
play[16969:1820508] Call 13 reloadRow
play[16969:1820508] Call 13 reloadRow end
play[16969:1820508] Call 36 reloadRow end
//结束

咋看起来和我们预期的似乎是一样的,又没那么一样。我们可以得到的信息包括:

- cellForRowAtIndexPath确实执行了第二次,但是第二次却没有触发第三次的执行
- 第二次cellForRowAtIndexPath与后续操作是在第一次reloadRow操作过程中同步完成的,即第二条log到第六条log是同步代码

似乎明白了:

- reloadRowsAtIndexPaths方法是同步执行的(包括使用[tableView beginUpdates]/[tableView endUpdates]包着的情况也是同步执行的)。因此36 reloadRow直接调用到13 cellForRowAtIndexPath了。

- 第二次sd_setImageWithURL一定是同步执行的:想想也就明白了,这里是第二次触发同一个地址的sd_setImageWithURL了,图像一定已经在本地有缓存了。在有缓存的时候,sd确实是同步返回图片并且执行callback。

- 似乎,因为reloadRowsAtIndexPaths这个方法是同步执行的缘故,reloadRowsAtIndexPaths的过程中,再次调用reloadRowsAtIndexPaths是无效的,这导致了没有触发第三次循环。

为了验证这个结果,我们依次尝试了三个方法:reloadRowsAtIndexPaths,reloadSections,reloadData。结果上看:

- reloadRowsAtIndexPaths和reloadSections会立刻同步执行,并且这个case下确实不会触发第三次刷新。

- reloadData会在下一次tick执行,因此如果使用reloadData的话,这里就无限循环了。(巧妙的是,因为延迟到下一次tick才执行,因此还没有卡死。。。)

- 同时,也确定了[tableView beginUpdates]/[tableView endUpdates]对以上结果并没有影响。

因此,结论上,这个写法之所以能正常执行,是sd缓存图片同步返回,和reloadRowsAtIndexPaths方法会同步执行,执行时重复触发无效,这几个特性,瞎猫碰见死耗子凑成了

正确的写法

虽然瞎猫碰见死耗子凑成了,但是事实上可以看见cellForRowAtIndexPath执行了两次,符合预期,sd_setImageWithURL执行了两次就没有必要了。

因此,从开发的角度,应该需要在加载图片之前和执行刷新之前,多次校验和场景判断,从而避免有没有必要的代码重复执行,最大化提升代码执行效率:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    VCCell *cell = [tableView dequeueReusableCellWithIdentifier:@"VCCell"];
    if (cell.img.sd_imageURL != self.dataImgUrl) {//imageview校验,如果cell是复用的,接下来就没了
        [cell.img sd_setImageWithURL:self.dataImgUrl completed:^(UIImage * _Nullable image, NSError * _Nullable error, SDImageCacheType cacheType, NSURL * _Nullable imageURL) {
            if (image) {
                if (self.imgHeight < 0.0) {//高度缓存储存校验,一定会挡在这里来避免重复刷新
                    self.imgHeight = image.size.height;
                    [tableView reloadRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationNone];
                }
            }
        }];
    }
    return cell;
}

而从正确的设计上考虑。我们刷新cell的目的是因为加载了图片后cell的高度发生改变。基于数据驱动原则:

1. sd_setImageWithURL:self回调中仅触发高度缓存刷新。
2. 监听高度缓存数组,对高度发生变化的cell执行reload。
关键词:oc
logo