在重用UITableViewCell时使用SDWebImage处理图像下载

时间:2014-05-22 22:23:28

标签: ios ipad uiimageview uiimage sdwebimage

我使用第三方库SDWebImage为我的UITableView单元下载图像,在单元格中创建UIImageView并在配置单元格时触发请求。

[imageView setImageWithURL:[NSURL URLWithString:imageUrl] placeholderImage:[UIImage imageNamed:@"default.jpg"] completed:^(UIImage *image, NSError *error, SDImageCacheType cacheType) {
    }];

它的工作正常,但是当我快速滚动时,大多数图像都没有完全下载(我可以在查尔斯看到),因为图像没有被缓存。即使我的单元格被重用,如何缓存已发送的请求,这样相同的请求也不会多次进行。

请忽略任何拼写错误:)

3 个答案:

答案 0 :(得分:10)

有效的iOS 10,不再需要原始答案的手动预取代码。只需设置prefetchDataSource即可。例如,在Swift 3中:

override func viewDidLoad() {
    super.viewDidLoad()

    tableView.prefetchDataSource = self
}

然后让prefetchRowsAtIndexPaths使用SDWebImagePrefetcher来获取行

extension ViewController: UITableViewDataSourcePrefetching {
    public func tableView(_ tableView: UITableView, prefetchRowsAt indexPaths: [IndexPath]) {
        let urls = indexPaths.map { baseURL.appendingPathComponent(images[$0.row]) }
        SDWebImagePrefetcher.shared().prefetchURLs(urls)
    }
}

您可以使用标准cellForRowAt

override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath)
    let url = baseURL.appendingPathComponent(images[indexPath.row])
    cell.imageView?.sd_setImage(with: url, placeholderImage: placeholder)
    return cell
}

就个人而言,我更喜欢AlamofireImage。所以UITableViewDataSourcePrefetching略有不同

extension ViewController: UITableViewDataSourcePrefetching {
    public func tableView(_ tableView: UITableView, prefetchRowsAt indexPaths: [IndexPath]) {
        let requests = indexPaths.map { URLRequest(url: baseURL.appendingPathComponent(images[$0.row])) }
        AlamofireImage.ImageDownloader.default.download(requests)
    }
}

显然,cellForRowAt会使用af_setImage

override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath)
    let url = baseURL.appendingPathComponent(images[indexPath.row])
    cell.imageView?.af_setImage(withURL: url, placeholderImage: placeholder)
    return cell
}

我在下面的原始答案显示,对于Objective-C,您如何在10版之前的iOS版本中执行此操作(我们必须执行自己的预取计算)。


这种取消不再可见的单元格下载的行为恰恰是在快速滚动时保持异步图像检索响应的原因,即使连接速度较慢也是如此。例如,如果您快速向下滚动到tableview的第100个单元格,那么您真的不想让图像检索在前面99行(不再可见)的图像检索后面积压。我建议单独保留UIImageView类别,但如果您想要为您可能要滚动到的单元格预取图片,请使用SDWebImagePrefetcher

例如,在我调用reloadData的地方,我还预先获取当前可见单元格之前和之后的十个单元格的图像:

[self.tableView reloadData];
dispatch_async(dispatch_get_main_queue(), ^{
    [self prefetchImagesForTableView:self.tableView];
});

同样,只要我停止滚动,我都会这样做:

- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView {
    [self prefetchImagesForTableView:self.tableView];
}

- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate {
    if (!decelerate)
        [self prefetchImagesForTableView:self.tableView];
}

就我如何预取十个前后单元格而言,我这样做:

#pragma mark - Prefetch cells

static NSInteger const kPrefetchRowCount = 10;

/** Prefetch a certain number of images for rows prior to and subsequent to the currently visible cells
 *
 * @param  tableView   The tableview for which we're going to prefetch images.
 */

