简体   繁体   English

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

[英]SDWebImage does not load remote images until scroll

I am using SDWebImage library to load remote images into a table view which uses a custom cell class i have created. 我正在使用SDWebImage库将远程图像加载到一个表视图中,该视图使用我创建的自定义单元类。 I simply use 我只是用

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

in cellForRowAtIndexPath: Now the problem is it loads images in the visible cells only and not for cells that are offscreen for which i have to scroll up and down to make them load. in cellForRowAtIndexPath:现在的问题是它只在可见单元格中加载图像,而不是在屏幕上为我必须向上和向下滚动以加载图像的单元格。 Is there any way i can load all images without having to scroll the table view. 有没有办法我可以加载所有图像,而无需滚动表格视图。 Thanks in advance!! 提前致谢!!

If you want to prefetch rows, you can respond to UIScrollViewDelegate methods to determine when the table scrolling is done, triggering a prefetch of the rows. 如果要预取行,可以响应UIScrollViewDelegate方法以确定表滚动何时完成,从而触发行的预取。 You can perform the prefetch using SDWebImagePrefetcher (in my original answer I was a little dismissive of this useful class, but it seems to work relatively well now): 你可以使用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

Here is the simple cellForRowAtIndexPath (not entirely relevant, but just showing that if you use SDWebImagePrefetcher , you don't have to mess around with cellForRowAtIndexPath : 这是简单的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;
}

These UIScrollViewDelegate methods prefetch more rows when scrolling finishes 滚动完成时,这些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];
}

You obviously need to implement a prefetch routine. 您显然需要实现预取例程。 This gets the NSIndexPath values for the cells on each side of the visible cells, gets their image URLs, and then prefetches that data. 这将获取可见单元格两侧的单元格的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];
    }
}

These are the utility methods for getting the NSIndexPath for the rows immediately preceding the visible cells as well as those immediately following the visible cells: 这些是为紧邻可见单元格之前的行以及紧跟可见单元格之后的行获取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;
}

There's a lot there, but in reality, SDWebImage and its SDWebImagePrefetcher is doing the heavy lifting. 那里有很多,但实际上, SDWebImage和它的SDWebImagePrefetcher正在进行繁重的工作。

I include my original answer below for the sake of completeness. 为了完整起见,我在下面提供了我原来的答案。


Original answer: 原始答案:

If you want to do some prefetching with SDWebImage , you could do something like the following: 如果您想使用SDWebImage进行预取,可以执行以下操作:

  1. Add a completion block to your setImageWithURL call: 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; } 

    I must confess I don't really like calling my prefetcher routine here (I wish iOS had some nice didFinishTableRefresh delegate method), but it works, even if it's calling the routine more times than I'd really want. 我必须承认我不喜欢在这里调用我的prefetcher例程(我希望iOS有一些不错的didFinishTableRefresh委托方法),但是它有效,即使它调用例程的次数比我真正想要的多。 I just make sure below that the routine below makes sure that it won't make redundant requests. 我只是确保下面的例程确保它不会发出冗余请求。

  2. Anyway, I write a prefetch routine that looks for, say, the next ten images: 无论如何,我编写了一个预取例程来查找接下来的十个图像:

     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. In the downloading routine, you can check to see if the image download has started and, if not, then start it. 在下载例程中,您可以检查图像下载是否已开始,如果没有,则启动它。 To do this with SDWebImage , I keep a weak pointer to the web image operation in my TableModelRow class (the model class that backs the individual rows of my table): 要使用SDWebImage执行此操作,我在TableModelRow类(支持表的各行的模型类)中保留了一个指向Web图像操作的weak指针:

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

    I then have the downloadImageIfNeeded routine start a download if it hasn't already (you can see why making that weak was so important ... I'm checking to see if this row already has an operation pending before starting another). 然后我将downloadImageIfNeeded例程开始下载(如果它还没有)(你可以看到为什么这个weak是如此重要......我正在检查这行是否已经有一个操作挂起,然后再启动另一个)。 I'm not doing anything with the downloaded image (short of, for debugging purposes, logging the fact that a download was done), but rather just downloading and letting SDImageWeb keep track of the cached image for me, so when cellForRowAtIndexPath later requests the image as the user scrolls down, it's there, ready and waiting. 我没有对下载的图像做任何事情(为了调试目的,记录下载已完成的事实),而是只是下载并让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 }]; } 

    Part of me thinks it might be more robust to call imageManager.imageCache instance method queryDiskCacheForKey first, but after doing some testing, it doesn't look like that's needed (and the downloadWithURL does that for us, anyway). 部分我认为首先调用imageManager.imageCache实例方法queryDiskCacheForKey可能会更健壮,但在进行一些测试之后,它看起来并不像需要的那样(而且无论如何, downloadWithURL都为我们做了这个)。

