我們應該看到過很多類似這樣的例子:某個控件擁有加載網(wǎng)絡圖片的能力纷妆。但這究竟是怎么做到的呢腌闯?看完這篇文章就明白了煌寇。
前言
這篇我們會介紹 AFNetworking 中的3個UIKit中的分類楚堤。UIActivityIndicatorView UIRefreshControl UIImageView。讀完本篇就能夠明白控件是如何顯示網(wǎng)絡圖片的塔粒。那么如果你有興趣结借,可以嘗試讓一個控件的layer也能夠加載網(wǎng)絡圖片。
提供的功能
我們解讀源碼不僅僅是了解內(nèi)部實現(xiàn)原理卒茬,還要讓開發(fā)者明白在這些分類中我能夠使用那些功能船老,因此在這個 提供的功能 小結中,我會把這3個分類提供的功能羅列出來圃酵,即使不看下邊的源碼解讀柳畔,也會有所收獲。
-
UIActivityIndicatorView+AFNetworking
UIActivityIndicatorView的這個分類最簡單辜昵,它只提供了一個方法:setAnimatingWithStateOfTask:
只要給UIActivityIndicatorView一個 task UIActivityIndicatorView會根據(jù)數(shù)據(jù)的加載情況 自動 開始動畫或者結束動畫荸镊。 -
UIRefreshControl+AFNetworking
UIRefreshControl的這個分類的使用跟上邊的UIActivityIndicatorView+AFNetworking
一模一樣咽斧。 -
UIImageView+AFNetworking
UIImageView是最常用的顯示圖片的控件堪置。額外增加了 placeholderImage(替代圖片) 這個屬性和 success failure 這兩個block來自定義一些事件。最后增加了兩個取消某個狀態(tài)下的圖片下載的方法张惹。我們看下邊的圖片就好了:
UIActivityIndicatorView+AFNetworking
This category adds methods to the UIKit framework's
UIActivityIndicatorView
class. The methods in this category provide support for automatically starting and stopping animation depending on the loading state of a session task.這個分類增加了
UIActivityIndicatorView
的一個方法舀锨。這個方法能夠提供根據(jù)task自動開始和結束動畫的功能
這個分類需要依賴 AFNetworking。需要監(jiān)聽AFNetworking中的網(wǎng)絡狀態(tài)的通知宛逗。按照通常的想法是坎匿,只要我監(jiān)聽了通知然后設置自己的狀態(tài)就完事了。然而這并不是好的設計。一個控件的某項新的功能應該交給一個專門負責這個功能的人去完成替蔬,這才是好的設計告私。
因此我們給UIActivityIndicatorView擴展了一個屬性af_notificationObserver
,這個屬性是專門處理上邊說的事件的管理者。
好吧承桥,我們寫出偽代碼:
- (通知監(jiān)聽者 *)af_notificationObserver {
return 通知監(jiān)聽者;
}
- (void)setAnimatingWithStateOfTask:(NSURLSessionTask *)task {
監(jiān)聽者根據(jù)task來做一些事;
}
這樣寫的好處是:當我們想擴展別的功能的時候驻粟,只需要在添加一個其他功能的負責人就可以,所有的邏輯都是負責人自己實現(xiàn)凶异。這種思想簡直完美蜀撑。我們看 AFNetworking 中對上邊偽代碼的實現(xiàn)。相信大多數(shù)朋友應該知道剩彬,往分類中添加屬性使用Runtime酷麦,不明白的可以看這篇 Objective-C runtime的常見應用.
- (AFActivityIndicatorViewNotificationObserver *)af_notificationObserver {
AFActivityIndicatorViewNotificationObserver *notificationObserver = objc_getAssociatedObject(self, @selector(af_notificationObserver));
if (notificationObserver == nil) {
notificationObserver = [[AFActivityIndicatorViewNotificationObserver alloc] initWithActivityIndicatorView:self];
objc_setAssociatedObject(self, @selector(af_notificationObserver), notificationObserver, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
return notificationObserver;
}
- (void)setAnimatingWithStateOfTask:(NSURLSessionTask *)task {
[[self af_notificationObserver] setAnimatingWithStateOfTask:task];
}
我們來看看這個af_notificationObserver有什么話要說呢?
-
UIActivityIndicatorView *activityIndicatorView
既然讓我來管理UIActivityIndicatorView喉恋,那就必須拿到這個控件才行沃饶。 -
initWithActivityIndicatorView:
我不可能憑空出現(xiàn),通過這個方法創(chuàng)建我瀑晒。 -
setAnimatingWithStateOfTask:
我就是通過這個方法來操控UIActivityIndicatorView的绍坝。
這么看來,這個af_notificationObserver只需要上邊3個東東就足夠了苔悦,那么我們就剩下setAnimatingWithStateOfTask:
這個方法的實現(xiàn)了轩褐。
- (void)setAnimatingWithStateOfTask:(NSURLSessionTask *)task {
NSNotificationCenter *notificationCenter = [NSNotificationCenter defaultCenter];
// 移除 AFNetworking 的通知
[notificationCenter removeObserver:self name:AFNetworkingTaskDidResumeNotification object:nil];
[notificationCenter removeObserver:self name:AFNetworkingTaskDidSuspendNotification object:nil];
[notificationCenter removeObserver:self name:AFNetworkingTaskDidCompleteNotification object:nil];
// task != nil
if (task) {
// task的狀態(tài)不等于完成
if (task.state != NSURLSessionTaskStateCompleted) {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wreceiver-is-weak"
#pragma clang diagnostic ignored "-Warc-repeated-use-of-weak"
// 狀態(tài)為運行中就開始,否則為停止
if (task.state == NSURLSessionTaskStateRunning) {
[self.activityIndicatorView startAnimating];
} else {
[self.activityIndicatorView stopAnimating];
}
#pragma clang diagnostic pop
// 移除 AFNetworking 的通知
[notificationCenter addObserver:self selector:@selector(af_startAnimating) name:AFNetworkingTaskDidResumeNotification object:task];
[notificationCenter addObserver:self selector:@selector(af_stopAnimating) name:AFNetworkingTaskDidCompleteNotification object:task];
[notificationCenter addObserver:self selector:@selector(af_stopAnimating) name:AFNetworkingTaskDidSuspendNotification object:task];
}
}
}
#pragma mark -
- (void)af_startAnimating {
dispatch_async(dispatch_get_main_queue(), ^{
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wreceiver-is-weak"
[self.activityIndicatorView startAnimating];
#pragma clang diagnostic pop
});
}
- (void)af_stopAnimating {
dispatch_async(dispatch_get_main_queue(), ^{
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wreceiver-is-weak"
[self.activityIndicatorView stopAnimating];
#pragma clang diagnostic pop
});
}
UIImageView+AFNetworking
我們在 AFImageDownloader 那篇文章中提到過玖详,要異步顯示網(wǎng)絡上的圖片把介,就要把圖片數(shù)據(jù)緩存下來才行。因此蟋座,要賦予UIImageView這項功能拗踢,就需要使用 AFImageDownloader 來獲取圖片數(shù)據(jù)。
不知道大家發(fā)現(xiàn)沒有向臀,像這張圖片中的這些方法巢墅,
首先我們先看看UIImageView擴展的一個屬性af_activeImageDownloadReceipt,這個屬性是圖片依據(jù)
@interface UIImageView (_AFNetworking)
@property (readwrite, nonatomic, strong, setter = af_setActiveImageDownloadReceipt:) AFImageDownloadReceipt *af_activeImageDownloadReceipt;
@end
@implementation UIImageView (_AFNetworking)
- (AFImageDownloadReceipt *)af_activeImageDownloadReceipt {
return (AFImageDownloadReceipt *)objc_getAssociatedObject(self, @selector(af_activeImageDownloadReceipt));
}
- (void)af_setActiveImageDownloadReceipt:(AFImageDownloadReceipt *)imageDownloadReceipt {
objc_setAssociatedObject(self, @selector(af_activeImageDownloadReceipt), imageDownloadReceipt, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
通過運行時為@selector(af_activeImageDownloadReceipt)
設置了關聯(lián)值芹彬,同樣的原理蓄髓。 sharedImageDownloader 也是這么設置的
+ (AFImageDownloader *)sharedImageDownloader {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wgnu"
return objc_getAssociatedObject(self, @selector(sharedImageDownloader)) ?: [AFImageDownloader defaultInstance];
#pragma clang diagnostic pop
}
+ (void)setSharedImageDownloader:(AFImageDownloader *)imageDownloader {
objc_setAssociatedObject(self, @selector(sharedImageDownloader), imageDownloader, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
在這里說下這個objc_setAssociatedObject方法,其中第二個參數(shù)是一個地址舒帮,因此我們可以用@selector
或者自定義一個全局的const字段会喝,取它的地址陡叠。 看下邊的例子,我為UIImageView擴展了一個屬性abc肢执。
static const NSString *abcde;
@interface UIImageView (_AFNetworking)
@property (readwrite, nonatomic, strong, setter = af_setActiveImageDownloadReceipt:) AFImageDownloadReceipt *af_activeImageDownloadReceipt;
@property (readwrite, nonatomic, strong)NSString *abc;
@end
@implementation UIImageView (_AFNetworking)
- (AFImageDownloadReceipt *)af_activeImageDownloadReceipt {
return (AFImageDownloadReceipt *)objc_getAssociatedObject(self, @selector(af_activeImageDownloadReceipt));
}
- (void)af_setActiveImageDownloadReceipt:(AFImageDownloadReceipt *)imageDownloadReceipt {
objc_setAssociatedObject(self, @selector(af_activeImageDownloadReceipt), imageDownloadReceipt, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
- (void)setAbc:(NSString *)abc {
objc_setAssociatedObject(self, &abcde, abc, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
- (NSString *)abc {
return objc_getAssociatedObject(self, &abcde);
}
我在使用的時候
UIImageView *imageView = [[UIImageView alloc] init];
[imageView setValue:@"qwer" forKey:@"abc"];
NSString *str = [imageView valueForKey:@"abc"];
NSLog(@"%@",str);
--
- (void)cancelImageDownloadTask {
if (self.af_activeImageDownloadReceipt != nil) {
[[self.class sharedImageDownloader] cancelTaskForImageDownloadReceipt:self.af_activeImageDownloadReceipt];
[self clearActiveDownloadInformation];
}
}
- (void)clearActiveDownloadInformation {
self.af_activeImageDownloadReceipt = nil;
}
- (BOOL)isActiveTaskURLEqualToURLRequest:(NSURLRequest *)urlRequest {
return [self.af_activeImageDownloadReceipt.task.originalRequest.URL.absoluteString isEqualToString:urlRequest.URL.absoluteString];
}
來看這個核心方法枉阵,處理手法和之前的代碼如出一轍,值得學習的是预茄,核心方法中的判斷比較詳細岭妖。
- (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
{
// urlRequest 不正確
if ([urlRequest URL] == nil) {
// 取消下載任務
[self cancelImageDownloadTask];
// 賦值替代圖片
self.image = placeholderImage;
return;
}
// 如果當前活動的下載和本下載一樣,就返回
if ([self isActiveTaskURLEqualToURLRequest:urlRequest]){
return;
}
// 取消之前的下載任務
[self cancelImageDownloadTask];
// 取出downloader
AFImageDownloader *downloader = [[self class] sharedImageDownloader];
// 取出緩存
id <AFImageRequestCache> imageCache = downloader.imageCache;
//Use the image from the image cache if it exists
UIImage *cachedImage = [imageCache imageforRequest:urlRequest withAdditionalIdentifier:nil];
if (cachedImage) {
// 如果寫了success Block 就調(diào)動block反璃,但不會給image賦值
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 = [downloader
downloadImageForURLRequest:urlRequest
withReceiptID:downloadID
success:^(NSURLRequest * _Nonnull request, NSHTTPURLResponse * _Nullable response, UIImage * _Nonnull responseObject) {
__strong __typeof(weakSelf)strongSelf = weakSelf;
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;
if ([strongSelf.af_activeImageDownloadReceipt.receiptID isEqual:downloadID]) {
if (failure) {
failure(request, response, error);
}
[strongSelf clearActiveDownloadInformation];
}
}];
self.af_activeImageDownloadReceipt = receipt;
}
}
方法不是最重要的,重要是梳理出這一整套的邏輯和想法淮蜈,下面我們就來分析分析斋攀。
- 首先我們規(guī)定,使用這個分類每加載一次圖片生成一個af_activeImageDownloadReceipt憑據(jù)梧田,這個憑據(jù)一旦下載完成后淳蔼,需要置為nil。
- 我們使用上邊的這個最長的方法來加載圖片裁眯。
- 我們先判斷這個urlRequest是不是有效的鹉梨。有效就繼續(xù)往下走,無效的話取消之前的下載穿稳,然后賦值替代圖片存皂。說明如果urlRequest失效,同時也取消了之前的下載
- 好逢艘,到這里旦袋,說明urlRequest是正確的,那么我們再判斷是不是現(xiàn)在下載的跟之前正在下載的URL是一樣的它改?存在這樣一種操作疤孕,我寫了兩個上邊的方法
- 這一步要取消之前的下載任務
- 在緩存中取圖片,如果圖片存在央拖,那么再看看是否設置了success祭阀,設置了就調(diào)用這個block,否則就使用替代圖片鲜戒。
- 請求失敗處理方法同上邊6.一樣专控。
總結
通過對上邊的方法的解讀,我們就很容易的給別的控件添加異步加載功能了袍啡。使用上邊的方法且改動很少的代碼就能完成踩官。
推薦閱讀
AFNetworking 3.0 源碼解讀(一)之 AFNetworkReachabilityManager
AFNetworking 3.0 源碼解讀(二)之 AFSecurityPolicy
AFNetworking 3.0 源碼解讀(三)之 AFURLRequestSerialization
AFNetworking 3.0 源碼解讀(四)之 AFURLResponseSerialization
AFNetworking 3.0 源碼解讀(五)之 AFURLSessionManager
AFNetworking 3.0 源碼解讀(六)之 AFHTTPSessionManager
AFNetworking 3.0 源碼解讀(七)之 AFAutoPurgingImageCache