UIImagView+AFNetworking實(shí)現(xiàn)分析

總覽

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

首先來(lái)看它的類圖:

class.png

其主要由以上幾個(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)

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • Spring Cloud為開(kāi)發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見(jiàn)模式的工具(例如配置管理,服務(wù)發(fā)現(xiàn),斷路器,智...
    卡卡羅2017閱讀 136,525評(píng)論 19 139
  • Android 自定義View的各種姿勢(shì)1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 178,828評(píng)論 25 709
  • 從三月份找實(shí)習(xí)到現(xiàn)在,面了一些公司,掛了不少,但最終還是拿到小米、百度、阿里、京東、新浪、CVTE、樂(lè)視家的研發(fā)崗...
    時(shí)芥藍(lán)閱讀 42,774評(píng)論 11 349
  • 關(guān)于作者 勇埃德,新晉華裔科普作家,被稱為是當(dāng)代最優(yōu)秀的科學(xué)記者。勇埃德供職于《大西洋月刊》,同時(shí)也是一個(gè)多產(chǎn)的科...
    蔚成閱讀 1,184評(píng)論 0 0
  • 難得來(lái)到此學(xué)院,卻遇傻子三條狗。 每天相處像美蘇,氣煞我也還得忍。 望得老天放口氣,吹得三屌屁滾流。 ...
    可以再等閱讀 137評(píng)論 0 1

友情鏈接更多精彩內(nèi)容