AFNetworking之UIKit擴展與緩存實現

寫在開頭:
  • 大概回憶下,之前我們講了AFNetworking整個網絡請求的流程,包括request的拼接,session代理的轉發,response的解析。以及對一些bug的適配,如果你還沒有看過,可以點這里:
    AFNetworking到底做了什么?
    AFNetworking到底做了什么(二)?
  • 除此之外我們還單獨的開了一篇講了AF對https的處理:
    AFNetworking之于https認證
  • 本文將涉及部分AF對UIKit的擴展與圖片下載相關緩存的實現,文章內容相對獨立,如果沒看過前文,也不影響閱讀。
回到正文:

我們來看看AF對UIkit的擴展:

UIKit擴展.png
一共如上這個多類,下面我們開始著重講其中兩個UIKit的擴展:
  • 一個是我們網絡請求時狀態欄的小菊花。
  • 一個是我們幾乎都用到過請求網絡圖片的如下一行方法:
 - (void)setImageWithURL:(NSURL *)url ;
我們開始吧:
1.AFNetworkActivityIndicatorManager

這個類的作用相當簡單,就是當網絡請求的時候,狀態欄上的小菊花就會開始轉:


小菊花.png

需要的代碼也很簡單,只需在你需要它的位置中(比如AppDelegate)導入類,并加一行代碼即可:

#import "AFNetworkActivityIndicatorManager.h"
[[AFNetworkActivityIndicatorManager sharedManager] setEnabled:YES];
接下來我們來講講這個類的實現:
  • 這個類的實現也非常簡單,還記得我們之前講的AF對NSURLSessionTask中做了一個Method Swizzling嗎?大意是把它的resumesuspend方法做了一個替換,在原有實現的基礎上添加了一個通知的發送。

  • 這個類就是基于這兩個通知和task完成的通知來實現的。

首先我們來看看它的初始化方法:
+ (instancetype)sharedManager {
    static AFNetworkActivityIndicatorManager *_sharedManager = nil;
    static dispatch_once_t oncePredicate;
    dispatch_once(&oncePredicate, ^{
        _sharedManager = [[self alloc] init];
    });

    return _sharedManager;
}

- (instancetype)init {
    self = [super init];
    if (!self) {
        return nil;
    }
    //設置狀態為沒有request活躍
    self.currentState = AFNetworkActivityManagerStateNotActive;
    //開始下載通知
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(networkRequestDidStart:) name:AFNetworkingTaskDidResumeNotification object:nil];
    //掛起通知
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(networkRequestDidFinish:) name:AFNetworkingTaskDidSuspendNotification object:nil];
    //完成通知
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(networkRequestDidFinish:) name:AFNetworkingTaskDidCompleteNotification object:nil];
    //開始延遲
    self.activationDelay = kDefaultAFNetworkActivityManagerActivationDelay;
    //結束延遲
    self.completionDelay = kDefaultAFNetworkActivityManagerCompletionDelay;
    return self;
}

  • 初始化如上,設置了一個state,這個state是一個枚舉:
typedef NS_ENUM(NSInteger, AFNetworkActivityManagerState) {
    //沒有請求
    AFNetworkActivityManagerStateNotActive,
    //請求延遲開始
    AFNetworkActivityManagerStateDelayingStart,
    //請求進行中
    AFNetworkActivityManagerStateActive,
    //請求延遲結束
    AFNetworkActivityManagerStateDelayingEnd
};

這個state一共如上4種狀態,其中兩種應該很好理解,而延遲開始和延遲結束怎么理解呢?

  • 原來這是AF對請求菊花顯示做的一個優化處理,試問如果一個請求時間很短,那么菊花很可能閃一下就結束了。如果很多請求過來,那么菊花會不停的閃啊閃,這顯然并不是我們想要的效果。

  • 所以多了這兩個參數:
    1)在一個請求開始的時候,我延遲一會在去轉菊花,如果在這延遲時間內,請求結束了,那么我就不需要去轉菊花了。
    2)但是一旦轉菊花開始,哪怕很短請求就結束了,我們還是會去轉一個時間再去結束,這時間就是延遲結束的時間。

  • 緊接著我們監聽了三個通知,用來監聽當前正在進行的網絡請求的狀態。

  • 然后設置了我們前面提到的這個轉菊花延遲開始和延遲結束的時間,這兩個默認值如下:

static NSTimeInterval const kDefaultAFNetworkActivityManagerActivationDelay = 1.0;
static NSTimeInterval const kDefaultAFNetworkActivityManagerCompletionDelay = 0.17;

接著我們來看看三個通知觸發調用的方法:

//請求開始
- (void)networkRequestDidStart:(NSNotification *)notification {
    
    if ([AFNetworkRequestFromNotification(notification) URL]) {
        //增加請求活躍數
        [self incrementActivityCount];
    }
}
//請求結束
- (void)networkRequestDidFinish:(NSNotification *)notification {
    //AFNetworkRequestFromNotification(notification)返回這個通知的request,用來判斷request是否是有效的
    if ([AFNetworkRequestFromNotification(notification) URL]) {
        //減少請求活躍數
        [self decrementActivityCount];
    }
}

方法很簡單,就是開始的時候增加了請求活躍數,結束則減少。調用了如下兩個方法進行加減:

//增加請求活躍數
- (void)incrementActivityCount {
    
    //活躍的網絡數+1,并手動發送KVO
    [self willChangeValueForKey:@"activityCount"];
    @synchronized(self) {
        _activityCount++;
    }
    [self didChangeValueForKey:@"activityCount"];

    //主線程去做
    dispatch_async(dispatch_get_main_queue(), ^{
        [self updateCurrentStateForNetworkActivityChange];
    });
}

//減少請求活躍數
- (void)decrementActivityCount {
    [self willChangeValueForKey:@"activityCount"];
    @synchronized(self) {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wgnu"
        _activityCount = MAX(_activityCount - 1, 0);
#pragma clang diagnostic pop
    }
    [self didChangeValueForKey:@"activityCount"];

    dispatch_async(dispatch_get_main_queue(), ^{
        [self updateCurrentStateForNetworkActivityChange];
    });
}

方法做了什么應該很容易看明白,這里需要注意的是,task的幾個狀態的通知,是會在多線程的環境下發送過來的。所以這里對活躍數的加減,都用了@synchronized這種方式的鎖,進行了線程保護。然后回到主線程調用了updateCurrentStateForNetworkActivityChange

我們接著來看看這個方法:

- (void)updateCurrentStateForNetworkActivityChange {
    //如果是允許小菊花
    if (self.enabled) {
        switch (self.currentState) {
            //不活躍
            case AFNetworkActivityManagerStateNotActive:
                //判斷活躍數,大于0為YES
                if (self.isNetworkActivityOccurring) {
                    //設置狀態為延遲開始
                    [self setCurrentState:AFNetworkActivityManagerStateDelayingStart];
                }
                break;
            
            case AFNetworkActivityManagerStateDelayingStart:
                //No op. Let the delay timer finish out.
                break;
            case AFNetworkActivityManagerStateActive:
                if (!self.isNetworkActivityOccurring) {
                    [self setCurrentState:AFNetworkActivityManagerStateDelayingEnd];
                }
                break;
            case AFNetworkActivityManagerStateDelayingEnd:
                if (self.isNetworkActivityOccurring) {
                    [self setCurrentState:AFNetworkActivityManagerStateActive];
                }
                break;
        }
    }
}
  • 這個方法先是判斷了我們一開始設置是否需要菊花的self.enabled,如果需要,才執行。
  • 這里主要是根據當前的狀態,來判斷下一個狀態應該是什么。其中有這么一個屬性self.isNetworkActivityOccurring:
//判斷是否活躍
 - (BOOL)isNetworkActivityOccurring {
    @synchronized(self) {
        return self.activityCount > 0;
    }
}

那么這個方法應該不難理解了。

這個類復寫了currentState的set方法,每當我們改變這個state,就會觸發set方法,而怎么該轉菊花也在該方法中:

//設置當前小菊花狀態
- (void)setCurrentState:(AFNetworkActivityManagerState)currentState {
    @synchronized(self) {
        if (_currentState != currentState) {
            //KVO
            [self willChangeValueForKey:@"currentState"];
            _currentState = currentState;
            switch (currentState) {
                //如果為不活躍
                case AFNetworkActivityManagerStateNotActive:
                    //取消兩個延遲用的timer
                    [self cancelActivationDelayTimer];
                    [self cancelCompletionDelayTimer];
                    //設置小菊花不可見
                    [self setNetworkActivityIndicatorVisible:NO];
                    break;
                case AFNetworkActivityManagerStateDelayingStart:
                    //開啟一個定時器延遲去轉菊花
                    [self startActivationDelayTimer];
                    break;
                    //如果是活躍狀態
                case AFNetworkActivityManagerStateActive:
                    //取消延遲完成的timer
                    [self cancelCompletionDelayTimer];
                    //開始轉菊花
                    [self setNetworkActivityIndicatorVisible:YES];
                    break;
                    //延遲完成狀態
                case AFNetworkActivityManagerStateDelayingEnd:
                    //開啟延遲完成timer
                    [self startCompletionDelayTimer];
                    break;
            }
        }
        [self didChangeValueForKey:@"currentState"];
    }
}

這個set方法就是這個類最核心的方法了。它的作用如下:

  • 這里根據當前狀態,是否需要開始執行一個延遲開始或者延遲完成,又或者是否需要取消這兩個延遲。
  • 還判斷了,是否需要去轉狀態欄的菊花,調用了setNetworkActivityIndicatorVisible:方法:
 - (void)setNetworkActivityIndicatorVisible:(BOOL)networkActivityIndicatorVisible {
    if (_networkActivityIndicatorVisible != networkActivityIndicatorVisible) {
        [self willChangeValueForKey:@"networkActivityIndicatorVisible"];
        @synchronized(self) {
             _networkActivityIndicatorVisible = networkActivityIndicatorVisible;
        }
        [self didChangeValueForKey:@"networkActivityIndicatorVisible"];
        
        //支持自定義的Block,去自己控制小菊花
        if (self.networkActivityActionBlock) {
            self.networkActivityActionBlock(networkActivityIndicatorVisible);
        } else {
            //否則默認AF根據該Bool,去控制狀態欄小菊花是否顯示
            [[UIApplication sharedApplication] setNetworkActivityIndicatorVisible:networkActivityIndicatorVisible];
        }
    }
}
  • 這個方法就是用來控制菊花是否轉。并且支持一個自定義的Block,我們可以自己去拿到這個菊花是否應該轉的狀態值,去做一些自定義的處理。
  • 如果我們沒有實現這個Block,則調用:
[[UIApplication sharedApplication] setNetworkActivityIndicatorVisible:networkActivityIndicatorVisible];

去轉菊花。

回到state的set方法中,我們除了控制菊花去轉,還調用了以下4個方法:

//開始任務到結束的時間,默認為1秒,如果1秒就結束,那么不轉菊花,延遲去開始轉
- (void)startActivationDelayTimer {
    //只執行一次
    self.activationDelayTimer = [NSTimer
                                 timerWithTimeInterval:self.activationDelay target:self selector:@selector(activationDelayTimerFired) userInfo:nil repeats:NO];
    //添加到主線程runloop去觸發
    [[NSRunLoop mainRunLoop] addTimer:self.activationDelayTimer forMode:NSRunLoopCommonModes];
}

//完成任務到下一個任務開始,默認為0.17秒,如果0.17秒就開始下一個,那么不停  延遲去結束菊花轉
- (void)startCompletionDelayTimer {
    //先取消之前的
    [self.completionDelayTimer invalidate];
    //延遲執行讓菊花不在轉
    self.completionDelayTimer = [NSTimer timerWithTimeInterval:self.completionDelay target:self selector:@selector(completionDelayTimerFired) userInfo:nil repeats:NO];
    [[NSRunLoop mainRunLoop] addTimer:self.completionDelayTimer forMode:NSRunLoopCommonModes];
}

- (void)cancelActivationDelayTimer {
    [self.activationDelayTimer invalidate];
}

- (void)cancelCompletionDelayTimer {
    [self.completionDelayTimer invalidate];
}

這4個方法分別是開始延遲執行一個方法,和結束的時候延遲執行一個方法,和對應這兩個方法的取消。其作用,注釋應該很容易理解。
我們繼續往下看,這兩個延遲調用的到底是什么:

- (void)activationDelayTimerFired {
    //活躍狀態,即活躍數大于1才轉
    if (self.networkActivityOccurring) {
        [self setCurrentState:AFNetworkActivityManagerStateActive];
    } else {
        [self setCurrentState:AFNetworkActivityManagerStateNotActive];
    }
}
- (void)completionDelayTimerFired {
    [self setCurrentState:AFNetworkActivityManagerStateNotActive];
}

一個開始,一個完成調用,都設置了不同的currentState的值,又回到之前stateset方法中了。

至此這個AFNetworkActivityIndicatorManager類就講完了,代碼還是相當簡單明了的。

分割圖.png
2.UIImageView+AFNetworking

接下來我們來講一個我們經常用的方法,這個方法的實現類是:UIImageView+AFNetworking.h。
這是個類目,并且給UIImageView擴展了4個方法:

- (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;
- (void)cancelImageDownloadTask;
  • 前兩個想必不用我說了,沒有誰沒用過吧...就是給一個UIImageView去異步的請求一張圖片,并且可以設置一張占位圖。
  • 第3個方法設置一張圖,并且可以拿到成功和失敗的回調。
  • 第4個方法,可以取消當前的圖片設置請求。

無論SDWebImage,還是YYKit,或者AF,都實現了這么個類目。
AF關于這個類目UIImageView+AFNetworking的實現,依賴于這么兩個類:AFImageDownloader,AFAutoPurgingImageCache。
當然AFImageDownloader中,關于圖片數據請求的部分,還是使用AFURLSessionManager來實現的。

接下來我們就來看看AFImageDownloader:

先看看初始化方法:

//該類為單例
+ (instancetype)defaultInstance {
    static AFImageDownloader *sharedInstance = nil;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        sharedInstance = [[self alloc] init];
    });
    return sharedInstance;
}
- (instancetype)init {
    NSURLSessionConfiguration *defaultConfiguration = [self.class defaultURLSessionConfiguration];
    AFHTTPSessionManager *sessionManager = [[AFHTTPSessionManager alloc] initWithSessionConfiguration:defaultConfiguration];
    sessionManager.responseSerializer = [AFImageResponseSerializer serializer];

    return [self initWithSessionManager:sessionManager
                 downloadPrioritization:AFImageDownloadPrioritizationFIFO
                 maximumActiveDownloads:4
                             imageCache:[[AFAutoPurgingImageCache alloc] init]];
}
+ (NSURLSessionConfiguration *)defaultURLSessionConfiguration {
    NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration defaultSessionConfiguration];

    //TODO set the default HTTP headers

    configuration.HTTPShouldSetCookies = YES;
    configuration.HTTPShouldUsePipelining = NO;

    configuration.requestCachePolicy = NSURLRequestUseProtocolCachePolicy;
    //是否允許蜂窩網絡,手機網
    configuration.allowsCellularAccess = YES;
    //默認超時
    configuration.timeoutIntervalForRequest = 60.0;
    //設置的圖片緩存對象
    configuration.URLCache = [AFImageDownloader defaultURLCache];

    return configuration;
}

該類為單例,上述方法中,創建了一個sessionManager,這個sessionManager將用于我們之后的網絡請求。從這里我們可以看到,這個類的網絡請求都是基于之前AF自己封裝的AFHTTPSessionManager。

  • 在這里初始化了一系列的對象,需要講一下的是AFImageDownloadPrioritizationFIFO,這個一個枚舉值:
typedef NS_ENUM(NSInteger, AFImageDownloadPrioritization) {
    //先進先出
    AFImageDownloadPrioritizationFIFO,
    //后進先出
    AFImageDownloadPrioritizationLIFO
};

這個枚舉值代表著,一堆圖片下載,執行任務的順序。

  • 還有一個AFAutoPurgingImageCache的創建,這個類是AF做圖片緩存用的。這里我們暫時就這么理解它,講完當前類,我們再來補充它。
  • 除此之外,我們還看到一個cache:
configuration.URLCache = [AFImageDownloader defaultURLCache];
//設置一個系統緩存,內存緩存為20M,磁盤緩存為150M,
//這個是系統級別維護的緩存。
 + (NSURLCache *)defaultURLCache {
    return [[NSURLCache alloc] initWithMemoryCapacity:20 * 1024 * 1024
                                         diskCapacity:150 * 1024 * 1024
                                             diskPath:@"com.alamofire.imagedownloader"];
}

大家看到這可能迷惑了,怎么這么多cache,那AF做圖片緩存到底用哪個呢?答案是AF自己控制的圖片緩存用AFAutoPurgingImageCache,而NSUrlRequest的緩存由它自己內部根據策略去控制,用的是NSURLCache,不歸AF處理,只需在configuration中設置上即可。

  • 那么看到這有些小伙伴又要問了,為什么不直接用NSURLCache,還要自定義一個AFAutoPurgingImageCache呢?原來是因為NSURLCache的諸多限制,例如只支持get請求等等。而且因為是系統維護的,我們自己的可控度不強,并且如果需要做一些自定義的緩存處理,無法實現。
  • 更多關于NSURLCache的內容,大家可以自行查閱。

接著上面的方法調用到這個最終的初始化方法中:

- (instancetype)initWithSessionManager:(AFHTTPSessionManager *)sessionManager
                downloadPrioritization:(AFImageDownloadPrioritization)downloadPrioritization
                maximumActiveDownloads:(NSInteger)maximumActiveDownloads
                            imageCache:(id <AFImageRequestCache>)imageCache {
    if (self = [super init]) {
        //持有
        self.sessionManager = sessionManager;
        //定義下載任務的順序,默認FIFO,先進先出-隊列模式,還有后進先出-棧模式
        self.downloadPrioritizaton = downloadPrioritization;
        //最大的下載數
        self.maximumActiveDownloads = maximumActiveDownloads;
        
        //自定義的cache
        self.imageCache = imageCache;

        //隊列中的任務,待執行的
        self.queuedMergedTasks = [[NSMutableArray alloc] init];
        //合并的任務,所有任務的字典
        self.mergedTasks = [[NSMutableDictionary alloc] init];
        //活躍的request數
        self.activeRequestCount = 0;

        //用UUID來拼接名字
        NSString *name = [NSString stringWithFormat:@"com.alamofire.imagedownloader.synchronizationqueue-%@", [[NSUUID UUID] UUIDString]];
        //創建一個串行的queue
        self.synchronizationQueue = dispatch_queue_create([name cStringUsingEncoding:NSASCIIStringEncoding], DISPATCH_QUEUE_SERIAL);

        name = [NSString stringWithFormat:@"com.alamofire.imagedownloader.responsequeue-%@", [[NSUUID UUID] UUIDString]];
        //創建并行queue
        self.responseQueue = dispatch_queue_create([name cStringUsingEncoding:NSASCIIStringEncoding], DISPATCH_QUEUE_CONCURRENT);
    }

    return self;
}

這邊初始化了一些屬性,這些屬性跟著注釋看應該很容易明白其作用。主要需要注意的就是,這里創建了兩個queue:一個串行的請求queue,和一個并行的響應queue。

  • 這個串行queue,是用來做內部生成task等等一系列業務邏輯的。它保證了我們在這些邏輯處理中的線程安全問題(迷惑的接著往下看)。
  • 這個并行queue,被用來做網絡請求完成的數據回調。

接下來我們來看看它的創建請求task的方法:

- (nullable AFImageDownloadReceipt *)downloadImageForURLRequest:(NSURLRequest *)request
                                                        success:(void (^)(NSURLRequest * _Nonnull, NSHTTPURLResponse * _Nullable, UIImage * _Nonnull))success
                                                        failure:(void (^)(NSURLRequest * _Nonnull, NSHTTPURLResponse * _Nullable, NSError * _Nonnull))failure {
    return [self downloadImageForURLRequest:request withReceiptID:[NSUUID UUID] success:success failure:failure];
}