- (void)prefetchImagesForTableView:(UITableView *)tableView {
    NSArray *indexPaths = [self.tableView indexPathsForVisibleRows];
    if ([indexPaths count] == 0) return;

    NSIndexPath *minimumIndexPath = indexPaths[0];
    NSIndexPath *maximumIndexPath = [indexPaths lastObject];

    // they should be sorted already, but if not, update min and max accordingly

    for (NSIndexPath *indexPath in indexPaths) {
        if ([minimumIndexPath compare:indexPath] == NSOrderedDescending)
            minimumIndexPath = indexPath;
        if ([maximumIndexPath compare:indexPath] == NSOrderedAscending)
            maximumIndexPath = indexPath;
    }

    // build array of imageURLs for cells to prefetch

    NSMutableArray<NSIndexPath *> *prefetchIndexPaths = [NSMutableArray array];

    NSArray<NSIndexPath *> *precedingRows = [self tableView:tableView indexPathsForPrecedingRows:kPrefetchRowCount fromIndexPath:minimumIndexPath];
    [prefetchIndexPaths addObjectsFromArray:precedingRows];

    NSArray<NSIndexPath *> *followingRows = [self tableView:tableView indexPathsForFollowingRows:kPrefetchRowCount fromIndexPath:maximumIndexPath];
    [prefetchIndexPaths addObjectsFromArray:followingRows];

    // build array of imageURLs for cells to prefetch (how you get the image URLs will vary based upon your implementation)

    NSMutableArray<NSURL *> *urls = [NSMutableArray array];
    for (NSIndexPath *indexPath in prefetchIndexPaths) {
        NSURL *url = self.objects[indexPath.row].imageURL;
        if (url) {
            [urls addObject:url];
        }
    }

    // now prefetch

    if ([urls count] > 0) {
        [[SDWebImagePrefetcher sharedImagePrefetcher] prefetchURLs:urls];
    }
}

/** Retrieve NSIndexPath for a certain number of rows preceding particular NSIndexPath in the table view.
 *
 * @param  tableView  The tableview for which we're going to retrieve indexPaths.
 * @param  count      The number of rows to retrieve
 * @param  indexPath  The indexPath where we're going to start (presumably the first visible indexPath)
 *
 * @return            An array of indexPaths.
 */

- (NSArray<NSIndexPath *> *)tableView:(UITableView *)tableView indexPathsForPrecedingRows:(NSInteger)count fromIndexPath:(NSIndexPath *)indexPath {
    NSMutableArray *indexPaths = [NSMutableArray array];
    NSInteger row = indexPath.row;
    NSInteger section = indexPath.section;

    for (NSInteger i = 0; i < count; i++) {
        if (row == 0) {
            if (section == 0) {
                return indexPaths;
            } else {
                section--;
                row = [tableView numberOfRowsInSection:section] - 1;
            }
        } else {
            row--;
        }
        [indexPaths addObject:[NSIndexPath indexPathForRow:row inSection:section]];
    }

    return indexPaths;
}

/** Retrieve NSIndexPath for a certain number of following particular NSIndexPath in the table view.
 *
 * @param  tableView  The tableview for which we're going to retrieve indexPaths.
 * @param  count      The number of rows to retrieve
 * @param  indexPath  The indexPath where we're going to start (presumably the last visible indexPath)
 *
 * @return            An array of indexPaths.
 */

- (NSArray<NSIndexPath *> *)tableView:(UITableView *)tableView indexPathsForFollowingRows:(NSInteger)count fromIndexPath:(NSIndexPath *)indexPath {
    NSMutableArray *indexPaths = [NSMutableArray array];
    NSInteger row = indexPath.row;
    NSInteger section = indexPath.section;
    NSInteger rowCountForSection = [tableView numberOfRowsInSection:section];

    for (NSInteger i = 0; i < count; i++) {
        row++;
        if (row == rowCountForSection) {
            row = 0;
            section++;
            if (section == [tableView numberOfSections]) {
                return indexPaths;
            }
            rowCountForSection = [tableView numberOfRowsInSection:section];
        }
        [indexPaths addObject:[NSIndexPath indexPathForRow:row inSection:section]];
    }

    return indexPaths;
}

答案 1 :(得分:2)

导致下载取消的行位于UIImageView+WebCache.m

- (void)setImageWithURL:(NSURL *)url placeholderImage:(UIImage *)placeholder options:(SDWebImageOptions)options progress:(SDWebImageDownloaderProgressBlock)progressBlock completed:(SDWebImageCompletedBlock)completedBlock

中的第一行

调用[self cancelCurrentImageLoad];。如果你摆脱这一行,下载操作应继续进行。

此时的问题是,如果单个UIImageView正在等待多次下载完成,则会出现竞争条件。上次完成的下载可能不一定是最后一次下载,因此您的单元格中可能会出现错误的图像。

有几种方法可以解决这个问题。最简单的可能只是在UIImageView+WebCache中添加另一个关联对象,只是最后加载的URL。然后,您可以在完成块中检查此URL,并仅在匹配时设置图像。

答案 2 :(得分:0)

我遇到了同样的问题,我发现UIImageView + WebCache在新的下载时取消了上次下载。

我不确定这是否是作者的意图。所以我在SDWebImage上编写了一个新的category UIImageView。

查看更多内容:ImageDownloadGroup

易于使用:

[cell.imageView mq_setImageWithURL:[NSURL URLWithString:@"http://www.domain.com/path/to/image.jpg"]
                   groupIdentifier:@"customGroupID"
                         completed:^(UIImage *image, NSError *error, SDImageCacheType cacheType, NSURL *imageURL) {

                         }];