【问题标题】:Loading an image into UIImage asynchronously将图像异步加载到 UIImage 中
【发布时间】:2012-04-04 20:58:20
【问题描述】:

我正在使用 iOS 5.0 SDK 和 XCode 4.2 开发一个 iOS 4 应用程序。

我必须在 UITableView 中显示一些博文。当我检索到所有 Web 服务数据后,我使用此方法创建一个 UITableViewCell:

- (BlogTableViewCell *)tableView:(UITableView *)tableView
         cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    static NSString* cellIdentifier = @"BlogCell";

    BlogTableViewCell* cell = [tableView dequeueReusableCellWithIdentifier:cellIdentifier];

    if (cell == nil)
    {
        NSArray* topLevelObjects =  [[NSBundle mainBundle] loadNibNamed:@"BlogTableViewCell" owner:nil options:nil];

        for(id currentObject in topLevelObjects)
        {
            if ([currentObject isKindOfClass:[BlogTableViewCell class]])
            {
                cell = (BlogTableViewCell *)currentObject;
                break;
            }
        }
    }

    BlogEntry* entry = [blogEntries objectAtIndex:indexPath.row];

    cell.title.text = entry.title;
    cell.text.text = entry.text;
    cell.photo.image = [UIImage imageWithData:[NSData dataWithContentsOfURL:[NSURL URLWithString:entry.photo]]];

    return cell;
}

但是这一行:

cell.photo.image = [UIImage imageWithData:[NSData dataWithContentsOfURL:[NSURL URLWithString:entry.photo]]];

太慢了(entry.photo 有一个 http url)。

有没有办法异步加载该图像?我认为这很困难,因为 tableView:cellForRowAtIndexPath 经常被调用。

