在滚动之前,SDWebImage不会加载远程图像

时间:2013-03-29 10:34:08

标签: ios objective-c uitableview lazy-loading sdwebimage

我正在使用SDWebImage库将远程图像加载到一个表视图中,该视图使用我创建的自定义单元类。我只是使用

[cell.imageView setImageWithURL:url placeholderImage:[UIImage imageNamed:@"loading.jpg"]];
在cellForRowAtIndexPath中

: 现在问题是它只在可见单元格中加载图像而不是在屏幕上的单元格,我必须向上和向下滚动以使它们加载。有没有办法我可以加载所有图像,而无需滚动表格视图。 在此先感谢!!

4 个答案:

答案 0 :(得分:20)

如果要预取行,可以响应UIScrollViewDelegate方法以确定表滚动何时完成,从而触发行的预取。您可以使用SDWebImagePrefetcher执行预取(在我的原始答案中,我对这个有用的类有点不屑一顾,但它现在看起来效果相当好):

- (void)viewDidLoad
{
    [super viewDidLoad];

    // the details don't really matter here, but the idea is to fetch data, 
    // call `reloadData`, and then prefetch the other images

    NSURL *url = [NSURL URLWithString:kUrlWithJSONData];
    NSURLRequest *request = [NSURLRequest requestWithURL:url];
    [NSURLConnection sendAsynchronousRequest:request queue:[NSOperationQueue mainQueue] completionHandler:^(NSURLResponse *response, NSData *data, NSError *connectionError) {
        if (connectionError) {
            NSLog(@"sendAsynchronousRequest error: %@", connectionError);
            return;
        }

        self.objects = [NSJSONSerialization JSONObjectWithData:data options:0 error:nil];

        [self.tableView reloadData];

        [self prefetchImagesForTableView:self.tableView];
    }];
}

// some of the basic `UITableViewDataDelegate` methods have been omitted because they're not really relevant

以下是简单的cellForRowAtIndexPath(并非完全相关,但只是表明如果您使用SDWebImagePrefetcher,则无需使用cellForRowAtIndexPath

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    static NSString *cellIdentifier = @"Cell";
    CustomCell *cell = [tableView dequeueReusableCellWithIdentifier:cellIdentifier];
    NSAssert([cell isKindOfClass:[CustomCell class]], @"cell should be CustomCell");

    [cell.customImageView setImageWithURL:[self urlForIndexPath:indexPath] placeholderImage:nil];
    [cell.customLabel setText:[self textForIndexPath:indexPath]];

    return cell;
}

这些UIScrollViewDelegate方法在滚动完成时预取更多行

- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView
{
    // if `decelerate` was true for `scrollViewDidEndDragging:willDecelerate:`
    // this will be called when the deceleration is done

    [self prefetchImagesForTableView:self.tableView];
}

- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate
{
    // if `decelerate` is true, then we shouldn't start prefetching yet, because
    // `cellForRowAtIndexPath` will be hard at work returning cells for the currently visible
    // cells.

    if (!decelerate)
        [self prefetchImagesForTableView:self.tableView];
}

您显然需要实现预取例程。这将获取可见单元格每一侧的单元格的NSIndexPath值,获取其图像URL,然后预取该数据。