I should point out that the SDImageWeb library does have a SDWebImagePrefetcher class (see the documentation ). 我应该指出, SDImageWeb库确实有一个SDWebImagePrefetcher类(参见文档 )。 The name of the class is incredibly promising, but looking at the code, with all deference to an otherwise excellent library, this doesn't feel very robust to me (eg it is a simple list of URLs to fetch and if you do it again, it cancels the prior list with no notion of "adding to the queue" or anything like that). 这个类的名称是非常有前途的,但看着代码,完全尊重一个优秀的库,这对我来说感觉不太健壮(例如,它是一个简单的URL列表,如果你再次这样做,它取消了先前的列表,没有“添加到队列”或类似的东西的概念)。 It's a promising notion, but a little weak in execution. 这是一个很有希望的概念,但在执行方面有点弱。 And when I tried it, my UX suffered noticeably. 当我尝试它时,我的用户体验明显遭受了损失。

So, I'm inclined to not use SDWebImagePrefetcher (until it's improved, at least), and stick to my rudimentary prefetching technique. 所以,我倾向于不使用SDWebImagePrefetcher (至少它是改进的),并坚持我的基本预取技术。 It's not terribly sophisticated, but it seems to work. 它不是非常复杂,但似乎有效。

I just had to solve this exact problem and didn't want the overhead of the prefetcher. 我只需解决这个确切的问题,并不想要预取器的开销。 There must be some extra under-the-hood stuff happening with the built-in imageView property that prevents the loading, because a new UIImageView works just fine. 内置的imageView属性必然会发生一些额外的引擎盖内容,这会阻止加载,因为新的UIImageView工作得很好。

My solution is pretty clean if you don't mind (or are already) using a subclass of UITableViewCell: 如果您不介意(或已经)使用UITableViewCell的子类,我的解决方案非常干净:

  1. Subclass UITableViewCell. 子类UITableViewCell。
  2. In your subclass, hide self.imageView. 在您的子类中,隐藏self.imageView。
  3. Create your own UIImageView subview and set this view's image. 创建自己的UIImageView子视图并设置视图的图像。

Here's a modified version of my own code (undocumented here is setting the frame to match the size & position of the iOS Photo app's album covers): 这是我自己的代码的修改版本(这里没有记录是设置框架以匹配iOS Photo应用程序专辑封面的大小和位置):

YourTableCell.h YourTableCell.h

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

YourTableCell.m 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 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];
    //...
}

This is an example and you need to implement this for your purpose. 这是一个示例,您需要为此目的实现此目的。
your UITableView delegate: 你的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;
}

your custom UITableViewCell .h file : 您的自定义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

your custom UITableViewCell .m file : 您的自定义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 needed to detect if we reuse this cell for another image instead of image which was downloaded while user scrolls the table view. currentLoadingIndexPath需要检测我们是否将此单元重用于另一个图像而不是用户滚动表视图时下载的图像。

I met the same problem,I found UIImageView+WebCache cancel last download when a new download come. 我遇到了同样的问题,我发现UIImageView + WebCache在新下载时取消了上次下载。

I not sure whether this is the intention of the author. 我不确定这是否是作者的意图。 So I write a new category of UIImageView base on SDWebImage. 所以我在SDWebImage上编写了一个新的UIImageView category

Easy to use: 易于使用:

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

                         }];

To view more: ImageDownloadGroup 查看更多: ImageDownloadGroup

Advanced usage: 高级用法:

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

                         }];

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM