将图像异步加载到UIImage中

38

我正在使用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 被频繁调用。

12个回答

81

我编写了一个自定义类,使用块和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;
    }

    }];
}

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

*** 注意 - 此示例假定正在使用ARC。如果没有,则需要管理对象的自己释放。


1
只需将imagedata添加到数组中,并在异步加载之前检查数组中是否存在imagedata。如果需要,我会为您准备好一些东西(但是稍后才能完成)。 - LJ Wilson
4
这是对所提出问题的一个很好的回答。它应该再获得至少几票支持。 - Mark
6
顺便提一下,在完成块中,您不应只更新单元格!您无法保证该单元格是否随后滚动到屏幕外,并因此被出列并重用于表的另一行(如果您正在快速滚动或网络连接缓慢,则非常重要)。您应该调用tableview的“cellForRowAtIndexPath”(请注意,这与表视图控制器的同名方法不同),并确保它不为 nil。因此CustomCell *updateCell = [tableView cellForRowAtIndexPath:indexPath]; if (updateCell) updateCell.photo.image = ...; - Rob
2
dispatch_get_current_queue在iOS 6中已被弃用。 - bbrame
我现在正在使用AFNetworking的异步加载图像方法。它具有打开/关闭缓存选项,并且速度非常快。由于我已经在使用AFNetworking,采用setImageWithURL方法是一个明智的选择。 - LJ Wilson
显示剩余7条评论

52
在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

一个分类是真正的选择。 - Johan Karlsson
我该如何调用类中的方法?UIImage *img = [UIImage loadFromURL:[NSURL URLWithString:@""] callback:<#^(UIImage *image)callback#>]; 我应该在回调函数中放什么? - Pinturikkio
@bbrame请给我一个答案! - Pinturikkio
想同时加载两张图片,为什么需要这么长时间? - iamVishal16
1
@Pinturikkio ... 一个示例实现如下:[UIImage loadFromURL:_myNSURL callback:^(UIImage *image){ _myImageView.image = image; _mySpinner.alpha = 0.0; [_mySpinner stopAnimating];}]; - Chris Allinson

28

3
如果有人需要改变大小/裁剪功能,我已将此库与 UIImage+Resize 库集成。请在 https://github.com/toptierlabs/ImageCacheResize 上查看。 - Tony
如果您在NSTemporaryDirectory并行写入信息,则此库存在问题。还有其他解决方案吗? - jose920405

6

Swift:

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
})

2
考虑处理失败情况。 - Mabedan
这有点混淆!!添加了扩展函数loadFromURL,并调用loadAsync? - Alupotha

5

Swift 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
                }
            })

2

是的,这相对容易。思路如下:

  1. 创建一个可变数组来存储你的图片。
  2. 如果需要,可以使用占位符(或者NSNull对象)填充数组。
  3. 创建一个方法在后台异步获取你的图片。
  4. 当一张图片到达时,用真实的图片替换占位符,并执行[self.tableView reloadData]

我已经多次测试了这种方法,取得了很好的效果。如果你需要异步部分的帮助/示例(我建议使用GCD),请告诉我。


如果图像存储在Web服务器上,为什么要花费资源下载所有图像,当你只需要显示单元格的图像呢? - LJ Wilson
主要是为了缓存的原因,但是对于大量数据集,您可以仅获取需要显示的图像。我的观点是强调一般概念。 - Alladinian
我想要补充一点,reloadData 应该在主线程中调用,这样视图几乎会立即更新。谢谢你的提示! - Jonathan Lin

2
考虑失败案例。
- (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);
        });
    });
}

1

虽然SDWebImage和其他第三方工具可能是很好的解决方案,但如果您不想使用第三方API,您也可以开发自己的解决方案。

参考这个关于懒加载的教程,它还讲述了如何在表格视图中建模数据的方法。


1
如果您使用苹果提供的示例代码,即Lazy Image,您可以轻松地完美执行此操作。
只需查看delegate rowforcell并将icondownloader文件添加到您的项目中即可。
您唯一需要做的更改是将apprecord对象更改为您的对象。

0

目前仅适用于SwiftUI。 - Omar N Shamali

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接