【问题标题】:How to cache images in Objective-C如何在 Objective-C 中缓存图像
【发布时间】:2021-02-20 17:25:27
【问题描述】:

我想创建一个方法来缓存来自 URL 的图像,我在 Swift 中获得了代码,因为我以前使用过它,我该如何在 Objective 中做类似的事情-C:

import UIKit

let imageCache: NSCache = NSCache<AnyObject, AnyObject>()

extension UIImageView {
    
    func loadImageUsingCacheWithUrlString(urlString: String) {
        
        self.image = nil
        
        if let cachedImage = imageCache.object(forKey: urlString as AnyObject) as? UIImage {
            self.image = cachedImage
            return
        }
        
        let url = URL(string: urlString)
        if let data = try? Data(contentsOf: url!) {
            
            DispatchQueue.main.async(execute: {
                
                if let downloadedImage = UIImage(data: data) {
                    imageCache.setObject(downloadedImage, forKey: urlString as AnyObject)
                    
                    self.image = downloadedImage
                }
            })
        }
    }
    
}

【问题讨论】:

  • 使用第三方库来完成这项工作。那里有许多高质量的开源项目。

标签: objective-c url caching uiimageview nscache


【解决方案1】:

在转换之前,您可能会考虑重构以使其异步:

  1. 永远不应该将Data(contentsOf:) 用于网络请求,因为 (a) 它是同步的并且会阻塞调用者(这是一种可怕的用户体验,而且在退化的情况下,可能会导致看门狗进程杀死您的应用程序); (b) 如果有问题,则没有诊断信息; (c) 不可取消。

  2. 完成后不要更新image 属性,您应该考虑完成处理程序模式,以便调用者知道何时完成请求并处理图像。这种模式避免了竞争条件,让您有并发的图像请求。

  3. 当您使用这种异步模式时,URLSession 在后台队列上运行其完成处理程序。您应该在此后台队列上继续处理图像和更新缓存。只有完成处理程序应该被分派回主队列。

  4. 我从您的回答中推断,您的意图是在 UIImageView 扩展中使用此代码。您确实应该将此代码放在一个单独的对象中(我创建了一个ImageManager 单例),以便此缓存不仅可用于图像视图,而且可用于您可能需要图像的任何地方。例如,您可以对UIImageView 之外的图像进行一些预取。如果这段代码被埋在了

因此,可能是这样的:

final class ImageManager {
    static let shared = ImageManager()

    enum ImageFetchError: Error {
        case invalidURL
        case networkError(Data?, URLResponse?)
    }

    private let imageCache = NSCache<NSString, UIImage>()

    private init() { }

    @discardableResult
    func fetchImage(urlString: String, completion: @escaping (Result<UIImage, Error>) -> Void) -> URLSessionTask? {
        if let cachedImage = imageCache.object(forKey: urlString as NSString) {
            completion(.success(cachedImage))
            return nil
        }

        guard let url = URL(string: urlString) else {
            completion(.failure(ImageFetchError.invalidURL))
            return nil
        }

        let task = URLSession.shared.dataTask(with: url) { data, response, error in
            guard
                error == nil,
                let responseData = data,
                let httpUrlResponse = response as? HTTPURLResponse,
                200 ..< 300 ~= httpUrlResponse.statusCode,
                let image = UIImage(data: responseData)
            else {
                DispatchQueue.main.async {
                    completion(.failure(error ?? ImageFetchError.networkError(data, response)))
                }
                return
            }

            self.imageCache.setObject(image, forKey: urlString as NSString)
            DispatchQueue.main.async {
                completion(.success(image))
            }
        }

        task.resume()

        return task
    }

}

你会这样称呼它:

ImageManager.shared.fetchImage(urlString: someUrl) { result in
    switch result {
    case .failure(let error): print(error)
    case .success(let image): // do something with image
    }
}

// but do not try to use `image` here, as it has not been fetched yet

