我们应该看到过很多类似这样的例子:某个控件拥有加载网络图片的能力。但这究竟是怎么做到的呢?看完这篇文章就明白了。
前言
这篇我们会介绍 AFNetworking 中的3个UIKit中的分类。UIActivityIndicatorView UIRefreshControl UIImageView。读完本篇就能够明白控件是如何显示网络图片的。那么如果你有兴趣,可以尝试让一个控件的layer也能够加载网络图片。
提供的功能
我们解读源码不仅仅是了解内部实现原理,还要让开发者明白在这些分类中我能够使用那些功能,因此在这个 提供的功能 小结中,我会把这3个分类提供的功能罗列出来,即使不看下边的源码解读,也会有所收获。
UIActivityIndicatorView+AFNetworking
UIActivityIndicatorView的这个分类最简单,它只提供了一个方法:setAnimatingWithStateOfTask:
只要给UIActivityIndicatorView一个 task UIActivityIndicatorView会根据数据的加载情况 自动 开始动画或者结束动画。UIRefreshControl+AFNetworking
UIRefreshControl的这个分类的使用跟上边的UIActivityIndicatorView+AFNetworking
一模一样。UIImageView+AFNetworking
UIImageView是最常用的显示图片的控件。额外增加了 placeholderImage(替代图片) 这个属性和 success failure 这两个block来自定义一些事件。最后增加了两个取消某个状态下的图片下载的方法。我们看下边的图片就好了:
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
的一个方法。这个方法能够提供根据task自动开始和结束动画的功能
这个分类需要依赖 AFNetworking。需要监听AFNetworking中的网络状态的通知。按照通常的想法是,只要我监听了通知然后设置自己的状态就完事了。然而这并不是好的设计。一个控件的某项新的功能应该交给一个专门负责这个功能的人去完成,这才是好的设计。
因此我们给UIActivityIndicatorView扩展了一个属性af_notificationObserver
,这个属性是专门处理上边说的事件的管理者。
好吧,我们写出伪代码:
- (通知监听者 *)af_notificationObserver { return 通知监听者;}- (void)setAnimatingWithStateOfTask:(NSURLSessionTask *)task { 监听者根据task来做一些事;}
这样写的好处是:当我们想扩展别的功能的时候,只需要在添加一个其他功能的负责人就可以,所有的逻辑都是负责人自己实现。这种思想简直完美。我们看 AFNetworking 中对上边伪代码的实现。相信大多数朋友应该知道,往分类中添加属性使用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:
我不可能凭空出现,通过这个方法创建我。setAnimatingWithStateOfTask:
我就是通过这个方法来操控UIActivityIndicatorView的。
这么看来,这个af_notificationObserver只需要上边3个东东就足够了,那么我们就剩下setAnimatingWithStateOfTask:
这个方法的实现了。
- (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的状态不等于完成 if (task.state != NSURLSessionTaskStateCompleted) { #pragma clang diagnostic push#pragma clang diagnostic ignored "-Wreceiver-is-weak"#pragma clang diagnostic ignored "-Warc-repeated-use-of-weak" // 状态为运行中就开始,否则为停止 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 那篇文章中提到过,要异步显示网络上的图片,就要把图片数据缓存下来才行。因此,要赋予UIImageView这项功能,就需要使用 AFImageDownloader 来获取图片数据。
不知道大家发现没有,像这张图片中的这些方法,,我们只需要实现参数最多的那个方法就行了。这应该就是所谓的 尾调函数 吧。
首先我们先看看UIImageView扩展的一个属性af_activeImageDownloadReceipt,这个属性是图片依据
@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)
设置了关联值,同样的原理。 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方法,其中第二个参数是一个地址,因此我们可以用@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]; // 取出缓存 idimageCache = downloader.imageCache; //Use the image from the image cache if it exists UIImage *cachedImage = [imageCache imageforRequest:urlRequest withAdditionalIdentifier:nil]; if (cachedImage) { // 如果写了success Block 就调动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; }}
方法不是最重要的,重要是梳理出这一整套的逻辑和想法,下面我们就来分析分析。
- 首先我们规定,使用这个分类每加载一次图片生成一个af_activeImageDownloadReceipt凭据,这个凭据一旦下载完成后,需要置为nil。
- 我们使用上边的这个最长的方法来加载图片。
- 我们先判断这个urlRequest是不是有效的。有效就继续往下走,无效的话取消之前的下载,然后赋值替代图片。说明如果urlRequest失效,同时也取消了之前的下载
- 好,到这里,说明urlRequest是正确的,那么我们再判断是不是现在下载的跟之前正在下载的URL是一样的?存在这样一种操作,我写了两个上边的方法
- 这一步要取消之前的下载任务
- 在缓存中取图片,如果图片存在,那么再看看是否设置了success,设置了就调用这个block,否则就使用替代图片。
- 请求失败处理方法同上边6.一样。
总结
通过对上边的方法的解读,我们就很容易的给别的控件添加异步加载功能了。使用上边的方法且改动很少的代码就能完成。