- (nullable AFImageDownloadReceipt *)downloadImageForURLRequest:(NSURLRequest *)request
                                                  withReceiptID:(nonnull NSUUID *)receiptID
                                                        success:(nullable void (^)(NSURLRequest *request, NSHTTPURLResponse  * _Nullable response, UIImage *responseObject))success
                                                        failure:(nullable void (^)(NSURLRequest *request, NSHTTPURLResponse * _Nullable response, NSError *error))failure {
    //還是類似之前的,同步串行去做下載的事 生成一個task,這些事情都是在當前線程中串行同步做的,所以不用擔心線程安全問題。
    __block NSURLSessionDataTask *task = nil;
    dispatch_sync(self.synchronizationQueue, ^{
        //url字符串
        NSString *URLIdentifier = request.URL.absoluteString;
        if (URLIdentifier == nil) {
            if (failure) {
                //錯誤返回,沒Url
                NSError *error = [NSError errorWithDomain:NSURLErrorDomain code:NSURLErrorBadURL userInfo:nil];
                dispatch_async(dispatch_get_main_queue(), ^{
                    failure(request, nil, error);
                });
            }
            return;
        }

        //如果這個任務已經存在,則添加成功失敗Block,然后直接返回,即一個url用一個request,可以響應好幾個block
        //從自己task字典中根據Url去取AFImageDownloaderMergedTask,里面有task id url等等信息
        AFImageDownloaderMergedTask *existingMergedTask = self.mergedTasks[URLIdentifier];
        if (existingMergedTask != nil) {
            //里面包含成功和失敗Block和UUid
            AFImageDownloaderResponseHandler *handler = [[AFImageDownloaderResponseHandler alloc] initWithUUID:receiptID success:success failure:failure];
            //添加handler
            [existingMergedTask addResponseHandler:handler];
            //給task賦值
            task = existingMergedTask.task;
            return;
        }

        //根據request的緩存策略,加載緩存
        switch (request.cachePolicy) {
            //這3種情況都會去加載緩存
            case NSURLRequestUseProtocolCachePolicy:
            case NSURLRequestReturnCacheDataElseLoad:
            case NSURLRequestReturnCacheDataDontLoad: {
                //從cache中根據request拿數據
                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;
        }

        //走到這說明即沒有請求中的request,也沒有cache,開始請求
        NSUUID *mergedTaskIdentifier = [NSUUID UUID];
        //task
        NSURLSessionDataTask *createdTask;
        __weak __typeof__(self) weakSelf = self;
        
        //用sessionManager的去請求,注意,只是創建task,還是掛起狀態
        createdTask = [self.sessionManager
                       dataTaskWithRequest:request
                       completionHandler:^(NSURLResponse * _Nonnull response, id  _Nullable responseObject, NSError * _Nullable error) {
                           
                           //在responseQueue中回調數據,初始化為并行queue
                           dispatch_async(self.responseQueue, ^{
                               __strong __typeof__(weakSelf) strongSelf = weakSelf;
                               
                               //拿到當前的task
                               AFImageDownloaderMergedTask *mergedTask = self.mergedTasks[URLIdentifier];
                               
                               //如果之前的task數組中,有這個請求的任務task,則從數組中移除
                               if ([mergedTask.identifier isEqual:mergedTaskIdentifier]) {
                                   //安全的移除,并返回當前被移除的AF task
                                   mergedTask = [strongSelf safelyRemoveMergedTaskWithURLIdentifier:URLIdentifier];
                                   //請求錯誤
                                   if (error) {
                                       //去遍歷task所有響應的處理
                                       for (AFImageDownloaderResponseHandler *handler in mergedTask.responseHandlers) {
                                           //主線程,調用失敗的Block
                                           if (handler.failureBlock) {
                                               dispatch_async(dispatch_get_main_queue(), ^{
                                                   handler.failureBlock(request, (NSHTTPURLResponse*)response, error);
                                               });
                                           }
                                       }
                                   } else {
                                       //成功根據request,往cache里添加
                                       [strongSelf.imageCache addImage:responseObject forRequest:request withAdditionalIdentifier:nil];
                                       //調用成功Block
                                       for (AFImageDownloaderResponseHandler *handler in mergedTask.responseHandlers) {
                                           if (handler.successBlock) {
                                               dispatch_async(dispatch_get_main_queue(), ^{
                                                   handler.successBlock(request, (NSHTTPURLResponse*)response, responseObject);
                                               });
                                           }
                                       }
                                       
                                   }
                               }
                               //減少活躍的任務數
                               [strongSelf safelyDecrementActiveTaskCount];
                               [strongSelf safelyStartNextTaskIfNecessary];
                           });
                       }];

        // 4) Store the response handler for use when the request completes
        //創建handler
        AFImageDownloaderResponseHandler *handler = [[AFImageDownloaderResponseHandler alloc] initWithUUID:receiptID
                                                                                                   success:success
                                                                                                   failure:failure];
        //創建task
        AFImageDownloaderMergedTask *mergedTask = [[AFImageDownloaderMergedTask alloc]
                                                   initWithURLIdentifier:URLIdentifier
                                                   identifier:mergedTaskIdentifier
                                                   task:createdTask];
        //添加handler
        [mergedTask addResponseHandler:handler];
        //往當前任務字典里添加任務
        self.mergedTasks[URLIdentifier] = mergedTask;

        // 5) Either start the request or enqueue it depending on the current active request count
        //如果小于,則開始任務下載resume
        if ([self isActiveRequestCountBelowMaximumLimit]) {
            [self startMergedTask:mergedTask];
        } else {
            
            [self enqueueMergedTask:mergedTask];
        }
        //拿到最終生成的task
        task = mergedTask.task;
    });
    if (task) {
        //創建一個AFImageDownloadReceipt并返回,里面就多一個receiptID。
        return [[AFImageDownloadReceipt alloc] initWithReceiptID:receiptID task:task];
    } else {
        return nil;
    }
}

就這么一個非常非常長的方法,這個方法執行的內容都是在我們之前創建的串行queue中,同步的執行的,這是因為這個方法絕大多數的操作都是需要線程安全的??梢詫χ创a和注釋來看,我們在這講下它做了什么:

  1. 首先做了一個url的判斷,如果為空則返回失敗Block。
  2. 判斷這個需要請求的url,是不是已經被生成的task中,如果是的話,則多添加一個回調處理就可以。回調處理對象為AFImageDownloaderResponseHandler。這個類非常簡單,總共就如下3個屬性:
@interface AFImageDownloaderResponseHandler : NSObject
@property (nonatomic, strong) NSUUID *uuid;
@property (nonatomic, copy) void (^successBlock)(NSURLRequest*, NSHTTPURLResponse*, UIImage*);
@property (nonatomic, copy) void (^failureBlock)(NSURLRequest*, NSHTTPURLResponse*, NSError*);
@end
@implementation AFImageDownloaderResponseHandler
//初始化回調對象
 - (instancetype)initWithUUID:(NSUUID *)uuid
                     success:(nullable void (^)(NSURLRequest *request, NSHTTPURLResponse * _Nullable response, UIImage *responseObject))success
                     failure:(nullable void (^)(NSURLRequest *request, NSHTTPURLResponse * _Nullable response, NSError *error))failure {
    if (self = [self init]) {
        self.uuid = uuid;
        self.successBlock = success;
        self.failureBlock = failure;
    }
    return self;
}

當這個task完成的時候,會調用我們添加的回調。

  1. 關于AFImageDownloaderMergedTask,我們在這里都用的是這種類型的task,其實這個task也很簡單:
@interface AFImageDownloaderMergedTask : NSObject
@property (nonatomic, strong) NSString *URLIdentifier;
@property (nonatomic, strong) NSUUID *identifier;
@property (nonatomic, strong) NSURLSessionDataTask *task;
@property (nonatomic, strong) NSMutableArray <AFImageDownloaderResponseHandler*> *responseHandlers;
@end
@implementation AFImageDownloaderMergedTask
 - (instancetype)initWithURLIdentifier:(NSString *)URLIdentifier identifier:(NSUUID *)identifier task:(NSURLSessionDataTask *)task {
    if (self = [self init]) {
        self.URLIdentifier = URLIdentifier;
        self.task = task;
        self.identifier = identifier;
        self.responseHandlers = [[NSMutableArray alloc] init];
    }
    return self;
}
//添加任務完成回調
 - (void)addResponseHandler:(AFImageDownloaderResponseHandler*)handler {
    [self.responseHandlers addObject:handler];
}
//移除任務完成回調
 - (void)removeResponseHandler:(AFImageDownloaderResponseHandler*)handler {
    [self.responseHandlers removeObject:handler];
}
@end

其實就是除了NSURLSessionDataTask,多加了幾個參數,URLIdentifieridentifier都是用來標識這個task的,responseHandlers是用來存儲task完成后的回調的,里面可以存一組,當任務完成時候,里面的回調都會被調用。

  1. 接著去根據緩存策略,去加載緩存,如果有緩存,從self.imageCache中返回緩存,否則繼續往下走。
  2. 走到這說明沒相同url的task,也沒有cache,那么就開始一個新的task,調用的是AFUrlSessionManager里的請求方法生成了一個task(這里我們就不贅述了,可以看之前的樓主之前的文章)。然后做了請求完成的處理。注意,這里處理實在我們一開始初始化的并行queue:self.responseQueue中的,這里的響應處理是多線程并發進行的。
    1)完成,則調用如下方法把這個task從全局字典中移除:
 //移除task相關,用同步串行的形式,防止移除中出現重復移除一系列問題
  - (AFImageDownloaderMergedTask*)safelyRemoveMergedTaskWithURLIdentifier:(NSString *)URLIdentifier {
    __block AFImageDownloaderMergedTask *mergedTask = nil;
    dispatch_sync(self.synchronizationQueue, ^{
        mergedTask = [self removeMergedTaskWithURLIdentifier:URLIdentifier];
    });
    return mergedTask;
}