【问题讨论】:

    标签: ios cocoa-touch uitableview asynchronous uiimageview


    【解决方案1】:

    为此,我编写了一个自定义类,使用块和 GCD:

    WebImageOperations.h

    #import <Foundation/Foundation.h>
    
    @interface WebImageOperations : NSObject {
    }
    
    // This takes in a string and imagedata object and returns imagedata processed on a background thread
    + (void)processImageDataWithURLString:(NSString *)urlString andBlock:(void (^)(NSData *imageData))processImage;
    @end
    

    WebImageOperations.m

    #import "WebImageOperations.h"
    #import <QuartzCore/QuartzCore.h>
    
    @implementation WebImageOperations
    
    
    + (void)processImageDataWithURLString:(NSString *)urlString andBlock:(void (^)(NSData *imageData))processImage
    {
        NSURL *url = [NSURL URLWithString:urlString];
    
        dispatch_queue_t callerQueue = dispatch_get_current_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);
            });
        });
        dispatch_release(downloadQueue);
    }
    
    @end
    

    在你的 ViewController 中

        - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
    {
    
        // Pass along the URL to the image (or change it if you are loading there locally)
        [WebImageOperations processImageDataWithURLString:entry.photo andBlock:^(NSData *imageData) {
        if (self.view.window) {
            UIImage *image = [UIImage imageWithData:imageData];
    
            cell.photo.image = image;
        }
    
        }];
    }
    

    它非常快,并且会在不影响 UI 或 TableView 滚动速度的情况下加载图像。

    *** 注意 - 此示例假定正在使用 ARC。如果没有,您将需要管理自己的对象发布)

    【讨论】:

    • 在这种情况下如何防止图像重新下载? (我的意思是每次重用一个单元格时,该方法都会触发,它会从服务器重新下载数据,对吗?)
    • 只需将图像数据添加到数组中,并在异步加载之前检查数组中是否存在图像数据。如果你需要的话,我会给你做饭(但会在今天晚些时候)
    • 这是对所提出问题的一个很好的回答。它应该至少有几票。
    • 顺便说一下,在完成块中,您应该只更新单元格!您无法保证该单元格随后没有滚出屏幕,因此已出列并重新用于表格的另一行(如果您滚动速度非常快或网络连接速度较慢,这一点很重要)。您应该调用 tableview 的 cellForRowAtIndexPath(不要与 table view 控制器的同名方法混淆)并确保它不是 nil。因此CustomCell *updateCell = [tableView cellForRowAtIndexPath:indexPath]; if (updateCell) updateCell.photo.image = ...;
    • dispatch_get_current_queue 在 iOS 6 中已弃用。
    【解决方案2】:

    在 iOS 6 及更高版本中,dispatch_get_current_queue 会给出弃用警告。

    这是上面@ElJay 答案和@khanlou here 的文章的综合的替代方案。

    在 UIImage 上创建一个类别:

    UIImage+Helpers.h

    @interface UIImage (Helpers)
    
    + (void) loadFromURL: (NSURL*) url callback:(void (^)(UIImage *image))callback;
    
    @end
    

    UIImage+Helpers.m

    #import "UIImage+Helpers.h"
    
    @implementation UIImage (Helpers)
    
    + (void) loadFromURL: (NSURL*) url callback:(void (^)(UIImage *image))callback {
        dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0ul);
        dispatch_async(queue, ^{
            NSData * imageData = [NSData dataWithContentsOfURL:url];
            dispatch_async(dispatch_get_main_queue(), ^{
                UIImage *image = [UIImage imageWithData:imageData];
                callback(image);
            });
        });
    }
    
    @end
    

    【讨论】:

    • 一个类别确实是要走的路。
    • 如何在类中调用方法? UIImage *img = [UIImage loadFromURL:[NSURL URLWithString:@""] 回调:];我应该在回调中放什么?
    • @bbrame 请给我一个答案!
    • 想同时加载两张图片。需要很长时间,为什么?
    • @Pinturikkio ... 一个示例实现是: [UIImage loadFromURL:_myNSURL callback:^(UIImage *image){ _myImageView.image = image; _mySpinner.alpha = 0.0; [_mySpinner 停止动画]; }];
    【解决方案3】:

    看看 SDWebImage:

    https://github.com/rs/SDWebImage

    这是一组很棒的类,可以为您处理所有事情。

    提姆

    【讨论】:

    • 如果有人需要调整大小/裁剪功能,我将此库与 UIImage+Resize 库集成。查看github.com/toptierlabs/ImageCacheResize
    • 如果您在 NSTemporaryDirectory 中与此并行写入信息,则此库有问题。还有其他解决方案吗?
    【解决方案4】:

    斯威夫特:

    extension UIImage {
    
        // Loads image asynchronously
        class func loadFromURL(url: NSURL, callback: (UIImage)->()) {
            dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), {
    
                let imageData = NSData(contentsOfURL: url)
                if let data = imageData {
                    dispatch_async(dispatch_get_main_queue(), {
                        if let image = UIImage(data: data) {
                            callback(image)
                        }
                    })
                }
            })
        }
    }
    

    用法:

    // First of all remove the old image (required for images in cells)
    imageView.image = nil 
    
    // Load image and apply to the view
    UIImage.loadFromURL("http://...", callback: { (image: UIImage) -> () in
         self.imageView.image = image
    })
    

    【讨论】:

    • 考虑处理失败案例
    • mm 令人困惑!添加的扩展 func 是 loadFromURL,并调用 loadAsync ?
    【解决方案5】:

    斯威夫特 4 |图片的异步加载

    创建一个名为 ImageLoader.swift 的新类

    import UIKit
    
    class ImageLoader {
    
    var cache = NSCache<AnyObject, AnyObject>()
    
    class var sharedInstance : ImageLoader {
        struct Static {
            static let instance : ImageLoader = ImageLoader()
        }
        return Static.instance
    }
    
    func imageForUrl(urlString: String, completionHandler:@escaping (_ image: UIImage?, _ url: String) -> ()) {
            let data: NSData? = self.cache.object(forKey: urlString as AnyObject) as? NSData
    
            if let imageData = data {
                let image = UIImage(data: imageData as Data)
                DispatchQueue.main.async {
                    completionHandler(image, urlString)
                }
                return
            }
    
        let downloadTask: URLSessionDataTask = URLSession.shared.dataTask(with: URL.init(string: urlString)!) { (data, response, error) in
            if error == nil {
                if data != nil {
                    let image = UIImage.init(data: data!)
                    self.cache.setObject(data! as AnyObject, forKey: urlString as AnyObject)
                    DispatchQueue.main.async {
                        completionHandler(image, urlString)
                    }
                }
            } else {
                completionHandler(nil, urlString)
            }
        }
        downloadTask.resume()
        }
    }
    

    在您的 ViewController 类中使用:

    ImageLoader.sharedInstance.imageForUrl(urlString: "https://www.logodesignlove.com/images/classic/apple-logo-rob-janoff-01.jpg", completionHandler: { (image, url) in
                    if image != nil {
                        self.imageView.image = image
                    }
                })
    

    【讨论】:

      【解决方案6】:

      是的,这相对容易。这个想法是这样的:

      1. 创建一个可变数组来保存您的图像
      2. 如果愿意,可以用占位符填充数组(如果不喜欢,可以使用 NSNull 对象)
      3. 创建一个在后台异步获取图像的方法
      4. 当图像到达时,将占位符与真实图像交换并执行[self.tableView reloadData]

      我已经对这种方法进行了多次测试,并给出了很好的结果。如果您需要异步部分的任何帮助/示例(我建议为此使用 gcd),请告诉我。

      【讨论】:

      • 如果图像存储在网络服务器上,当您只需要显示单元格的图像时,为什么还要花费资源下载所有图像?
      • 主要是出于缓存原因,但是对于大型集合,您只能获取显示所需的图像。我的意思是强调一般概念。
      • 我要补充一点,应该从主线程调用reloadData,以便几乎立即更新视图。感谢您的提示!
      【解决方案7】:

      考虑失败案例。

      - (void) loadFromURL: (NSURL*) url callback:(void (^)(UIImage *image))callback {
          dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0ul);
          dispatch_async(queue, ^{
              NSError * error = nil;
              NSData * imageData = [NSData dataWithContentsOfURL:url options:0 error:&error];
              if (error)
                  callback(nil);
      
              dispatch_async(dispatch_get_main_queue(), ^{
                  UIImage *image = [UIImage imageWithData:imageData];
                  callback(image);
              });
          });
      }
      

      【讨论】:

        【解决方案8】:

        如果您使用 Apple 为此目的提供的示例代码,您可以轻松完美地做到这一点: 示例代码: Lazy Image

        只需查看委托 rowforcell 并将 icondownloader 文件添加到您的项目。

        您需要做的唯一更改是使用您的对象更改 apprecord 对象。

        【讨论】:

          【解决方案9】:

          虽然 SDWebImage 和其他 3rd 方可能是很好的解决方案,但如果您不热衷于使用 3rd 方 API,您也可以开发自己的解决方案。

          请参阅此tutorial about lazy loading,其中还讨论了如何在表格视图中对数据进行建模。

          【讨论】:

            【解决方案10】:

            AsyncImage 可以同步加载和显示图片。在 iOS 15 之后正式引入

            cell.photo = AsyncImage(url: URL(string: entry.photo))
                .frame(width: 200, height: 200)
            

            它还支持:

            doc中查看更多信息

            【讨论】:

              【解决方案11】:

              你可能不得不继承你的 UIImageView。我最近做了一个简单的项目来解释这个特殊的任务——后台异步图像加载——看看 GitHub 上的at my project。具体看KDImageView类。

              【讨论】:

                【解决方案12】:

                使用下面的代码 sn-p 将图像加载到 imageview 中

                func imageDownloading() {
                
                DispatchQueue.global().async {
                
                    let url = URL(string: "http://verona-api.municipiumstaging.it/system/images/image/image/22/app_1920_1280_4.jpg")!
                
                    do {
                
                        let data = try Data(contentsOf: url)
                
                        DispatchQueue.main.async {
                
                        self.imageView.image = UIImage(data: data)
                
                        }
                
                    } catch {
                        print(error.localizedDescription)
                    }
                }
                }
                

                【讨论】:

                  猜你喜欢
                  • 1970-01-01
                  • 1970-01-01
                  • 2017-12-04
                  • 2016-01-16
                  • 1970-01-01
                  • 1970-01-01
                  • 2014-12-28
                  • 1970-01-01
                  • 1970-01-01
                  相关资源
                  最近更新 更多