如果您想在UIImageView 扩展中使用它,例如,您可以保存URLSessionTask,这样如果您在前一张完成之前请求另一张图片,您可以取消它。 (例如,如果在表格视图中使用它并且用户滚动速度非常快,这是一种非常常见的情况。您不想积压大量网络请求。)我们可以

extension UIImageView {
    private static var taskKey = 0
    private static var urlKey = 0

    private var currentTask: URLSessionTask? {
        get { objc_getAssociatedObject(self, &Self.taskKey) as? URLSessionTask }
        set { objc_setAssociatedObject(self, &Self.taskKey, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) }
    }

    private var currentURLString: String? {
        get { objc_getAssociatedObject(self, &Self.urlKey) as? String }
        set { objc_setAssociatedObject(self, &Self.urlKey, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) }
    }

    func setImage(with urlString: String) {
        if let oldTask = currentTask {
            currentTask = nil
            oldTask.cancel()
        }

        image = nil

        currentURLString = urlString

        let task = ImageManager.shared.fetchImage(urlString: urlString) { result in
            // only reset if the current value is for this url

            if urlString == self.currentURLString {
                self.currentTask = nil
                self.currentURLString = nil
            }

            // now use the image

            if case .success(let image) = result {
                self.image = image
            }
        }

        currentTask = task
    }
}

你可以在这个UIImageView 扩展中做很多其他事情(例如占位符图像等),但是通过将UIImageView 扩展与网络层分离,可以将这些不同的任务保留在各自的类中(本着单一责任原则的精神)。


好的,说完这些,让我们看看 Objective-C 的再现。例如,您可以创建一个 ImageManager 单例:

//  ImageManager.h

@import UIKit;

NS_ASSUME_NONNULL_BEGIN

typedef NS_ENUM(NSUInteger, ImageManagerError) {
    ImageManagerErrorInvalidURL,
    ImageManagerErrorNetworkError,
    ImageManagerErrorNotValidImage
};

@interface ImageManager : NSObject

// if you make this singleton, mark normal instantiation methods as unavailable ...

+ (instancetype)alloc __attribute__((unavailable("alloc not available, call sharedImageManager instead")));
- (instancetype)init __attribute__((unavailable("init not available, call sharedImageManager instead")));
+ (instancetype)new __attribute__((unavailable("new not available, call sharedImageManager instead")));
- (instancetype)copy __attribute__((unavailable("copy not available, call sharedImageManager instead")));

// ... and expose singleton access point

@property (class, nonnull, readonly, strong) ImageManager *sharedImageManager;

// provide fetch method

- (NSURLSessionTask * _Nullable)fetchImageWithURLString:(NSString *)urlString completion:(void (^)(UIImage * _Nullable image, NSError * _Nullable error))completion;

@end

NS_ASSUME_NONNULL_END

然后实现这个单例:

//  ImageManager.m

#import "ImageManager.h"

@interface ImageManager()
@property (nonatomic, strong) NSCache<NSString *, UIImage *> *imageCache;
@end

@implementation ImageManager

+ (instancetype)sharedImageManager {
    static dispatch_once_t onceToken;
    static ImageManager *shared;
    dispatch_once(&onceToken, ^{
        shared = [[self alloc] initPrivate];
    });
    return shared;
}

- (instancetype)initPrivate
{
    self = [super init];
    if (self) {
        _imageCache = [[NSCache alloc] init];
    }
    return self;
}