2)去循環這個task的responseHandlers,調用它的成功或者失敗的回調。
3)并且調用下面兩個方法,去減少正在請求的任務數,和開啟下一個任務:

//減少活躍的任務數
 - (void)safelyDecrementActiveTaskCount {
    //回到串行queue去-
    dispatch_sync(self.synchronizationQueue, ^{
        if (self.activeRequestCount > 0) {
            self.activeRequestCount -= 1;
        }
    });
}
//如果可以,則開啟下一個任務
 - (void)safelyStartNextTaskIfNecessary {
    //回到串行queue
    dispatch_sync(self.synchronizationQueue, ^{
        //先判斷并行數限制
        if ([self isActiveRequestCountBelowMaximumLimit]) {
            while (self.queuedMergedTasks.count > 0) {
                //獲取數組中第一個task
                AFImageDownloaderMergedTask *mergedTask = [self dequeueMergedTask];
                //如果狀態是掛起狀態
                if (mergedTask.task.state == NSURLSessionTaskStateSuspended) {
                    [self startMergedTask:mergedTask];
                    break;
                }
            }
        }
    });
}

這里需要注意的是,跟我們本類的一些數據相關的操作,都是在我們一開始的串行queue中同步進行的。
4)除此之外,如果成功,還把成功請求到的數據,加到AF自定義的cache中:

//成功根據request,往cache里添加
[strongSelf.imageCache addImage:responseObject forRequest:request withAdditionalIdentifier:nil];
  1. NSUUID生成的唯一標識,去生成AFImageDownloaderResponseHandler,然后生成一個AFImageDownloaderMergedTask,把之前第5步生成的createdTask和回調都綁定給這個AF自定義可合并回調的task,然后這個task加到全局的task映射字典中,key為url:
self.mergedTasks[URLIdentifier] = mergedTask;
  1. 判斷當前正在下載的任務是否超過最大并行數,如果沒有則開始下載,否則先加到等待的數組中去:
//如果小于最大并行數,則開始任務下載resume
if ([self isActiveRequestCountBelowMaximumLimit]) {
    [self startMergedTask:mergedTask];
} else {
    
    [self enqueueMergedTask:mergedTask];
}
//判斷并行數限制
 - (BOOL)isActiveRequestCountBelowMaximumLimit {
    return self.activeRequestCount < self.maximumActiveDownloads;
}
//開始下載
 - (void)startMergedTask:(AFImageDownloaderMergedTask *)mergedTask {
    [mergedTask.task resume];
    //任務活躍數+1
    ++self.activeRequestCount;
}
//把任務先加到數組里
 - (void)enqueueMergedTask:(AFImageDownloaderMergedTask *)mergedTask {
    switch (self.downloadPrioritizaton) {
            //先進先出
        case AFImageDownloadPrioritizationFIFO:
            [self.queuedMergedTasks addObject:mergedTask];
            break;
            //后進先出
        case AFImageDownloadPrioritizationLIFO:
            [self.queuedMergedTasks insertObject:mergedTask atIndex:0];
            break;
    }
}
  • 先判斷并行數限制,如果小于最大限制,則開始下載,把當前活躍的request數量+1。
  • 如果暫時不能下載,被加到等待下載的數組中去的話,會根據我們一開始設置的下載策略,是先進先出,還是后進先出,去插入這個下載任務。
  1. 最后判斷這個mergeTask是否為空。不為空,我們生成了一個AFImageDownloadReceipt,綁定了一個UUID。否則為空返回nil:
if (task) {
    //創建一個AFImageDownloadReceipt并返回,里面就多一個receiptID。
    return [[AFImageDownloadReceipt alloc] initWithReceiptID:receiptID task:task];
} else {
    return nil;
}

這個AFImageDownloadReceipt僅僅是多封裝了一個UUID:

@interface AFImageDownloadReceipt : NSObject
@property (nonatomic, strong) NSURLSessionDataTask *task;
@property (nonatomic, strong) NSUUID *receiptID;
@end
@implementation AFImageDownloadReceipt
 - (instancetype)initWithReceiptID:(NSUUID *)receiptID task:(NSURLSessionDataTask *)task {
    if (self = [self init]) {
        self.receiptID = receiptID;
        self.task = task;
    }
    return self;
}

這么封裝是為了標識每一個task,我們后面可以根據這個AFImageDownloadReceipt來對task做取消操作。

這個AFImageDownloader中最核心的方法基本就講完了,還剩下一些方法沒講,像前面講到的task的取消的方法:

//根據AFImageDownloadReceipt來取消任務,即對應一個響應回調。
- (void)cancelTaskForImageDownloadReceipt:(AFImageDownloadReceipt *)imageDownloadReceipt {
    dispatch_sync(self.synchronizationQueue, ^{
        //拿到url
        NSString *URLIdentifier = imageDownloadReceipt.task.originalRequest.URL.absoluteString;
        //根據url拿到task
        AFImageDownloaderMergedTask *mergedTask = self.mergedTasks[URLIdentifier];
        
        //快速遍歷查找某個下標,如果返回YES,則index為當前下標
        NSUInteger index = [mergedTask.responseHandlers indexOfObjectPassingTest:^BOOL(AFImageDownloaderResponseHandler * _Nonnull handler, __unused NSUInteger idx, __unused BOOL * _Nonnull stop) {
            
            return handler.uuid == imageDownloadReceipt.receiptID;
        }];

        if (index != NSNotFound) {
            //移除響應處理
            AFImageDownloaderResponseHandler *handler = mergedTask.responseHandlers[index];
            [mergedTask removeResponseHandler:handler];
            NSString *failureReason = [NSString stringWithFormat:@"ImageDownloader cancelled URL request: %@",imageDownloadReceipt.task.originalRequest.URL.absoluteString];
            NSDictionary *userInfo = @{NSLocalizedFailureReasonErrorKey:failureReason};
            NSError *error = [NSError errorWithDomain:NSURLErrorDomain code:NSURLErrorCancelled userInfo:userInfo];
            //并調用失敗block,原因為取消
            if (handler.failureBlock) {
                dispatch_async(dispatch_get_main_queue(), ^{
                    handler.failureBlock(imageDownloadReceipt.task.originalRequest, nil, error);
                });
            }
        }
        
        //如果任務里的響應回調為空或者狀態為掛起,則取消task,并且從字典中移除
        if (mergedTask.responseHandlers.count == 0 && mergedTask.task.state == NSURLSessionTaskStateSuspended) {
            [mergedTask.task cancel];
            [self removeMergedTaskWithURLIdentifier:URLIdentifier];
        }
    });
}
//根據URLIdentifier移除task
- (AFImageDownloaderMergedTask *)removeMergedTaskWithURLIdentifier:(NSString *)URLIdentifier {
    AFImageDownloaderMergedTask *mergedTask = self.mergedTasks[URLIdentifier];
    [self.mergedTasks removeObjectForKey:URLIdentifier];
    return mergedTask;
}

