總覽
UIImagView+AFNetworking是我們常用的一個(gè)擴(kuò)展,用它一行代碼就可以實(shí)現(xiàn)圖片的加載和自動(dòng)緩存,非常的方便。下面我們來(lái)看下它是怎么實(shí)現(xiàn)的:
首先來(lái)看它的類圖:

其主要由以上幾個(gè)類和protocol組成。下面我們大致說(shuō)一下各個(gè)類的作用:
- UIimageView+AFNetworking
這個(gè)類是我們使用的擴(kuò)展類,里面主要是調(diào)度了圖片的加載相關(guān)操作,比如查找緩存以及下載圖片。 - AFImageDownloader
這個(gè)類是真正的下載管理類,下載使用AFHTTPSessionManager來(lái)進(jìn)行。每個(gè)下載以task的方式運(yùn)行。 - AFAutoPurgingImageCache
這個(gè)類是緩存類,里面管理了整個(gè)緩存內(nèi)容。 - AFCachedImage
這個(gè)類是圖片數(shù)據(jù)類,對(duì)圖片數(shù)據(jù)和圖片標(biāo)示進(jìn)行包裝。 - AFImageDownloadReceipt
這個(gè)類主要用來(lái)做任務(wù)的取消。 - AFImageDownloaderMergedTask
這個(gè)類是下載任務(wù)類,封裝了任務(wù)相關(guān)信息。 - AFImageDownloaderResponseHandler
這個(gè)類回調(diào)類,主要封裝了一個(gè)成功和一個(gè)失敗回調(diào)。
執(zhí)行過(guò)程
1.首先在UIImageView+AFNetworking中,利用setImage系列方法來(lái)加載圖片:
- (void)setImageWithURL:(NSURL *)url;
- (void)setImageWithURL:(NSURL *)url
placeholderImage:(nullable UIImage *)placeholderImage;
- (void)setImageWithURLRequest:(NSURLRequest *)urlRequest
placeholderImage:(nullable UIImage *)placeholderImage
success:(nullable void (^)(NSURLRequest *request, NSHTTPURLResponse * _Nullable response, UIImage *image))success
failure:(nullable void (^)(NSURLRequest *request, NSHTTPURLResponse * _Nullable response, NSError *error))failure;
前兩個(gè)方法最終其實(shí)是調(diào)用第三個(gè)方法,所以我們直接看第三個(gè)方法。這個(gè)方法首先會(huì)做一些必要的檢查工作,之后會(huì)去調(diào)用AFImageRequestCache類查找本地是否已經(jīng)有了此緩存。如果找到直接返回,如果找不到,就去服務(wù)端請(qǐng)求圖片。
2.緩存是存在AFImageDownloader中的imageChche屬性里面。找緩存API為:
- (nullable UIImage *)imageforRequest:(NSURLRequest *)request withAdditionalIdentifier:(nullable NSString *)identifier;
在這個(gè)方法里面,會(huì)根據(jù)真正存儲(chǔ)圖片的類AFCachedImage里面的identifier來(lái)確定是否是需要的圖片,identifier是由url加一個(gè)additionalIdentifier組成。其拼接為一個(gè)完成key的方法為:
- (NSString *)imageCacheKeyFromURLRequest:(NSURLRequest *)request withAdditionalIdentifier:(NSString *)additionalIdentifier {
NSString *key = request.URL.absoluteString;
if (additionalIdentifier != nil) {
key = [key stringByAppendingString:additionalIdentifier];
}
return key;
}
3.如果沒(méi)找到緩存,就會(huì)去服務(wù)端下載圖片。在AFImageDownloader中有兩個(gè)方法提供下載功能:
- (nullable AFImageDownloadReceipt *)downloadImageForURLRequest:(NSURLRequest *)request
success:(nullable void (^)(NSURLRequest *request, NSHTTPURLResponse * _Nullable response, UIImage *responseObject))success
failure:(nullable void (^)(NSURLRequest *request, NSHTTPURLResponse * _Nullable response, NSError *error))failure;
- (nullable AFImageDownloadReceipt *)downloadImageForURLRequest:(NSURLRequest *)request
withReceiptID:(NSUUID *)receiptID
success:(nullable void (^)(NSURLRequest *request, NSHTTPURLResponse * _Nullable response, UIImage *responseObject))success
failure:(nullable void (^)(NSURLRequest *request, NSHTTPURLResponse * _Nullable response, NSError *error))failure;
第一個(gè)方法會(huì)去調(diào)第二個(gè)方法。在這個(gè)方法中,首先會(huì)去做一些基礎(chǔ)的檢查工作,之后會(huì)去檢查要請(qǐng)求的圖片是否已在任務(wù)中,所有任務(wù)都放在AFImageDownloader的mergedTasks字典屬性中,key是url。如果發(fā)現(xiàn)任務(wù)已存在,就在任務(wù)中多添加一個(gè)回調(diào),回調(diào)放在AFImageDownloaderResponseHandler類中。如果沒(méi)有找到則根據(jù)初始設(shè)定的緩存規(guī)則來(lái)確定是不是需要讀取緩存。
// 2) Attempt to load the image from the image cache if the cache policy allows it
switch (request.cachePolicy) {
case NSURLRequestUseProtocolCachePolicy:
case NSURLRequestReturnCacheDataElseLoad:
case NSURLRequestReturnCacheDataDontLoad: {
UIImage *cachedImage = [self.imageCache imageforRequest:request withAdditionalIdentifier:nil];
if (cachedImage != nil) {
if (success) {
dispatch_async(dispatch_get_main_queue(), ^{
success(request, nil, cachedImage);
});
}
return;
}
break;
}
default:
break;
}
這里一共有三種緩存策略可以使用緩存:
- NSURLRequestUseProtocolCachePolicy。 對(duì)特定的 URL 請(qǐng)求使用網(wǎng)絡(luò)協(xié)議中實(shí)現(xiàn)的緩存邏輯
- NSURLRequestReturnCacheDataElseLoad。無(wú)論緩存是否過(guò)期,先使用本地緩存數(shù)據(jù)。如果緩存中沒(méi)有請(qǐng)求所對(duì)應(yīng)的數(shù)據(jù),那么從原始地址加載數(shù)據(jù)。
- NSURLRequestReturnCacheDataDontLoad。 無(wú)論緩存是否過(guò)期,先使用本地緩存數(shù)據(jù)。如果緩存中沒(méi)有請(qǐng)求所對(duì)應(yīng)的數(shù)據(jù),那么放棄從原始地址加載數(shù)據(jù),請(qǐng)求視為失敗。
如果不允許使用緩存或者沒(méi)找到緩存,就要走正常的請(qǐng)求流程,真正的去請(qǐng)求數(shù)據(jù)了。請(qǐng)求到的數(shù)據(jù)好會(huì)添加到緩存中,以供下次使用。
請(qǐng)求及緩存處理中的多線程
首先來(lái)看請(qǐng)求相關(guān)部分:
NSString *name = [NSString stringWithFormat:@"com.alamofire.imagedownloader.synchronizationqueue-%@", [[NSUUID UUID] UUIDString]];
self.synchronizationQueue = dispatch_queue_create([name cStringUsingEncoding:NSASCIIStringEncoding], DISPATCH_QUEUE_SERIAL);
name = [NSString stringWithFormat:@"com.alamofire.imagedownloader.responsequeue-%@", [[NSUUID UUID] UUIDString]];
self.responseQueue = dispatch_queue_create([name cStringUsingEncoding:NSASCIIStringEncoding], DISPATCH_QUEUE_CONCURRENT);
請(qǐng)求隊(duì)列synchronizationQueue是一個(gè)串行隊(duì)列,這是因?yàn)檎?qǐng)求處理需要做一些任務(wù)檢查和添加操作,而這些操作都是屬于共享的資源(這里的共享資源主要指activeRequestCount、mergedTasks等)。如果使用并行隊(duì)列的話,需要對(duì)每個(gè)共享資源加鎖,而這里的幾個(gè)共享資源都是邏輯上相關(guān)的,所以很容易造成死鎖,所以這里使用串行隊(duì)列可以省很多麻煩。 關(guān)于使用串行隊(duì)列來(lái)避免加鎖我們可以舉一個(gè)例子來(lái)看一下:
- (void)safelyDecrementActiveTaskCount {
dispatch_sync(self.synchronizationQueue, ^{
if (self.activeRequestCount > 0) {
self.activeRequestCount -= 1;
}
});
}
這里將活躍請(qǐng)求的數(shù)量的遞減放到了串行隊(duì)列中,如果想通過(guò)隊(duì)列顯示數(shù)量的增減,那數(shù)量的增加必然也要放到同一個(gè)串行隊(duì)列中,才可以在不用鎖的情況下保證資源的正確性。所以有了下面的代碼:
- (void)safelyStartNextTaskIfNecessary {
dispatch_sync(self.synchronizationQueue, ^{
if ([self isActiveRequestCountBelowMaximumLimit]) {
while (self.queuedMergedTasks.count > 0) {
AFImageDownloaderMergedTask *mergedTask = [self dequeueMergedTask];
if (mergedTask.task.state == NSURLSessionTaskStateSuspended) {
[self startMergedTask:mergedTask];
break;
}
}
}
});
}
這里,資源的增加同樣放到了synchronizationQueue隊(duì)列中執(zhí)行。這樣就保證了共享資源的安全性。
返回隊(duì)列responseQueue是一個(gè)并行隊(duì)列。這是由于返回隊(duì)列的一個(gè)重要作用就是快速分發(fā)返回結(jié)果,而這些并沒(méi)有對(duì)共享資源做出修改,這樣可以保證最快速的分發(fā)。
除了請(qǐng)求相關(guān)內(nèi)容,在緩存管理上,也有一些值得說(shuō)道的地方:
NSString *queueName = [NSString stringWithFormat:@"com.alamofire.autopurgingimagecache-%@", [[NSUUID UUID] UUIDString]];
self.synchronizationQueue = dispatch_queue_create([queueName cStringUsingEncoding:NSASCIIStringEncoding], DISPATCH_QUEUE_CONCURRENT);
這里緩存的管理是創(chuàng)建了一個(gè)并行隊(duì)列synchronizationQueue。緩存的讀取用了這樣 一個(gè)方法:
- (nullable UIImage *)imageWithIdentifier:(NSString *)identifier {
__block UIImage *image = nil;
dispatch_sync(self.synchronizationQueue, ^{
AFCachedImage *cachedImage = self.cachedImages[identifier];
image = [cachedImage accessImage];
});
return image;
}
緩存的增刪代碼如下:
- (void)addImage:(UIImage *)image withIdentifier:(NSString *)identifier {
dispatch_barrier_async(self.synchronizationQueue, ^{
AFCachedImage *cacheImage = [[AFCachedImage alloc] initWithImage:image identifier:identifier];
AFCachedImage *previousCachedImage = self.cachedImages[identifier];
if (previousCachedImage != nil) {
self.currentMemoryUsage -= previousCachedImage.totalBytes;
}
self.cachedImages[identifier] = cacheImage;
self.currentMemoryUsage += cacheImage.totalBytes;
});
dispatch_barrier_async(self.synchronizationQueue, ^{
if (self.currentMemoryUsage > self.memoryCapacity) {
UInt64 bytesToPurge = self.currentMemoryUsage - self.preferredMemoryUsageAfterPurge;
NSMutableArray <AFCachedImage*> *sortedImages = [NSMutableArray arrayWithArray:self.cachedImages.allValues];
NSSortDescriptor *sortDescriptor = [[NSSortDescriptor alloc] initWithKey:@"lastAccessDate"
ascending:YES];
[sortedImages sortUsingDescriptors:@[sortDescriptor]];
UInt64 bytesPurged = 0;
for (AFCachedImage *cachedImage in sortedImages) {
[self.cachedImages removeObjectForKey:cachedImage.identifier];
bytesPurged += cachedImage.totalBytes;
if (bytesPurged >= bytesToPurge) {
break ;
}
}
self.currentMemoryUsage -= bytesPurged;
}
});
}
- (BOOL)removeImageWithIdentifier:(NSString *)identifier {
__block BOOL removed = NO;
dispatch_barrier_sync(self.synchronizationQueue, ^{
AFCachedImage *cachedImage = self.cachedImages[identifier];
if (cachedImage != nil) {
[self.cachedImages removeObjectForKey:identifier];
self.currentMemoryUsage -= cachedImage.totalBytes;
removed = YES;
}
});
return removed;
}
通過(guò)上面的一系列方法可以看到寫(xiě)入用了dispatch_barrier_sync和dispatch_barrier_async來(lái)保證執(zhí)行的一致性。對(duì)于讀取,使用了dispatch_sync來(lái)實(shí)現(xiàn)滿足同時(shí)支持多個(gè)讀取操作,也就是單一資源的多讀單寫(xiě)(具體在這兒有介紹)。我想這也是cache操作使用并行而不是串行隊(duì)列的原因。
參考
iOS - 關(guān)于NSURLCache
NSURLRequestCachePolicy—iOS緩存策略
底層并發(fā) API
AFNetworking之UIKit擴(kuò)展與緩存實(shí)現(xiàn)