- (NSURLSessionTask *)fetchImageWithURLString:(NSString *)urlString completion:(void (^)(UIImage *image, NSError *error))completion {
    UIImage *cachedImage = [self.imageCache objectForKey:urlString];
    if (cachedImage) {
        completion(cachedImage, nil);
        return nil;
    }

    NSURL *url = [NSURL URLWithString:urlString];
    if (!url) {
        NSError *error = [NSError errorWithDomain:[[NSBundle mainBundle] bundleIdentifier] code:ImageManagerErrorInvalidURL userInfo:nil];
        completion(nil, error);
        return nil;
    }

    NSURLSessionTask *task = [NSURLSession.sharedSession dataTaskWithURL:url completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
        if (error) {
            dispatch_async(dispatch_get_main_queue(), ^{
                completion(nil, error);
            });
            return;
        }

        if (!data) {
            NSError *error = [NSError errorWithDomain:[[NSBundle mainBundle] bundleIdentifier] code:ImageManagerErrorNetworkError userInfo:nil];
            dispatch_async(dispatch_get_main_queue(), ^{
                completion(nil, error);
            });
        }

        UIImage *image = [UIImage imageWithData:data];
        if (!image) {
            NSDictionary *userInfo = @{
                @"data": data,
                @"response": response ? response : [NSNull null]
            };
            NSError *error = [NSError errorWithDomain:[[NSBundle mainBundle] bundleIdentifier] code:ImageManagerErrorNotValidImage userInfo:userInfo];
            dispatch_async(dispatch_get_main_queue(), ^{
                completion(nil, error);
            });
        }

        [self.imageCache setObject:image forKey:urlString];
        dispatch_async(dispatch_get_main_queue(), ^{
            completion(image, nil);
        });
    }];

    [task resume];

    return task;
}

@end

你会这样称呼它:

[[ImageManager sharedImageManager] fetchImageWithURLString:urlString completion:^(UIImage * _Nullable image, NSError * _Nullable error) {
    if (error) {
        NSLog(@"%@", error);
        return;
    }

    // do something with `image` here ...
}];

// but not here, because the above runs asynchronously

同样,您可以在 UIImageView 扩展中使用它:

#import <objc/runtime.h>

@implementation UIImageView (Cache)

- (void)setImage:(NSString *)urlString
{
    NSURLSessionTask *oldTask = objc_getAssociatedObject(self, &taskKey);
    if (oldTask) {
        objc_setAssociatedObject(self, &taskKey, nil, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
        [oldTask cancel];
    }

    image = nil

    objc_setAssociatedObject(self, &urlKey, urlString, OBJC_ASSOCIATION_RETAIN_NONATOMIC);

    NSURLSessionTask *task = [[ImageManager sharedImageManager] fetchImageWithURLString:urlString completion:^(UIImage * _Nullable image, NSError * _Nullable error) {
        NSString *currentURL = objc_getAssociatedObject(self, &urlKey);
        if ([currentURL isEqualToString:urlString]) {
            objc_setAssociatedObject(self, &urlKey, nil, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
            objc_setAssociatedObject(self, &taskKey, nil, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
        }

        if (image) {
            self.image = image;
        }
    }];

    objc_setAssociatedObject(self, &taskKey, task, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
@end

【讨论】:

    【解决方案2】:

    经过反复试验,这行得通:

    #import "UIImageView+Cache.h"
    
    @implementation UIImageView (Cache)
    
    NSCache* imageCache;
    
    - (void)loadImageUsingCacheWithUrlString:(NSString*)urlString {
        
        imageCache = [[NSCache alloc] init];
        
        self.image = nil;
        
        UIImage *cachedImage = [imageCache objectForKey:(id)urlString];
        
        if (cachedImage != nil) {
            self.image = cachedImage;
            return;
        }
        
        NSURL *url = [NSURL URLWithString:urlString];
        
        NSData *data = [NSData dataWithContentsOfURL:url];
        
        if (data != nil) {
            dispatch_async(dispatch_get_main_queue(), ^{
                
                UIImage *downloadedImage = [UIImage imageWithData:data];
                
                if (downloadedImage != nil) {
                    [imageCache setObject:downloadedImage forKey:urlString];
                    self.image = downloadedImage;
                }
            });
        }
    }
    
    @end
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2013-11-24
      • 1970-01-01
      • 2017-09-24
      • 2018-06-17
      • 2012-03-08
      • 2015-12-11
      相关资源
      最近更新 更多