方法比較簡單,大家自己看看就好。至此```AFImageDownloader``這個類講完了。如果大家看的感覺比較繞,沒關系,等到最后我們一起來總結一下,捋一捋。

分割圖.png

我們之前講到AFAutoPurgingImageCache這個類略過去了,現在我們就來補充一下這個類的相關內容:
首先來講講這個類的作用,它是AF自定義用來做圖片緩存的。我們來看看它的初始化方法:

- (instancetype)init {
    //默認為內存100M,后者為緩存溢出后保留的內存
    return [self initWithMemoryCapacity:100 * 1024 * 1024 preferredMemoryCapacity:60 * 1024 * 1024];
}

- (instancetype)initWithMemoryCapacity:(UInt64)memoryCapacity preferredMemoryCapacity:(UInt64)preferredMemoryCapacity {
    if (self = [super init]) {
        //內存大小
        self.memoryCapacity = memoryCapacity;
        self.preferredMemoryUsageAfterPurge = preferredMemoryCapacity;
        //cache的字典
        self.cachedImages = [[NSMutableDictionary alloc] init];

        NSString *queueName = [NSString stringWithFormat:@"com.alamofire.autopurgingimagecache-%@", [[NSUUID UUID] UUIDString]];
        //并行的queue
        self.synchronizationQueue = dispatch_queue_create([queueName cStringUsingEncoding:NSASCIIStringEncoding], DISPATCH_QUEUE_CONCURRENT);

        //添加通知,收到內存警告的通知
        [[NSNotificationCenter defaultCenter]
         addObserver:self
         selector:@selector(removeAllImages)
         name:UIApplicationDidReceiveMemoryWarningNotification
         object:nil];

    }
    return self;
}

初始化方法很簡單,總結一下:

  1. 聲明了一個默認的內存緩存大小100M,還有一個意思是如果超出100M之后,我們去清除緩存,此時仍要保留的緩存大小60M。(如果還是不理解,可以看后文,源碼中會講到)
  2. 創建了一個并行queue,這個并行queue,這個類除了初始化以外,所有的方法都是在這個并行queue中調用的。
  3. 創建了一個cache字典,我們所有的緩存數據,都被保存在這個字典中,key為url,value為AFCachedImage。
    關于這個AFCachedImage,其實就是Image之外封裝了幾個關于這個緩存的參數,如下:
@interface AFCachedImage : NSObject
@property (nonatomic, strong) UIImage *image;
@property (nonatomic, strong) NSString *identifier;  //url標識
@property (nonatomic, assign) UInt64 totalBytes;   //總大小
@property (nonatomic, strong) NSDate *lastAccessDate;  //上次獲取時間
@property (nonatomic, assign) UInt64 currentMemoryUsage; //這個參數沒被用到過
@end
@implementation AFCachedImage
//初始化
 -(instancetype)initWithImage:(UIImage *)image identifier:(NSString *)identifier {
    if (self = [self init]) {
        self.image = image;
        self.identifier = identifier;

        CGSize imageSize = CGSizeMake(image.size.width * image.scale, image.size.height * image.scale);
        CGFloat bytesPerPixel = 4.0;
        CGFloat bytesPerSize = imageSize.width * imageSize.height;
        self.totalBytes = (UInt64)bytesPerPixel * (UInt64)bytesPerSize;
        self.lastAccessDate = [NSDate date];
    }
    return self;
}
//上次獲取緩存的時間
 - (UIImage*)accessImage {
    self.lastAccessDate = [NSDate date];
    return self.image;
}
  1. 添加了一個通知,監聽內存警告,當發成內存警告,調用該方法,移除所有的緩存,并且把當前緩存數置為0:
//移除所有圖片
 - (BOOL)removeAllImages {
    __block BOOL removed = NO;
    dispatch_barrier_sync(self.synchronizationQueue, ^{
        if (self.cachedImages.count > 0) {
            [self.cachedImages removeAllObjects];
            self.currentMemoryUsage = 0;
            removed = YES;
        }
    });
    return removed;
}

注意這個類大量的使用了dispatch_barrier_syncdispatch_barrier_async,小伙伴們如果對這兩個方法有任何疑惑,可以看看這篇文章:dispatch_barrier_async與dispatch_barrier_sync異同。
1)這里我們可以看到使用了dispatch_barrier_sync,這里沒有用鎖,但是因為使用了dispatch_barrier_sync,不僅同步了synchronizationQueue隊列,而且阻塞了當前線程,所以保證了里面執行代碼的線程安全問題。
2)在這里其實使用鎖也可以,但是AF在這的處理卻是使用同步的機制來保證線程安全,或許這跟圖片的加載緩存的使用場景,高頻次有關系,在這里使用sync,并不需要在去開辟新的線程,浪費性能,只需要在原有線程,提交到synchronizationQueue隊列中,阻塞的執行即可。這樣省去大量的開辟線程與使用鎖帶來的性能消耗。(當然這僅僅是我的一個猜測,有不同意見的朋友歡迎討論~)

  • 在這里用了dispatch_barrier_sync,因為synchronizationQueue是個并行queue,所以在這里不會出現死鎖的問題。
  • 關于保證線程安全的同時,同步還是異步,與性能方面的考量,可以參考這篇文章:Objc的底層并發API

接著我們來看看這個類最核心的一個方法:

//添加image到cache里
- (void)addImage:(UIImage *)image withIdentifier:(NSString *)identifier {
   
    //用dispatch_barrier_async,來同步這個并行隊列
    dispatch_barrier_async(self.synchronizationQueue, ^{
        //生成cache對象
        AFCachedImage *cacheImage = [[AFCachedImage alloc] initWithImage:image identifier:identifier];
        
        //去之前cache的字典里取
        AFCachedImage *previousCachedImage = self.cachedImages[identifier];
        //如果有被緩存過
        if (previousCachedImage != nil) {
            //當前已經使用的內存大小減去圖片的大小
            self.currentMemoryUsage -= previousCachedImage.totalBytes;
        }
        //把新cache的image加上去
        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];
            
            //根據lastAccessDate排序 升序,越晚的越后面
            NSSortDescriptor *sortDescriptor = [[NSSortDescriptor alloc] initWithKey:@"lastAccessDate"
                                                                           ascending:YES];
            
            [sortedImages sortUsingDescriptors:@[sortDescriptor]];

            UInt64 bytesPurged = 0;
            //移除早期的cache bytesToPurge大小
            for (AFCachedImage *cachedImage in sortedImages) {
                [self.cachedImages removeObjectForKey:cachedImage.identifier];
                bytesPurged += cachedImage.totalBytes;
                if (bytesPurged >= bytesToPurge) {
                    break ;
                }
            }
            //減去被清掉的內存
            self.currentMemoryUsage -= bytesPurged;
        }
    });
}

看注釋應該很容易明白,這個方法做了兩件事:

  1. 設置緩存到字典里,并且把對應的緩存大小設置到當前已緩存的數量屬性中。
  2. 判斷是緩存超出了我們設置的最大緩存100M,如果是的話,則清除掉部分早時間的緩存,清除到緩存小于我們溢出后保留的內存60M以內。

當然在這里更需要說一說的是dispatch_barrier_async,這里整個類都沒有使用dispatch_async,所以不存在是為了做一個柵欄,來同步上下文的線程。其實它在本類中的作用很簡單,就是一個串行執行。

  • 講到這,小伙伴們又疑惑了,既然就是只是為了串行,那為什么我們不用一個串行queue就得了?非得用dispatch_barrier_async干嘛?其實小伙伴要是看的仔細,就明白了,上文我們說過,我們要用dispatch_barrier_sync來保證線程安全。如果我們使用串行queue,那么線程是極其容易死鎖的。

還有剩下的幾個方法:

//根據id獲取圖片
- (nullable UIImage *)imageWithIdentifier:(NSString *)identifier {
    __block UIImage *image = nil;
    //用同步的方式獲取,防止線程安全問題
    dispatch_sync(self.synchronizationQueue, ^{
        AFCachedImage *cachedImage = self.cachedImages[identifier];
        //并且刷新獲取的時間
        image = [cachedImage accessImage];
    });
    return image;
}

//根據request和additionalIdentifier添加cache
- (void)addImage:(UIImage *)image forRequest:(NSURLRequest *)request withAdditionalIdentifier:(NSString *)identifier {
    [self addImage:image withIdentifier:[self imageCacheKeyFromURLRequest:request withAdditionalIdentifier:identifier]];
}

//根據request和additionalIdentifier移除圖片
- (BOOL)removeImageforRequest:(NSURLRequest *)request withAdditionalIdentifier:(NSString *)identifier {
    return [self removeImageWithIdentifier:[self imageCacheKeyFromURLRequest:request withAdditionalIdentifier:identifier]];
}
//根據request和additionalIdentifier獲取圖片

- (nullable UIImage *)imageforRequest:(NSURLRequest *)request withAdditionalIdentifier:(NSString *)identifier {
    return [self imageWithIdentifier:[self imageCacheKeyFromURLRequest:request withAdditionalIdentifier:identifier]];
}

//生成id的方式為Url字符串+additionalIdentifier
- (NSString *)imageCacheKeyFromURLRequest:(NSURLRequest *)request withAdditionalIdentifier:(NSString *)additionalIdentifier {
    NSString *key = request.URL.absoluteString;
    if (additionalIdentifier != nil) {
        key = [key stringByAppendingString:additionalIdentifier];
    }
    return key;
}

這幾個方法都很簡單,大家自己看看就好了,就不贅述了。至此AFAutoPurgingImageCache也講完了,我們還是等到最后再來總結。

分割圖.png

我們繞了一大圈,總算回到了UIImageView+AFNetworking這個類,現在圖片下載的方法,和緩存的方法都有了,實現這個類也是水到渠成的事了。

我們來看下面我們絕大多數人很熟悉的方法,看看它的實現:

- (void)setImageWithURL:(NSURL *)url {
    [self setImageWithURL:url placeholderImage:nil];
}

- (void)setImageWithURL:(NSURL *)url
       placeholderImage:(UIImage *)placeholderImage
{
    //設置head,可接受類型為image
    NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
    [request addValue:@"image/*" forHTTPHeaderField:@"Accept"];

    [self setImageWithURLRequest:request placeholderImage:placeholderImage success:nil failure:nil];
}

上述方法按順序往下調用,第二個方法給head的Accept類型設置為Image。接著調用到第三個方法,也是這個類目唯一一個重要的方法:

- (void)setImageWithURLRequest:(NSURLRequest *)urlRequest
              placeholderImage:(UIImage *)placeholderImage
                       success:(void (^)(NSURLRequest *request, NSHTTPURLResponse * _Nullable response, UIImage *image))success
                       failure:(void (^)(NSURLRequest *request, NSHTTPURLResponse * _Nullable response, NSError *error))failure
{
    //url為空,則取消
    if ([urlRequest URL] == nil) {
        //取消task
        [self cancelImageDownloadTask];
        //設置為占位圖
        self.image = placeholderImage;
        return;
    }
    
    //看看設置的當前的回調的request和需要請求的request是不是為同一個,是的話為重復調用,直接返回
    if ([self isActiveTaskURLEqualToURLRequest:urlRequest]){
        return;
    }
    
    //開始請求前,先取消之前的task,即解綁回調
    [self cancelImageDownloadTask];

    //拿到downloader
    AFImageDownloader *downloader = [[self class] sharedImageDownloader];
    //拿到cache
    id <AFImageRequestCache> imageCache = downloader.imageCache;

    //Use the image from the image cache if it exists
    UIImage *cachedImage = [imageCache imageforRequest:urlRequest withAdditionalIdentifier:nil];
    //去獲取cachedImage
    if (cachedImage) {
        //有的話直接設置,并且置空回調
        if (success) {
            success(urlRequest, nil, cachedImage);
        } else {
            self.image = cachedImage;
        }
        [self clearActiveDownloadInformation];
    } else {
        //無緩存,如果有占位圖,先設置
        if (placeholderImage) {
            self.image = placeholderImage;
        }

        __weak __typeof(self)weakSelf = self;
        NSUUID *downloadID = [NSUUID UUID];
        AFImageDownloadReceipt *receipt;
        //去下載,并得到一個receipt,可以用來取消回調
        receipt = [downloader
                   downloadImageForURLRequest:urlRequest
                   withReceiptID:downloadID
                   success:^(NSURLRequest * _Nonnull request, NSHTTPURLResponse * _Nullable response, UIImage * _Nonnull responseObject) {
                       __strong __typeof(weakSelf)strongSelf = weakSelf;
                       //判斷receiptID和downloadID是否相同 成功回調,設置圖片
                       if ([strongSelf.af_activeImageDownloadReceipt.receiptID isEqual:downloadID]) {
                           if (success) {
                               success(request, response, responseObject);
                           } else if(responseObject) {
                               strongSelf.image = responseObject;
                           }
                           //置空回調
                           [strongSelf clearActiveDownloadInformation];
                       }

                   }
                   failure:^(NSURLRequest * _Nonnull request, NSHTTPURLResponse * _Nullable response, NSError * _Nonnull error) {
                       __strong __typeof(weakSelf)strongSelf = weakSelf;
                       //失敗有failuerBlock就回調,
                        if ([strongSelf.af_activeImageDownloadReceipt.receiptID isEqual:downloadID]) {
                            if (failure) {
                                failure(request, response, error);
                            }
                            //置空回調對象
                            [strongSelf clearActiveDownloadInformation];
                        }
                   }];
        //賦值
        self.af_activeImageDownloadReceipt = receipt;
    }
}

這個方法,細節的地方可以關注注釋,這里總結一下做了什么:
1)去判斷url是否為空,如果為空則取消task,調用如下方法:

//取消task
- (void)cancelImageDownloadTask {
    if (self.af_activeImageDownloadReceipt != nil) {
        //取消事件回調響應
        [[self.class sharedImageDownloader] cancelTaskForImageDownloadReceipt:self.af_activeImageDownloadReceipt];
        //置空
        [self clearActiveDownloadInformation];
     }
}
//置空
- (void)clearActiveDownloadInformation {
    self.af_activeImageDownloadReceipt = nil;
}

  • 這里注意cancelImageDownloadTask中,調用了self.af_activeImageDownloadReceipt這么一個屬性,看看定義的地方:
@interface UIImageView (_AFNetworking)
@property (readwrite, nonatomic, strong, setter = af_setActiveImageDownloadReceipt:) AFImageDownloadReceipt *af_activeImageDownloadReceipt;
@end
@implementation UIImageView (_AFNetworking)
//綁定屬性 AFImageDownloadReceipt,就是一個事件響應的接受對象,包含一個task,一個uuid
 - (AFImageDownloadReceipt *)af_activeImageDownloadReceipt {
    return (AFImageDownloadReceipt *)objc_getAssociatedObject(self, @selector(af_activeImageDownloadReceipt));
}
//set
 - (void)af_setActiveImageDownloadReceipt:(AFImageDownloadReceipt *)imageDownloadReceipt {
    objc_setAssociatedObject(self, @selector(af_activeImageDownloadReceipt), imageDownloadReceipt, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
@end

我們現在是給UIImageView添加的一個類目,所以我們無法直接添加屬性,而是使用的是runtime的方式來生成set和get方法生成了一個AFImageDownloadReceipt類型的屬性??催^上文應該知道這個對象里面就一個task和一個UUID。這個屬性就是我們這次下載任務相關聯的信息。

2)然后做了一系列判斷,見注釋。
3)然后生成了一個我們之前分析過得AFImageDownloader,然后去獲取緩存,如果有緩存,則直接讀緩存。還記得AFImageDownloader里也有一個讀緩存的方法么?那個是和cachePolicy相關的,而這個是有緩存的話直接讀取。不明白的可以回過頭去看看。
4)走到這說明沒緩存了,然后就去用AFImageDownloader,我們之前講過的方法,去請求圖片。完成后,則調用成功或者失敗的回調,并且置空屬性self.af_activeImageDownloadReceipt,成功則設置圖片。

除此之外還有一個取消這次任務的方法:

//取消task
- (void)cancelImageDownloadTask {
    if (self.af_activeImageDownloadReceipt != nil) {
        //取消事件回調響應
        [[self.class sharedImageDownloader] cancelTaskForImageDownloadReceipt:self.af_activeImageDownloadReceipt];
        //置空
        [self clearActiveDownloadInformation];
     }
}

其實也是去調用我們之前講過的AFImageDownloader的取消方法。

這個類總共就這么幾行代碼,就完成了我們幾乎沒有人不用的,設置ImageView圖片的方法。當然真正的難點在于AFImageDownloaderAFAutoPurgingImageCache。

接下來我們來總結一下整個請求圖片,緩存,然后設置圖片的流程:
  • 調用- (void)setImageWithURL:(NSURL *)url;時,我們生成
    AFImageDownloader單例,并替我們請求數據。
  • AFImageDownloader會生成一個AFAutoPurgingImageCache替我們緩存生成的數據。當然我們設置的時候,給sessionconfiguration設置了一個系統級別的緩存NSUrlCache,這兩者是互相獨立工作的,互不影響的。
  • 然后AFImageDownloader,就實現下載和協調AFAutoPurgingImageCache去緩存,還有一些取消下載的方法。然后通過回調把數據給到我們的類目UIImageView+AFNetworking,如果成功獲取數據,則由類目設置上圖片,整個流程結束。

經過這三個文件:
UIImageView+AFNetworking、AFImageDownloader、AFAutoPurgingImageCache,至此整個設置網絡圖片的方法結束了。

寫在最后:
  • 對于UIKit的總結,我們就到此為止了,其它部分的擴展,小伙伴們可以自行閱讀,都很簡單,基本上每個類200行左右的代碼。核心功能基本上都是圍繞AFURLSessionManager實現的。

  • 本來想本篇放在三里面完結,想想還是覺得自己...too young too simple...
    但是下一篇應該是一個結束了,我們會講講AF2.x,然后詳細總結一下AF存在的意義。大家任何有疑問或者不同意見的,歡迎評論,樓主會一一回復的。求關注,求贊??。感謝~~

后續文章:

AFNetworking到底做了什么?(終)

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,702評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,143評論 3 415
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事?!?“怎么了?”我有些...
    開封第一講書人閱讀 175,553評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,620評論 1 307
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,416評論 6 405
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,940評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,024評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,170評論 0 287
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,709評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,597評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,784評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,291評論 5 357
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,029評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,407評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,663評論 1 280
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,403評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,746評論 2 370

推薦閱讀更多精彩內容