/** 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 (indexPath.section < minimumIndexPath.section || (indexPath.section == minimumIndexPath.section && indexPath.row < minimumIndexPath.row)) minimumIndexPath = indexPath;
        if (indexPath.section > maximumIndexPath.section || (indexPath.section == maximumIndexPath.section && indexPath.row > maximumIndexPath.row)) maximumIndexPath = indexPath;
    }

    // build array of imageURLs for cells to prefetch

    NSMutableArray *imageURLs = [NSMutableArray array];
    indexPaths = [self tableView:tableView priorIndexPathCount:kPrefetchRowCount fromIndexPath:minimumIndexPath];
    for (NSIndexPath *indexPath in indexPaths)
        [imageURLs addObject:[self urlForIndexPath:indexPath]];
    indexPaths = [self tableView:tableView nextIndexPathCount:kPrefetchRowCount fromIndexPath:maximumIndexPath];
    for (NSIndexPath *indexPath in indexPaths)
        [imageURLs addObject:[self urlForIndexPath:indexPath]];

    // now prefetch

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

这些是实用方法,用于获取紧邻可见单元格之前的行的NSIndexPath以及紧跟可见单元格之后的行:

/** 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 *)tableView:(UITableView *)tableView priorIndexPathCount:(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 *)tableView:(UITableView *)tableView nextIndexPathCount:(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;
}

那里有很多,但实际上,SDWebImage及其SDWebImagePrefetcher正在进行繁重的工作。

为了完整起见,我在下面提供了我原来的答案。


原始答案:

如果您想使用SDWebImage进行预取,可以执行以下操作:

  1. setImageWithURL电话中添加完成功能块:

    - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
    {
        NSLog(@"%s", __FUNCTION__);
    
        static NSString *cellIdentifier = @"Cell";
        UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:cellIdentifier];
    
        TableModelRow *rowData = self.objects[indexPath.row];
    
        cell.textLabel.text = rowData.title;
        [cell.imageView setImageWithURL:rowData.url
                       placeholderImage:[UIImage imageNamed:@"placeholder.png"]
                              completed:^(UIImage *image, NSError *error, SDImageCacheType cacheType) {
                                  [self prefetchImagesForTableView:tableView];
                              }];
    
        return cell;
    }
    

    我必须承认我不喜欢在这里调用我的prefetcher例程(我希望iOS有一些很好的didFinishTableRefresh委托方法),但是它有效,即使它调用例程的次数多于我真的很想要。我只是在下面确定下面的例程确保它不会发出冗余请求。

  2. 无论如何,我编写了一个预取例程来查找接下来的十张图片:

    const NSInteger kPrefetchRowCount = 10;
    
    - (void)prefetchImagesForTableView:(UITableView *)tableView
    {
        // determine the minimum and maximum visible rows
    
        NSArray *indexPathsForVisibleRows = [tableView indexPathsForVisibleRows];
        NSInteger minimumVisibleRow = [indexPathsForVisibleRows[0] row];
        NSInteger maximumVisibleRow = [indexPathsForVisibleRows[0] row];
    
        for (NSIndexPath *indexPath in indexPathsForVisibleRows)
        {
            if (indexPath.row < minimumVisibleRow) minimumVisibleRow = indexPath.row;
            if (indexPath.row > maximumVisibleRow) maximumVisibleRow = indexPath.row;
        }
    
        // now iterate through our model;
        // `self.objects` is an array of `TableModelRow` objects, one object
        // for every row of the table.
    
        [self.objects enumerateObjectsUsingBlock:^(TableModelRow *obj, NSUInteger idx, BOOL *stop) {
            NSAssert([obj isKindOfClass:[TableModelRow class]], @"Expected TableModelRow object");
    
            // if the index is within `kPrefetchRowCount` rows of our visible rows, let's
            // fetch the image, if it hasn't already done so.
    
            if ((idx < minimumVisibleRow && idx >= (minimumVisibleRow - kPrefetchRowCount)) ||
                (idx > maximumVisibleRow && idx <= (maximumVisibleRow + kPrefetchRowCount)))
            {
                // my model object has method for initiating a download if needed
    
                [obj downloadImageIfNeeded];
            }
        }];
    }
    
  3. 在下载例程中,您可以检查图像下载是否已开始,如果没有,则启动它。要使用SDWebImage执行此操作,我会在weak类(支持表的各行的模型类)中保留一个TableModelRow指向Web图像操作的指针:

    @property (nonatomic, weak) id<SDWebImageOperation> webImageOperation;
    

    然后让我downloadImageIfNeeded例程开始下载(如果还没有)(您可以看到为什么使weak如此重要...我正在检查这行是否已经在开始另一个之前有一个待处理的操作)我没有对下载的图像做任何事情(为了调试目的,记录下载完成的事实),而是只是下载并让SDImageWeb跟踪我的缓存图像,所以当cellForRowAtIndexPath稍后在用户向下滚动时请求图像,它就在那里,准备就绪并等待。

    - (void)downloadImageIfNeeded
    {
        if (self.webImageOperation)
            return;
    
        SDWebImageManager *imageManager = [SDWebImageManager sharedManager];
    
        self.webImageOperation = [imageManager downloadWithURL:self.url
                                                       options:0
                                                      progress:nil
                                                     completed:^(UIImage *image, NSError *error, SDImageCacheType cacheType, BOOL finished) {
                                                         NSLog(@"%s: downloaded %@", __FUNCTION__, self.title);
                                                         // I'm not going to do anything with the image, but `SDWebImage` has now cached it for me
                                                     }];
    }
    

    部分我认为首先调用imageManager.imageCache实例方法queryDiskCacheForKey可能会更强大,但在进行一些测试之后,它看起来并不像需要的那样(downloadWithURL无论如何,这对我们来说是这样的。

  4. 我应该指出SDImageWeb库确实有一个SDWebImagePrefetcher类(请参阅the documentation)。这个类的名称是非常有前途的,但看着代码,完全尊重一个优秀的库,这对我来说感觉不太健壮(例如,它是一个简单的URL列表,如果你再次这样做,它取消了先前的列表,没有“添加到队列”或类似的东西的概念)。这是一个很有希望的概念,但在执行方面有点弱。当我尝试它时,我的用户体验明显受到了影响。

    所以,我倾向于不使用SDWebImagePrefetcher(直到它得到改善,至少),并坚持我的基本预取技术。它不是非常复杂,但似乎有用。

答案 1 :(得分:2)

我只需要解决这个确切的问题,并且不需要预取器的开销。内置的imageView属性必须有一些额外的引擎盖内容,这会阻止加载,因为新的UIImageView工作得很好。

如果您不介意(或者已经)使用UITableViewCell的子类,我的解决方案非常简洁:

  1. 子类UITableViewCell。
  2. 在您的子类中,隐藏self.imageView。
  3. 创建您自己的UIImageView子视图并设置视图的图像。
  4. 这是我自己代码的修改版本(此处未记载的是设置框架以匹配iOS Photo应用程序专辑封面的大小和位置):

    <强> YourTableCell.h

    @interface YourTableCell : UITableViewCell
        @property (nonatomic, strong) UIImageView *coverPhoto;
    @end
    

    <强> YourTableCell.m

    @implementation YourTableCell
    
    @synthesize coverPhoto;
    
    - (id)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier
    {
        self = [super initWithStyle:style reuseIdentifier:reuseIdentifier];
        if (self) {
            self.imageView.image = nil;
            self.coverPhoto = [[UIImageView alloc] init];
    
            // Any customization, such as initial image, frame bounds, etc. goes here.        
    
            [self.contentView addSubview:self.coverPhoto];
        }
        return self;
    }
    //...
    @end
    

    <强> YourTableViewController.m

    - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
    {
        static NSString *CellIdentifier = @"Cell";
        YourTableCell *cell = (YourTableCell *)[tableView dequeueReusableCellWithIdentifier:CellIdentifier];
        //...
        [cell.coverPhoto setImageWithURL:coverUrl placeholderImage:nil options:SDWebImageCacheMemoryOnly];
        //...
    }
    

答案 2 :(得分:1)

这是一个示例,您需要为此目的实现此目的。
您的UITableView代理:

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    YourCustomTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"YourCustomTableViewCellReuseIdentifier"];

    if (!cell)
    {
        cell = [[[YourCustomTableViewCell alloc] initWithStyle:UITableViewCellStyleDefault
                               reuseIdentifier:CellIdentifier];        
    }

    NSString *imageURL = // ... get image url, typically from array
    [cell loadImageWithURLString:imageURL forIndexPath:indexPath]; 

    return cell;
}

您的自定义UITableViewCell .h文件

#import <UIKit/UIKit.h>
#import "UIImageView+WebCache.h"
#import "SDImageCache.h"

@interface YourCustomTableViewCell
{
    NSIndexPath *currentLoadingIndexPath;
}

- (void)loadImageWithURLString:(NSString *)urlString forIndexPath:(NSIndexPath *)indexPath;

@end

您的自定义UITableViewCell .m文件

// ... some other methods

- (void)loadImageWithURLString:(NSString *)urlString forIndexPath:(NSIndexPath *)indexPath
{
    currentLoadingIndexPath = indexPath;
    [self.imageView cancelCurrentImageLoad];
    [self.imageView setImage:nil];

    NSURL *imageURL = [NSURL URLWithString:urlString];
    [self.imageView setImageWithURL:imageURL
                   placeholderImage:nil
                            options:SDWebImageRetryFailed
                          completed:^(UIImage *image, NSError *error, SDImageCacheType cacheType)
    {
        if (currentLoadingIndexPath != indexPath)
        {
            return;
        }

        if (error)
        {
            ... // handle error
        }
        else
        {
            [imageView setImage:image];
        }
    }];
}

// ... some other methods

currentLoadingIndexPath需要检测我们是否将此单元重用于另一个图像而不是用户滚动表视图时下载的图像。

答案 3 :(得分:0)

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

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

易于使用:

[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) {

                         }];

查看更多内容:ImageDownloadGroup

高级用法:

//  create customGroup
MQImageDownloadGroup *customGroup = [[MQImageDownloadGroup alloc] initWithGroupIdentifier:@"tableViewCellGroup"];
customGroup.maxConcurrentDownloads = 99;

//  add to MQImageDownloadGroupManage
[[MQImageDownloadGroupManage shareInstance] addGroup:customGroup];

//  use download group
[cell.imageView mq_setImageWithURL:@"https://xxx"
                   groupIdentifier:@"tableViewCellGroup"
                         completed:^(UIImage *image, NSError *error, SDImageCacheType cacheType, NSURL *imageURL) {

                         }];