能夠非同步載入圖片的UIImageview,通過呼叫方法loadImageWithUrl:與loadImageWithUrl:andDefaultImage:來進行非同步載入。用到了NSCache、檔案快取、NSOperation、NSQueue來完成。
首先是標頭檔案的定義
定義快取地址的巨集
#define kCIVCache [NSHomeDirectory() stringByAppendingString:@"/Library/Caches/CIVCache.txt"]
定義載入Operation介面
@protocol AsyImageLoadOperation <NSObject>
- (void)cancel;
@end
定義單例佇列Manager
@interface QueueManager : NSObject
+(id)sharedManager;
@property (nonatomic,retain) NSOperationQueue *queue;
@end
定義快取模型
@interface CIVImageCache : NSObject
@property (strong,nonatomic) NSURL *url;
@property (strong,nonatomic) NSData *imageData;
@end
定義圖片載入Operation
@interface ImageLoadOperation : NSOperation<AsyImageLoadOperation>
-(id)initWithUrl:(NSURL *)url;
@property (nonatomic,strong) NSURL *url;
@property (nonatomic,strong) CIVImageCache *resultCache;
@end
定義AsyImageView
@interface AsyImageView : UIImageView
-(void)loadImageWithUrl:(NSURL *)url;
-(void)loadImageWithUrl:(NSURL *)url andDefultImage:(UIImage *)image;
@end
接著就是實現標頭檔案,.m檔案中需要引入
#import "objc/runtime.h"
#include <sys/sysctl.h>
定義靜態的operationKey字元變數
static char operationKey;
QueueManager的實現
@implementation QueueManager
unsigned int countOfCores() {
unsigned int ncpu;
size_t len = sizeof(ncpu);
sysctlbyname("hw.ncpu", &ncpu, &len, NULL, 0);
return ncpu;
}
static QueueManager *instance;
+(id)sharedManager{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
instance=[[QueueManager alloc] init];
});
return instance;
}
-(id)init{
self=[super init];
if (self) {
_queue=[[NSOperationQueue alloc] init];
_queue.maxConcurrentOperationCount=countOfCores();
}
return self;
}
countOfCores方法用來獲取當前機器的CPU核數。在init初始化中初始化一個NSOperationQueue,併為這個佇列指定最大併發運算元(最好是CPU有多少核就設定為多少的最大併發數)。單例的實現使用GCD的dispatch_once。
實現快取模型物件
@implementation CIVImageCache
-(id)init{
self=[super init];
if (self){
_imageData=[[NSData alloc] init];
_url=[[NSURL alloc] init];
}
return self;
}
-(id)initWithCoder:(NSCoder *)aDecoder{
self=[super init];
if (self){
_imageData = [aDecoder decodeObjectForKey:@"_imageData"];
_url = [aDecoder decodeObjectForKey:@"_url"];
}
return self;
}
-(void)encodeWithCoder:(NSCoder *)aCoder{
[aCoder encodeObject:_imageData forKey:@"_imageData"];
[aCoder encodeObject:_url forKey:@"_url"];
}
-(id)copyWithZone:(NSZone *)zone{
CIVImageCache *uObject=[[[self class] allocWithZone:zone] init];
uObject.imageData=self.imageData;
uObject.url=self.url;
return uObject;
}
@end
這個沒有什麼好說的,這個物件主要用來在載入完成後傳遞資料以及將資料儲存在本地。
接下來實現圖片載入操作(ImageLoadOperation)
@implementation ImageLoadOperation
-(id)initWithUrl:(NSURL *)url{
self=[super init];
if (self) {
_url=url;
}
return self;
}
-(void)main{
NSData *cacheData=[NSData dataWithContentsOfFile:kCIVCache];
NSDictionary *cacheDic=[NSKeyedUnarchiver unarchiveObjectWithData:cacheData];
if (cacheDic!=nil) {
if ([[cacheDic allKeys] containsObject:_url.description]) {
CIVImageCache *cache=[[CIVImageCache alloc] init];
cache.url=_url;
cache.imageData=[cacheDic objectForKey:_url.description];
_resultCache=cache;
}else{
[self loadFromInternet];
}
}else{
[self loadFromInternet];
}
}
-(void)loadFromInternet{
NSData *imageData=[NSData dataWithContentsOfURL:_url];
UIImage *image=[UIImage imageWithData:imageData];
imageData = UIImageJPEGRepresentation(image, 0.0000001);
CIVImageCache *cache=[[CIVImageCache alloc] init];
cache.url=_url;
cache.imageData=imageData;
_resultCache=cache;
}
@end
main函式中為主要的載入操作,首先從本地快取中獲取資料,判斷是否已經存在URL的請求快取,如果沒有呼叫loadFromInternet方法從網路下載圖片。
最後來實現非同步
@interface AsyImageView ()
@property (nonatomic,weak) NSOperation *lastOperation;
@property (strong, nonatomic) NSCache *memCache;
@property (assign, nonatomic) dispatch_queue_t ioQueue;
@end
定義AsyImageView的“私有”變數,lastOperation用來關聯operation的順序(這個最後好像沒有用到)memCache則用來進行快取,ioQueue是用來快取檔案的操作佇列。
AsyImageView使用兩種初始化方法來初始化:
-(id)init{
self=[super init];
if (self) {
_ioQueue = dispatch_queue_create("com.noez.AsyImageCache", DISPATCH_QUEUE_SERIAL);
NSString *fullNamespace = [@"com.noez.AsyImageCache." stringByAppendingString:@"1"];
_memCache = [[NSCache alloc] init];
_memCache.name = fullNamespace;
}
return self;
}
-(id)initWithFrame:(CGRect)frame{
self=[super initWithFrame:frame];
if (self) {
_ioQueue = dispatch_queue_create("com.noez.AsyImageCache", DISPATCH_QUEUE_SERIAL);
NSString *fullNamespace = [@"com.noez.AsyImageCache." stringByAppendingString:@"1"];
_memCache = [[NSCache alloc] init];
_memCache.name = fullNamespace;
}
return self;
}
在AsyImageView中的layoutSubviews中也需要初始化ioQueue與memCache,如果是直接在XIB中直接設定AsyImageView的話並不會呼叫兩個初始化方法。
loadImageWithUrl方法中開始非同步的載入過程:
-(void)loadImageWithUrl:(NSURL *)url{
[self cancelCurrentImageLoad];
NSOperationQueue *queue=[[QueueManager sharedManager] queue];
__weak AsyImageView *weakSelf=self;
id<AsyImageLoadOperation> operation=[self downloadWithURL:url queue:queue completed:^(UIImage *image, NSData *data, NSError *error, BOOL isFinished) {
void (^block)(void) = ^{
__strong AsyImageView *sself = weakSelf;
if (!sself) return;
if (image){
sself.image = image;
[sself setNeedsLayout];
}
};
if ([NSThread isMainThread]){
block();
}
else{
dispatch_sync(dispatch_get_main_queue(), block);
}
}];
objc_setAssociatedObject(self, &operationKey, operation, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
在這個方法中首先呼叫cancelCurrentImageLoad來取消當前的載入操作:
- (void)cancelCurrentImageLoad
{
// Cancel in progress downloader from queue
id<AsyImageLoadOperation> operation = objc_getAssociatedObject(self, &operationKey);
if (operation)
{
[operation cancel];
objc_setAssociatedObject(self, &operationKey, nil, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
}
之後獲取單例的操作佇列後呼叫downloadWithURL方法開始非同步載入,載入完之後通過block將圖片賦值給image屬性。
-(id<AsyImageLoadOperation>)downloadWithURL:(NSURL *)url queue:(NSOperationQueue *)queue completed:(void (^)(UIImage *image, NSData *data, NSError *error, BOOL isFinished))completedBlock{
ImageLoadOperation *op=[[ImageLoadOperation alloc] init];
[self queryDiskCacheForKey:url.description done:^(UIImage *image) {
if (image==nil) {
op.url=url;
__weak ImageLoadOperation *weakOp=op;
op.completionBlock=^{
CIVImageCache *cache=weakOp.resultCache;
UIImage *dimage=[UIImage imageWithData:cache.imageData];
completedBlock(dimage,nil,nil,YES);
[self storeImage:dimage imageData:cache.imageData forKey:cache.url.description toDisk:YES];
};
[self.lastOperation addDependency:op];//待定
self.lastOperation=op;
[queue addOperation:op];
}else{
completedBlock(image,nil,nil,YES);
}
}];
return op;
}
在載入前首先呼叫queryDiskCacheForKey方法從快取中獲取圖片,如果快取中沒有圖片,則使用圖片載入操作載入圖片,在操作完成時使用block儲存圖片並呼叫completedBlock顯示圖片。如果快取中有圖片則直接呼叫completedBlock顯示圖片。一下分別是儲存圖片與從快取獲取圖片的方法:
- (void)storeImage:(UIImage *)image imageData:(NSData *)imageData forKey:(NSString *)key toDisk:(BOOL)toDisk{
if (!image || !key){
return;
}
[self.memCache setObject:image forKey:key cost:image.size.height * image.size.width * image.scale];
if (toDisk){
dispatch_async(self.ioQueue, ^{
NSData *data = imageData;
if (!data){
if (image){
data = UIImageJPEGRepresentation(image, (CGFloat)1.0);
}
}
if (data){
NSData *cacheData=[NSData dataWithContentsOfFile:kCIVCache];
if (cacheData==nil) {
cacheData=[[NSData alloc] init];
}
NSMutableDictionary *dic=[NSKeyedUnarchiver unarchiveObjectWithData:cacheData];
if (dic==nil) {
dic=[[NSMutableDictionary alloc] init];
}
if (![[dic allKeys] containsObject:key]) {
[dic setObject:data forKey:key];
}
NSData *data=[NSKeyedArchiver archivedDataWithRootObject:dic];
[data writeToFile:kCIVCache atomically:YES];
}
});
}
}
- (void)queryDiskCacheForKey:(NSString *)key done:(void (^)(UIImage *image))doneBlock{
if (!doneBlock) return;
if (!key){
doneBlock(nil);
return;
}
UIImage *image = [self imageFromMemoryCacheForKey:key];
if (image){
doneBlock(image);
return;
}
if (_ioQueue==nil) {
_ioQueue = dispatch_queue_create("com.noez.AsyImageCache", DISPATCH_QUEUE_SERIAL);
}
dispatch_async(self.ioQueue, ^{
@autoreleasepool{
UIImage *diskImage = [self diskImageForKey:key];
if (diskImage){
CGFloat cost = diskImage.size.height * diskImage.size.width * diskImage.scale;
[self.memCache setObject:diskImage forKey:key cost:cost];
}
dispatch_async(dispatch_get_main_queue(), ^{
doneBlock(diskImage);
});
}
});
}