如何在 Objective-C 中缓存图像
How to cache images in Objective-C
我想创建一个方法来缓存来自 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
}
})
}
}
}
经过反复试验,这成功了:
#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
在转换之前,您可以考虑重构使其异步:
永远不要使用 Data(contentsOf:)
进行网络请求,因为 (a) 它是同步的并且会阻塞调用者(这是一个糟糕的用户体验,而且在退化的情况下,会导致看门狗进程杀死你的应用程序); (b) 如果有问题,没有诊断信息; (c) 不可取消。
而不是在完成后更新 image
属性,您应该考虑完成处理程序模式,以便调用者知道请求何时完成以及图像何时被处理。此模式避免了竞争条件,并允许您进行并发图像请求。
当您使用此异步模式时,URLSession
在后台队列上运行其完成处理程序。您应该将图像处理和缓存更新保留在此后台队列中。只应将完成处理程序分派回主队列。
我从您的回答中推断,您的意图是在 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
,这样如果您在前一个图像完成之前请求另一个图像,您可以取消它。 (例如,如果在 table 视图中使用它并且用户滚动非常快,这是一个非常常见的场景。您不希望在大量网络请求中积压。)我们可以
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
扩展与网络层分开,可以保持这些不同各自的任务类(本着单一责任原则的精神)。
OK,说完这些,让我们看看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
我想创建一个方法来缓存来自 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
}
})
}
}
}
经过反复试验,这成功了:
#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
在转换之前,您可以考虑重构使其异步:
永远不要使用
Data(contentsOf:)
进行网络请求,因为 (a) 它是同步的并且会阻塞调用者(这是一个糟糕的用户体验,而且在退化的情况下,会导致看门狗进程杀死你的应用程序); (b) 如果有问题,没有诊断信息; (c) 不可取消。而不是在完成后更新
image
属性,您应该考虑完成处理程序模式,以便调用者知道请求何时完成以及图像何时被处理。此模式避免了竞争条件,并允许您进行并发图像请求。当您使用此异步模式时,
URLSession
在后台队列上运行其完成处理程序。您应该将图像处理和缓存更新保留在此后台队列中。只应将完成处理程序分派回主队列。我从您的回答中推断,您的意图是在
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
,这样如果您在前一个图像完成之前请求另一个图像,您可以取消它。 (例如,如果在 table 视图中使用它并且用户滚动非常快,这是一个非常常见的场景。您不希望在大量网络请求中积压。)我们可以
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
扩展与网络层分开,可以保持这些不同各自的任务类(本着单一责任原则的精神)。
OK,说完这些,让我们看看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