首页 文章

从UITableView单元格中的URL加载异步图像 - 滚动时图像更改为错误的图像

提问于
浏览
146

我写了两种方法来在我的UITableView单元格内异步加载图片 . 在这两种情况下,图像都会正常加载,但是当我滚动表格时,图像会改变几次,直到滚动结束并且图像将返回到右图像 . 我不知道为什么会这样 .

#define kBgQueue dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)

- (void)viewDidLoad
{
    [super viewDidLoad];
    dispatch_async(kBgQueue, ^{
        NSData* data = [NSData dataWithContentsOfURL: [NSURL URLWithString:
                                                       @"http://myurl.com/getMovies.php"]];
        [self performSelectorOnMainThread:@selector(fetchedData:)
                               withObject:data waitUntilDone:YES];
    });
}

-(void)fetchedData:(NSData *)data
{
    NSError* error;
    myJson = [NSJSONSerialization
              JSONObjectWithData:data
              options:kNilOptions
              error:&error];
    [_myTableView reloadData];
}    

- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
    // Return the number of sections.
    return 1;
}

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section{
    // Return the number of rows in the section.
    // Usually the number of items in your array (the one that holds your list)
    NSLog(@"myJson count: %d",[myJson count]);
    return [myJson count];
}
    - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{

        myCell *cell = [tableView dequeueReusableCellWithIdentifier:@"cell"];
        if (cell == nil) {
            cell = [[myCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:@"cell"];
        }

        dispatch_async(kBgQueue, ^{
        NSData *imgData = [NSData dataWithContentsOfURL:[NSURL URLWithString:[NSString stringWithFormat:@"http://myurl.com/%@.jpg",[[myJson objectAtIndex:indexPath.row] objectForKey:@"movieId"]]]];

            dispatch_async(dispatch_get_main_queue(), ^{
        cell.poster.image = [UIImage imageWithData:imgData];
            });
        });
         return cell;
}

......

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{

            myCell *cell = [tableView dequeueReusableCellWithIdentifier:@"cell"];
            if (cell == nil) {
                cell = [[myCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:@"cell"];
            }
    NSURL* url = [NSURL URLWithString:[NSString stringWithFormat:@"http://myurl.com/%@.jpg",[[myJson objectAtIndex:indexPath.row] objectForKey:@"movieId"]]];
    NSURLRequest* request = [NSURLRequest requestWithURL:url];


    [NSURLConnection sendAsynchronousRequest:request
                                       queue:[NSOperationQueue mainQueue]
                           completionHandler:^(NSURLResponse * response,
                                               NSData * data,
                                               NSError * error) {
                               if (!error){
                                   cell.poster.image = [UIImage imageWithData:data];
                                   // do whatever you want with image
                               }

                           }];
     return cell;
}

13 回答

  • 3

    假设您正在寻找快速的战术修复,您需要做的是确保单元格图像已初始化,并且单元格的行仍然可见,例如:

    - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
        MyCell *cell = [tableView dequeueReusableCellWithIdentifier:@"cell" forIndexPath:indexPath];
    
        cell.poster.image = nil; // or cell.poster.image = [UIImage imageNamed:@"placeholder.png"];
    
        NSURL *url = [NSURL URLWithString:[NSString stringWithFormat:@"http://myurl.com/%@.jpg", self.myJson[indexPath.row][@"movieId"]]];
    
        NSURLSessionTask *task = [[NSURLSession sharedSession] dataTaskWithURL:url completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
            if (data) {
                UIImage *image = [UIImage imageWithData:data];
                if (image) {
                    dispatch_async(dispatch_get_main_queue(), ^{
                        MyCell *updateCell = (id)[tableView cellForRowAtIndexPath:indexPath];
                        if (updateCell)
                            updateCell.poster.image = image;
                    });
                }
            }
        }];
        [task resume];
    
        return cell;
    }
    

    上面的代码解决了一个问题,这个问题源于该单元被重用的事实:

    • 在启动后台请求之前,您没有初始化单元格图像(这意味着在下载新图像时,出列单元格的最后一个图像仍然可见) . 确保 nil 任何图像视图的 image 属性,否则您将看到图像的闪烁 .

    • 一个更微妙的问题是,在一个非常慢的网络上,您的异步请求可能无法在单元格滚出屏幕之前完成 . 您可以使用 UITableView 方法 cellForRowAtIndexPath: (不要与类似名称的 UITableViewDataSource 方法 tableView:cellForRowAtIndexPath: 混淆)以查看该行的单元格是否仍然可见 . 如果单元格不可见,此方法将返回 nil .

    问题是,当您的异步方法完成时,单元格已经滚动,更糟糕的是,单元格已被重用于表格的另一行 . 通过检查该行是否仍然可见,您将确保不会意外地使用自从屏幕滚动的行的图像更新图像 .

    • 与手头的问题有点无关,我仍然觉得有必要更新它以利用现代约定和API,特别是:

    • 使用 NSURLSession 而不是将 -[NSData contentsOfURL:] 分派给后台队列;

    • 使用 dequeueReusableCellWithIdentifier:forIndexPath: 而不是 dequeueReusableCellWithIdentifier: (但请确保使用单元格原型或注册类或NIB作为该标识符);和

    • 我使用了符合Cocoa naming conventions的类名(即以大写字母开头) .

    即使进行了这些更正,也存在以下问题:

    • 上面的代码没有缓存下载的图像 . 这意味着如果您在屏幕上滚动图像并返回屏幕,应用程序可能会尝试再次检索图像 . 也许你很幸运,你的服务器响应头将允许 NSURLSessionNSURLCache 提供相当透明的缓存,但如果没有,你将会做出不必要的服务器请求并提供更慢的UX .

    • 我们没有取消滚动屏幕的单元格请求 . 因此,如果您快速滚动到第100行,该行的图像可能会滞后于之前甚至不可见的前99行的请求 . 您总是希望确保优先考虑可见单元格的请求,以获得最佳用户体验 .

    解决这些问题的最简单方法是使用 UIImageView 类别,例如SDWebImageAFNetworking提供的类别 . 如果你愿意,你可以编写自己的代码来处理上述问题,但这是很多工作,上面的 UIImageView 类别已经为你做了这个 .

  • -1
    - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath 
    {
            MyCell *cell = [tableView dequeueReusableCellWithIdentifier:@"cell" forIndexPath:indexPath];
    
            cell.poster.image = nil; // or cell.poster.image = [UIImage imageNamed:@"placeholder.png"];
    
            NSURL *url = [NSURL URLWithString:[NSString stringWithFormat:@"http://myurl.com/%@.jpg", self.myJson[indexPath.row][@"movieId"]]];
    
            NSURLSessionTask *task = [[NSURLSession sharedSession] dataTaskWithURL:url completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
                if (data) {
                    UIImage *image = [UIImage imageWithData:data];
                    if (image) {
                        dispatch_async(dispatch_get_main_queue(), ^{
                            MyCell *updateCell = (id)[tableView cellForRowAtIndexPath:indexPath];
                            if (updateCell)
                                updateCell.poster.image = image;
                        });
                    }
                }
            }];
            [task resume];
    
            return cell;
        }
    
  • 0

    / 我这样做了,还测试了它 /

    步骤1 =在viewDidLoad方法中为这样的表注册自定义单元类(如果是表格中的原型单元格)或nib(对于自定义单元格的自定义nib)

    [self.yourTableView registerClass:[CustomTableViewCell class] forCellReuseIdentifier:@"CustomCell"];
    

    要么

    [self.yourTableView registerNib:[UINib nibWithNibName:@"CustomTableViewCell" bundle:nil] forCellReuseIdentifier:@"CustomCell"];
    

    步骤2 =使用UITableView的“dequeueReusableCellWithIdentifier:forIndexPath:”方法(为此,您必须注册class或nib):

    - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
                CustomTableViewCell * cell = [tableView dequeueReusableCellWithIdentifier:@"CustomCell" forIndexPath:indexPath];
    
                cell.imageViewCustom.image = nil; // [UIImage imageNamed:@"default.png"];
                cell.textLabelCustom.text = @"Hello";
    
                dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
                    // retrive image on global queue
                    UIImage * img = [UIImage imageWithData:[NSData dataWithContentsOfURL:     [NSURL URLWithString:kImgLink]]];
    
                    dispatch_async(dispatch_get_main_queue(), ^{
    
                        CustomTableViewCell * cell = (CustomTableViewCell *)[tableView cellForRowAtIndexPath:indexPath];
                      // assign cell image on main thread
                        cell.imageViewCustom.image = img;
                    });
                });
    
                return cell;
            }
    
  • 1

    有多个框架可以解决这个问题 . 仅举几个:

    迅速:

    Objective-C的:

  • 215

    斯威夫特3

    我使用NSCache为图像加载器编写了自己的light实现 . No cell image flickering!

    ImageCacheLoader.swift

    typealias ImageCacheLoaderCompletionHandler = ((UIImage) -> ())
    
    class ImageCacheLoader {
    
        var task: URLSessionDownloadTask!
        var session: URLSession!
        var cache: NSCache<NSString, UIImage>!
    
        init() {
            session = URLSession.shared
            task = URLSessionDownloadTask()
            self.cache = NSCache()
        }
    
        func obtainImageWithPath(imagePath: String, completionHandler: @escaping ImageCacheLoaderCompletionHandler) {
            if let image = self.cache.object(forKey: imagePath as NSString) {
                DispatchQueue.main.async {
                    completionHandler(image)
                }
            } else {
                /* You need placeholder image in your assets, 
                   if you want to display a placeholder to user */
                let placeholder = #imageLiteral(resourceName: "placeholder")
                DispatchQueue.main.async {
                    completionHandler(placeholder)
                }
                let url: URL! = URL(string: imagePath)
                task = session.downloadTask(with: url, completionHandler: { (location, response, error) in
                    if let data = try? Data(contentsOf: url) {
                        let img: UIImage! = UIImage(data: data)
                        self.cache.setObject(img, forKey: imagePath as NSString)
                        DispatchQueue.main.async {
                            completionHandler(img)
                        }
                    }
                })
                task.resume()
            }
        }
    }
    

    用法示例

    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    
        let cell = tableView.dequeueReusableCell(withIdentifier: "Identifier")
    
        cell.title = "Cool title"
    
        imageLoader.obtainImageWithPath(imagePath: viewModel.image) { (image) in
            // Before assigning the image, check whether the current cell is visible
            if let updateCell = tableView.cellForRow(at: indexPath) {
                updateCell.imageView.image = image
            }
        }    
        return cell
    }
    
  • 13

    最好的答案不是正确的方法:( . 你实际上将indexPath与模型绑定,这并不总是好的 . 想象一下,在加载图像时已经添加了一些行 . 现在,给定indexPath的单元格存在于屏幕上,但是图像不再是正确的!情况有点不太可能并且难以复制,但这是可能的 .

    最好使用MVVM方法,在控制器中使用viewModel绑定单元格并在viewModel中加载图像(使用switchToLatest方法分配ReactiveCocoa信号),然后订阅此信号并将图像分配给单元格! ;)

    你必须记住不要滥用MVVM . 观点必须简单明了!而ViewModels应该是可重用的!这就是在控制器中绑定View(UITableViewCell)和ViewModel非常重要的原因 .

  • 14

    在我的情况下,它不是由于图像缓存(使用SDWebImage) . 这是因为自定义单元格的标记与indexPath.row不匹配 .

    在cellForRowAtIndexPath上:

    1)为自定义单元格指定索引值 . 例如,

    cell.tag = indexPath.row
    

    2)在主线程上,在分配图像之前,通过将图像与标记匹配来检查图像是否属于相应的单元格 .

    dispatch_async(dispatch_get_main_queue(), ^{
       if(cell.tag == indexPath.row) {
         UIImage *tmpImage = [[UIImage alloc] initWithData:imgData];
         thumbnailImageView.image = tmpImage;
       }});
    });
    
  • 0

    这是swift版本(使用@Nitesh Borad目标C代码): -

    if let img: UIImage = UIImage(data: previewImg[indexPath.row]) {
                    cell.cardPreview.image = img
                } else {
                    // The image isn't cached, download the img data
                    // We should perform this in a background thread
                    let imgURL = NSURL(string: "webLink URL")
                    let request: NSURLRequest = NSURLRequest(URL: imgURL!)
                    let session = NSURLSession.sharedSession()
                    let task = session.dataTaskWithRequest(request, completionHandler: {data, response, error -> Void in
                        let error = error
                        let data = data
                        if error == nil {
                            // Convert the downloaded data in to a UIImage object
                            let image = UIImage(data: data!)
                            // Store the image in to our cache
                            self.previewImg[indexPath.row] = data!
                            // Update the cell
                            dispatch_async(dispatch_get_main_queue(), {
                                if let cell: YourTableViewCell = tableView.cellForRowAtIndexPath(indexPath) as? YourTableViewCell {
                                    cell.cardPreview.image = image
                                }
                            })
                        } else {
                            cell.cardPreview.image = UIImage(named: "defaultImage")
                        }
                    })
                    task.resume()
                }
    
  • 2

    谢谢你“抢”....我和UICollectionView有同样的问题,你的答案帮助我解决了我的问题 . 这是我的代码:

    if ([Dict valueForKey:@"ImageURL"] != [NSNull null])
        {
            cell.coverImageView.image = nil;
            cell.coverImageView.imageURL=nil;
    
            dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    
                if ([Dict valueForKey:@"ImageURL"] != [NSNull null] )
                {
                    dispatch_async(dispatch_get_main_queue(), ^{
    
                        myCell *updateCell = (id)[collectionView cellForItemAtIndexPath:indexPath];
    
                        if (updateCell)
                        {
                            cell.coverImageView.image = nil;
                            cell.coverImageView.imageURL=nil;
    
                            cell.coverImageView.imageURL=[NSURL URLWithString:[Dict valueForKey:@"ImageURL"]];
    
                        }
                        else
                        {
                            cell.coverImageView.image = nil;
                            cell.coverImageView.imageURL=nil;
                        }
    
    
                    });
                }
            });
    
        }
        else
        {
            cell.coverImageView.image=[UIImage imageNamed:@"default_cover.png"];
        }
    
  • 0

    我想你想加快你在背景中为单元格加载图像时的单元格加载速度 . 为此,我们完成了以下步骤:

    • 检查文件是否存在于文档目录中 .

    • 如果没有,则首次加载图像,并将其保存到手机文档目录 . 如果您不想将图像保存在手机中,则可以直接在后台加载单元格图像 .

    • 现在加载过程:

    只需包括: #import "ManabImageOperations.h"

    对于单元格,代码如下所示:

    NSString *imagestr=[NSString stringWithFormat:@"http://www.yourlink.com/%@",[dictn objectForKey:@"member_image"]];
    
            NSString *docDir=[NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES)objectAtIndex:0];
            NSLog(@"Doc Dir: %@",docDir);
    
            NSString  *pngFilePath = [NSString stringWithFormat:@"%@/%@",docDir,[dictn objectForKey:@"member_image"]];
    
            BOOL fileExists = [[NSFileManager defaultManager] fileExistsAtPath:pngFilePath];
            if (fileExists)
            {
                [cell1.memberimage setImage:[UIImage imageWithContentsOfFile:pngFilePath] forState:UIControlStateNormal];
            }
            else
            {
                [ManabImageOperations processImageDataWithURLString:imagestr andBlock:^(NSData *imageData)
                 {
                     [cell1.memberimage setImage:[[UIImage alloc]initWithData: imageData] forState:UIControlStateNormal];
                    [imageData writeToFile:pngFilePath atomically:YES];
                 }];
    }
    

    ManabImageOperations.h:

    #import <Foundation/Foundation.h>
    
        @interface ManabImageOperations : NSObject
        {
        }
        + (void)processImageDataWithURLString:(NSString *)urlString andBlock:(void (^)(NSData *imageData))processImage;
        @end
    

    ManabImageOperations.m:

    #import "ManabImageOperations.h"
    #import <QuartzCore/QuartzCore.h>
    @implementation ManabImageOperations
    
    + (void)processImageDataWithURLString:(NSString *)urlString andBlock:(void (^)(NSData *imageData))processImage
    {
        NSURL *url = [NSURL URLWithString:urlString];
    
        dispatch_queue_t callerQueue = dispatch_get_main_queue();
        dispatch_queue_t downloadQueue = dispatch_queue_create("com.myapp.processsmagequeue", NULL);
        dispatch_async(downloadQueue, ^{
            NSData * imageData = [NSData dataWithContentsOfURL:url];
    
            dispatch_async(callerQueue, ^{
                processImage(imageData);
            });
        });
      //  downloadQueue=nil;
        dispatch_release(downloadQueue);
    
    }
    @end
    

    如果有任何问题,请检查答案和评论....

  • 3

    Simply change,

    dispatch_async(kBgQueue, ^{
         NSData *imgData = [NSData dataWithContentsOfURL:[NSURL URLWithString:   [NSString stringWithFormat:@"http://myurl.com/%@.jpg",[[myJson objectAtIndex:indexPath.row] objectForKey:@"movieId"]]]];
         dispatch_async(dispatch_get_main_queue(), ^{
            cell.poster.image = [UIImage imageWithData:imgData];
         });
     });
    

    Into

    dispatch_async(kBgQueue, ^{
             NSData *imgData = [NSData dataWithContentsOfURL:[NSURL URLWithString:   [NSString stringWithFormat:@"http://myurl.com/%@.jpg",[[myJson objectAtIndex:indexPath.row] objectForKey:@"movieId"]]]];
             cell.poster.image = [UIImage imageWithData:imgData];
             dispatch_async(dispatch_get_main_queue(), ^{
                [self.tableView reloadRowsAtIndexPaths:indexPaths withRowAnimation:UITableViewRowAnimationNone];
             });
         });
    
  • 3

    您只需传递您的网址即可

    NSURL *url = [NSURL URLWithString:@"http://www.myurl.com/1.png"];
    NSURLSessionTask *task = [[NSURLSession sharedSession] dataTaskWithURL:url completionHandler:^(NSData * _Nullable data,    NSURLResponse * _Nullable response, NSError * _Nullable error) {
        if (data) {
            UIImage *image = [UIImage imageWithData:data];
            if (image) {
                dispatch_async(dispatch_get_main_queue(), ^{
                        yourimageview.image = image;
                });
            }
        }
    }];
    [task resume];
    
  • 4
    -(UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{
        Static NSString *CellIdentifier = @"Cell";
        QTStaffViewCell *cell = (QTStaffViewCell *)[tableView dequeueReusableCellWithIdentifier:CellIdentifier];
    
        If (cell == nil)
        {
    
            NSArray *nib = [[NSBundle mainBundle] loadNibNamed:@"QTStaffViewCell" owner:self options:nil];
            cell = [nib objectAtIndex: 0];
    
        }
    
        StaffData = [self.staffArray objectAtIndex:indexPath.row];
        NSString *title = StaffData.title;
        NSString *fName = StaffData.firstname;
        NSString *lName = StaffData.lastname;
    
        UIFont *FedSanDemi = [UIFont fontWithName:@"Aller" size:18];
        cell.drName.text = [NSString stringWithFormat:@"%@ %@ %@", title,fName,lName];
        [cell.drName setFont:FedSanDemi];
    
        UIFont *aller = [UIFont fontWithName:@"Aller" size:14];
        cell.drJob.text = StaffData.job;
        [cell.drJob setFont:aller];
    
        if ([StaffData.title isEqualToString:@"Dr"])
        {
            cell.drJob.frame = CGRectMake(83, 26, 227, 40);
        }
        else
        {
            cell.drJob.frame = CGRectMake(90, 26, 227, 40);
    
        }
    
        if ([StaffData.staffPhoto isKindOfClass:[NSString class]])
        {
            NSURL *url = [NSURL URLWithString:StaffData.staffPhoto];
            NSURLSession *session = [NSURLSession sharedSession];
            NSURLSessionDownloadTask *task = [session downloadTaskWithURL:url
                    completionHandler:^(NSURL *location,NSURLResponse *response, NSError *error) {
    
          NSData *imageData = [NSData dataWithContentsOfURL:location];
          UIImage *image = [UIImage imageWithData:imageData];
    
          dispatch_sync(dispatch_get_main_queue(),
                 ^{
                        cell.imageView.image = image;
                  });
        }];
            [task resume];
        }
           return cell;}
    

相关问题