寫在開頭:
- 大概回憶下,之前我們講了
AFNetworking
整個網路請求的流程,包括request
的拼接,session
代理的轉發,response
的解析。以及對一些bug
的適配,如果你還沒有看過,可以點這裡:
AFNetworking到底做了什麼?
AFNetworking到底做了什麼(二)? - 除此之外我們還單獨的開了一篇講了AF對
https
的處理:
AFNetworking之於https認證 - 本文將涉及部分AF對UIKit的擴充套件與圖片下載相關快取的實現,文章內容相對獨立,如果沒看過前文,也不影響閱讀。
回到正文:
我們來看看AF對UIkit
的擴充套件:
一共如上這個多類,下面我們開始著重講其中兩個UIKit的擴充套件:
- 一個是我們網路請求時狀態列的小菊花。
- 一個是我們幾乎都用到過請求網路圖片的如下一行方法:
1- (void)setImageWithURL:(NSURL *)url ;
我們開始吧:
1.AFNetworkActivityIndicatorManager
這個類的作用相當簡單,就是當網路請求的時候,狀態列上的小菊花就會開始轉:
需要的程式碼也很簡單,只需在你需要它的位置中(比如AppDelegate)匯入類,並加一行程式碼即可:
1 |
#import "AFNetworkActivityIndicatorManager.h" |
1 |
[[AFNetworkActivityIndicatorManager sharedManager] setEnabled:YES]; |
接下來我們來講講這個類的實現:
- 這個類的實現也非常簡單,還記得我們之前講的AF對
NSURLSessionTask
中做了一個Method Swizzling嗎?大意是把它的resume
和suspend
方法做了一個替換,在原有實現的基礎上新增了一個通知的傳送。 - 這個類就是基於這兩個通知和task完成的通知來實現的。
首先我們來看看它的初始化方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
+ (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是一個列舉:
12345678910typedef NS_ENUM(NSInteger, AFNetworkActivityManagerState) {//沒有請求AFNetworkActivityManagerStateNotActive,//請求延遲開始AFNetworkActivityManagerStateDelayingStart,//請求進行中AFNetworkActivityManagerStateActive,//請求延遲結束AFNetworkActivityManagerStateDelayingEnd};
這個state一共如上4種狀態,其中兩種應該很好理解,而延遲開始和延遲結束怎麼理解呢?- 原來這是AF對請求菊花顯示做的一個優化處理,試問如果一個請求時間很短,那麼菊花很可能閃一下就結束了。如果很多請求過來,那麼菊花會不停的閃啊閃,這顯然並不是我們想要的效果。
- 所以多了這兩個引數:
1)在一個請求開始的時候,我延遲一會在去轉菊花,如果在這延遲時間內,請求結束了,那麼我就不需要去轉菊花了。
2)但是一旦轉菊花開始,哪怕很短請求就結束了,我們還是會去轉一個時間再去結束,這時間就是延遲結束的時間。
- 緊接著我們監聽了三個通知,用來監聽當前正在進行的網路請求的狀態。
- 然後設定了我們前面提到的這個轉菊花延遲開始和延遲結束的時間,這兩個預設值如下:
12static NSTimeInterval const kDefaultAFNetworkActivityManagerActivationDelay = 1.0;static NSTimeInterval const kDefaultAFNetworkActivityManagerCompletionDelay = 0.17;
接著我們來看看三個通知觸發呼叫的方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
//請求開始 - (void)networkRequestDidStart:(NSNotification *)notification { if ([AFNetworkRequestFromNotification(notification) URL]) { //增加請求活躍數 [self incrementActivityCount]; } } //請求結束 - (void)networkRequestDidFinish:(NSNotification *)notification { //AFNetworkRequestFromNotification(notification)返回這個通知的request,用來判斷request是否是有效的 if ([AFNetworkRequestFromNotification(notification) URL]) { //減少請求活躍數 [self decrementActivityCount]; } } |
方法很簡單,就是開始的時候增加了請求活躍數,結束則減少。呼叫瞭如下兩個方法進行加減:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
//增加請求活躍數 - (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
我們接著來看看這個方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
- (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
:
123456//判斷是否活躍- (BOOL)isNetworkActivityOccurring {@synchronized(self) {return self.activityCount > 0;}}
那麼這個方法應該不難理解了。
這個類複寫了currentState的set方法,每當我們改變這個state,就會觸發set方法,而怎麼該轉菊花也在該方法中:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
//設定當前小菊花狀態 - (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:
方法:
1234567891011121314151617- (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,則呼叫:
1[[UIApplication sharedApplication] setNetworkActivityIndicatorVisible:networkActivityIndicatorVisible];
去轉菊花。
回到state的set方法中,我們除了控制菊花去轉,還呼叫了以下4個方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
//開始任務到結束的時間,預設為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個方法分別是開始延遲執行一個方法,和結束的時候延遲執行一個方法,和對應這兩個方法的取消。其作用,註釋應該很容易理解。
我們繼續往下看,這兩個延遲呼叫的到底是什麼:
1 2 3 4 5 6 7 8 9 10 11 |
- (void)activationDelayTimerFired { //活躍狀態,即活躍數大於1才轉 if (self.networkActivityOccurring) { [self setCurrentState:AFNetworkActivityManagerStateActive]; } else { [self setCurrentState:AFNetworkActivityManagerStateNotActive]; } } - (void)completionDelayTimerFired { [self setCurrentState:AFNetworkActivityManagerStateNotActive]; } |
一個開始,一個完成呼叫,都設定了不同的currentState的值,又回到之前state
的set
方法中了。
至此這個AFNetworkActivityIndicatorManager
類就講完了,程式碼還是相當簡單明瞭的。
2.UIImageView+AFNetworking
接下來我們來講一個我們經常用的方法,這個方法的實現類是:UIImageView+AFNetworking.h
。
這是個類目,並且給UIImageView擴充套件了4個方法:
1 2 3 4 5 6 7 8 9 |
- (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:
先看看初始化方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
//該類為單例 + (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
,這個一個列舉值:
123456typedef NS_ENUM(NSInteger, AFImageDownloadPrioritization) {//先進先出AFImageDownloadPrioritizationFIFO,//後進先出AFImageDownloadPrioritizationLIFO};
這個列舉值代表著,一堆圖片下載,執行任務的順序。 - 還有一個
AFAutoPurgingImageCache
的建立,這個類是AF做圖片快取用的。這裡我們暫時就這麼理解它,講完當前類,我們再來補充它。 - 除此之外,我們還看到一個cache:
1configuration.URLCache = [AFImageDownloader defaultURLCache];
1234567//設定一個系統快取,記憶體快取為20M,磁碟快取為150M,//這個是系統級別維護的快取。+ (NSURLCache *)defaultURLCache {return [[NSURLCache alloc] initWithMemoryCapacity:20 * 1024 * 1024diskCapacity:150 * 1024 * 1024diskPath:@"com.alamofire.imagedownloader"];}
大家看到這可能迷惑了,怎麼這麼多cache,那AF做圖片快取到底用哪個呢?答案是AF自己控制的圖片快取用AFAutoPurgingImageCache
,而NSUrlRequest
的快取由它自己內部根據策略去控制,用的是NSURLCache
,不歸AF處理,只需在configuration中設定上即可。- 那麼看到這有些小夥伴又要問了,為什麼不直接用
NSURLCache
,還要自定義一個AFAutoPurgingImageCache
呢?原來是因為NSURLCache
的諸多限制,例如只支援get請求等等。而且因為是系統維護的,我們自己的可控度不強,並且如果需要做一些自定義的快取處理,無法實現。 - 更多關於
NSURLCache
的內容,大家可以自行查閱。
- 那麼看到這有些小夥伴又要問了,為什麼不直接用
接著上面的方法呼叫到這個最終的初始化方法中:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
- (instancetype)initWithSessionManager:(AFHTTPSessionManager *)sessionManager downloadPrioritization:(AFImageDownloadPrioritization)downloadPrioritization maximumActiveDownloads:(NSInteger)maximumActiveDownloads imageCache:(id )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的方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 |
- (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中,同步的執行的,這是因為這個方法絕大多數的操作都是需要執行緒安全的。可以對著原始碼和註釋來看,我們在這講下它做了什麼:
- 首先做了一個url的判斷,如果為空則返回失敗Block。
- 判斷這個需要請求的url,是不是已經被生成的task中,如果是的話,則多新增一個回撥處理就可以。回撥處理物件為
AFImageDownloaderResponseHandler
。這個類非常簡單,總共就如下3個屬性:
1234567891011121314151617@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 *)uuidsuccess:(nullable void (^)(NSURLRequest *request, NSHTTPURLResponse * _Nullable response, UIImage *responseObject))successfailure:(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完成的時候,會呼叫我們新增的回撥。 - 關於
AFImageDownloaderMergedTask
,我們在這裡都用的是這種型別的task,其實這個task也很簡單:
12345678910111213141516171819202122232425@interface AFImageDownloaderMergedTask : NSObject@property (nonatomic, strong) NSString *URLIdentifier;@property (nonatomic, strong) NSUUID *identifier;@property (nonatomic, strong) NSURLSessionDataTask *task;@property (nonatomic, strong) NSMutableArray *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
,多加了幾個引數,URLIdentifier
和identifier
都是用來標識這個task的,responseHandlers是用來儲存task完成後的回撥的,裡面可以存一組,當任務完成時候,裡面的回撥都會被呼叫。 - 接著去根據快取策略,去載入快取,如果有快取,從
self.imageCache
中返回快取,否則繼續往下走。 - 走到這說明沒相同url的task,也沒有cache,那麼就開始一個新的task,呼叫的是
AFUrlSessionManager
裡的請求方法生成了一個task(這裡我們就不贅述了,可以看之前的樓主之前的文章)。然後做了請求完成的處理。注意,這裡處理實在我們一開始初始化的並行queue:self.responseQueue
中的,這裡的響應處理是多執行緒併發進行的。
1)完成,則呼叫如下方法把這個task從全域性字典中移除:
12345678//移除task相關,用同步序列的形式,防止移除中出現重複移除一系列問題- (AFImageDownloaderMergedTask*)safelyRemoveMergedTaskWithURLIdentifier:(NSString *)URLIdentifier {__block AFImageDownloaderMergedTask *mergedTask = nil;dispatch_sync(self.synchronizationQueue, ^{mergedTask = [self removeMergedTaskWithURLIdentifier:URLIdentifier];});return mergedTask;}
2)去迴圈這個task的responseHandlers
,呼叫它的成功或者失敗的回撥。
3)並且呼叫下面兩個方法,去減少正在請求的任務數,和開啟下一個任務:
123456789101112131415161718192021222324252627//減少活躍的任務數- (void)safelyDecrementActiveTaskCount {//回到序列queue去-dispatch_sync(self.synchronizationQueue, ^{if (self.activeRequestCount > 0) {self.activeRequestCount -= 1;}});}//如果可以,則開啟下一個任務- (void)safelyStartNextTaskIfNecessary {//回到序列queuedispatch_sync(self.synchronizationQueue, ^{//先判斷並行數限制if ([self isActiveRequestCountBelowMaximumLimit]) {while (self.queuedMergedTasks.count > 0) {//獲取陣列中第一個taskAFImageDownloaderMergedTask *mergedTask = [self dequeueMergedTask];//如果狀態是掛起狀態if (mergedTask.task.state == NSURLSessionTaskStateSuspended) {[self startMergedTask:mergedTask];break;}}}});}
這裡需要注意的是,跟我們本類的一些資料相關的操作,都是在我們一開始的序列queue中同步進行的。
4)除此之外,如果成功,還把成功請求到的資料,加到AF自定義的cache中:
12//成功根據request,往cache裡新增[strongSelf.imageCache addImage:responseObject forRequest:request withAdditionalIdentifier:nil]; - 用
NSUUID
生成的唯一標識,去生成AFImageDownloaderResponseHandler
,然後生成一個AFImageDownloaderMergedTask
,把之前第5步生成的createdTask
和回撥都繫結給這個AF自定義可合併回撥的task,然後這個task加到全域性的task對映字典中,key為url:
1self.mergedTasks[URLIdentifier] = mergedTask; - 判斷當前正在下載的任務是否超過最大並行數,如果沒有則開始下載,否則先加到等待的陣列中去:
1234567//如果小於最大並行數,則開始任務下載resumeif ([self isActiveRequestCountBelowMaximumLimit]) {[self startMergedTask:mergedTask];} else {[self enqueueMergedTask:mergedTask];}
123//判斷並行數限制- (BOOL)isActiveRequestCountBelowMaximumLimit {return self.activeRequestCount
12345678910111213141516171819//開始下載- (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。
- 如果暫時不能下載,被加到等待下載的陣列中去的話,會根據我們一開始設定的下載策略,是先進先出,還是後進先出,去插入這個下載任務。
- 最後判斷這個mergeTask是否為空。不為空,我們生成了一個
AFImageDownloadReceipt
,繫結了一個UUID。否則為空返回nil:
123456if (task) {//建立一個AFImageDownloadReceipt並返回,裡面就多一個receiptID。return [[AFImageDownloadReceipt alloc] initWithReceiptID:receiptID task:task];} else {return nil;}
這個AFImageDownloadReceipt
僅僅是多封裝了一個UUID:
123456789101112@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的取消的方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
//根據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]; } }); } |
1 2 3 4 5 6 |
//根據URLIdentifier移除task - (AFImageDownloaderMergedTask *)removeMergedTaskWithURLIdentifier:(NSString *)URLIdentifier { AFImageDownloaderMergedTask *mergedTask = self.mergedTasks[URLIdentifier]; [self.mergedTasks removeObjectForKey:URLIdentifier]; return mergedTask; } |
方法比較簡單,大家自己看看就好。至此`AFImageDownloader
這個類講完了。如果大家看的感覺比較繞,沒關係,等到最後我們一起來總結一下,捋一捋。
我們之前講到AFAutoPurgingImageCache
這個類略過去了,現在我們就來補充一下這個類的相關內容:
首先來講講這個類的作用,它是AF自定義用來做圖片快取的。我們來看看它的初始化方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
- (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; } |
初始化方法很簡單,總結一下:
- 宣告瞭一個預設的記憶體快取大小100M,還有一個意思是如果超出100M之後,我們去清除快取,此時仍要保留的快取大小60M。(如果還是不理解,可以看後文,原始碼中會講到)
- 建立了一個並行queue,這個並行queue,這個類除了初始化以外,所有的方法都是在這個並行queue中呼叫的。
- 建立了一個cache字典,我們所有的快取資料,都被儲存在這個字典中,key為url,value為
AFCachedImage
。
關於這個AFCachedImage
,其實就是Image之外封裝了幾個關於這個快取的引數,如下:
123456789101112131415161718192021222324252627@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;} - 新增了一個通知,監聽記憶體警告,當發成記憶體警告,呼叫該方法,移除所有的快取,並且把當前快取數置為0:
123456789101112//移除所有圖片- (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_sync
與dispatch_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。
- 在這裡用了
接著我們來看看這個類最核心的一個方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 |
//新增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 *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; } }); } |
看註釋應該很容易明白,這個方法做了兩件事:
- 設定快取到字典裡,並且把對應的快取大小設定到當前已快取的數量屬性中。
- 判斷是快取超出了我們設定的最大快取100M,如果是的話,則清除掉部分早時間的快取,清除到快取小於我們溢位後保留的記憶體60M以內。
當然在這裡更需要說一說的是dispatch_barrier_async
,這裡整個類都沒有使用dispatch_async
,所以不存在是為了做一個柵欄,來同步上下文的執行緒。其實它在本類中的作用很簡單,就是一個序列執行。
- 講到這,小夥伴們又疑惑了,既然就是隻是為了序列,那為什麼我們不用一個序列queue就得了?非得用
dispatch_barrier_async
幹嘛?其實小夥伴要是看的仔細,就明白了,上文我們說過,我們要用dispatch_barrier_sync
來保證執行緒安全。如果我們使用序列queue,那麼執行緒是極其容易死鎖的。
還有剩下的幾個方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
//根據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
也講完了,我們還是等到最後再來總結。
我們繞了一大圈,總算回到了UIImageView+AFNetworking
這個類,現在圖片下載的方法,和快取的方法都有了,實現這個類也是水到渠成的事了。
我們來看下面我們絕大多數人很熟悉的方法,看看它的實現:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
- (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。接著呼叫到第三個方法,也是這個類目唯一一個重要的方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 |
- (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 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,呼叫如下方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
//取消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
這麼一個屬性,看看定義的地方:
12345678910111213@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
,成功則設定圖片。
除此之外還有一個取消這次任務的方法:
1 2 3 4 5 6 7 8 9 |
//取消task - (void)cancelImageDownloadTask { if (self.af_activeImageDownloadReceipt != nil) { //取消事件回撥響應 [[self.class sharedImageDownloader] cancelTaskForImageDownloadReceipt:self.af_activeImageDownloadReceipt]; //置空 [self clearActiveDownloadInformation]; } } |
其實也是去呼叫我們之前講過的AFImageDownloader
的取消方法。
這個類總共就這麼幾行程式碼,就完成了我們幾乎沒有人不用的,設定ImageView圖片的方法。當然真正的難點在於AFImageDownloader
和AFAutoPurgingImageCache
。
接下來我們來總結一下整個請求圖片,快取,然後設定圖片的流程:
- 呼叫
- (void)setImageWithURL:(NSURL *)url;
時,我們生成
AFImageDownloader
單例,並替我們請求資料。 - 而
AFImageDownloader
會生成一個AFAutoPurgingImageCache
替我們快取生成的資料。當然我們設定的時候,給session
的configuration
設定了一個系統級別的快取NSUrlCache
,這兩者是互相獨立工作的,互不影響的。 - 然後
AFImageDownloader
,就實現下載和協調AFAutoPurgingImageCache
去快取,還有一些取消下載的方法。然後通過回撥把資料給到我們的類目UIImageView+AFNetworking
,如果成功獲取資料,則由類目設定上圖片,整個流程結束。
經過這三個檔案:
UIImageView+AFNetworking
、AFImageDownloader
、AFAutoPurgingImageCache
,至此整個設定網路圖片的方法結束了。
寫在最後:
- 對於UIKit的總結,我們就到此為止了,其它部分的擴充套件,小夥伴們可以自行閱讀,都很簡單,基本上每個類200行左右的程式碼。核心功能基本上都是圍繞
AFURLSessionManager
實現的。 - 本來想本篇放在三裡面完結,想想還是覺得自己…too young too simple…
但是下一篇應該是一個結束了,我們會講講AF2.x,然後詳細總結一下AF存在的意義。大家任何有疑問或者不同意見的,歡迎評論,樓主會一一回復的。求關注,求贊?。感謝~~