我们应该看到过很多类似这样的例子:某个控件拥有加载网络图片的能力。但这究竟是怎么做到的呢?看完这篇文章就明白了。

前言

这篇我们会介绍 AFNetworking 中的3个UIKit中的分类。UIActivityIndicatorView UIRefreshControl UIImageView。读完本篇就能够明白控件是如何显示网络图片的。那么如果你有兴趣,可以尝试让一个控件的layer也能够加载网络图片。

提供的功能

我们解读源码不仅仅是了解内部实现原理,还要让开发者明白在这些分类中我能够使用那些功能,因此在这个 提供的功能 小结中,我会把这3个分类提供的功能罗列出来,即使不看下边的源码解读,也会有所收获。

  1. UIActivityIndicatorView+AFNetworking UIActivityIndicatorView的这个分类最简单,它只提供了一个方法:setAnimatingWithStateOfTask: 只要给UIActivityIndicatorView一个 task UIActivityIndicatorView会根据数据的加载情况 自动 开始动画或者结束动画。
  2. UIRefreshControl+AFNetworking UIRefreshControl的这个分类的使用跟上边的UIActivityIndicatorView+AFNetworking一模一样。
  3. 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,不明白的可以看这篇 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: 我不可能凭空出现,通过这个方法创建我。
  • 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];
// 取出缓存
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 就调动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;
}
}

方法不是最重要的,重要是梳理出这一整套的逻辑和想法,下面我们就来分析分析。

  1. 首先我们规定,使用这个分类每加载一次图片生成一个af_activeImageDownloadReceipt凭据,这个凭据一旦下载完成后,需要置为nil。
  2. 我们使用上边的这个最长的方法来加载图片。
  3. 我们先判断这个urlRequest是不是有效的。有效就继续往下走,无效的话取消之前的下载,然后赋值替代图片。说明如果urlRequest失效,同时也取消了之前的下载
  4. 好,到这里,说明urlRequest是正确的,那么我们再判断是不是现在下载的跟之前正在下载的URL是一样的?存在这样一种操作,我写了两个上边的方法
  5. 这一步要取消之前的下载任务
  6. 在缓存中取图片,如果图片存在,那么再看看是否设置了success,设置了就调用这个block,否则就使用替代图片。
  7. 请求失败处理方法同上边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

AFNetworking 3.0 源码解读(八)之 AFImageDownloader

AFNetworking 3.0 源码解读(九)之 AFNetworkActivityIndicatorManager

AFNetworking 3.0 源码解读(十)之 UIActivityIndicatorView/UIRefreshControl/UIImageView + AFNetworking的更多相关文章

  1. AFNetworking 3.0 源码解读(十一)之 UIButton/UIProgressView/UIWebView + AFNetworking

    AFNetworking的源码解读马上就结束了,这一篇应该算是倒数第二篇,下一篇会是对AFNetworking中的技术点进行总结. 前言 上一篇我们总结了 UIActivityIndicatorVie ...

  2. AFNetworking 3.0 源码解读(九)之 AFNetworkActivityIndicatorManager

    让我们的APP像艺术品一样优雅,开发工程师更像是一名匠人,不仅需要精湛的技艺,而且要有一颗匠心. 前言 AFNetworkActivityIndicatorManager 是对状态栏中网络激活那个小控 ...

  3. AFNetworking 3.0 源码解读(八)之 AFImageDownloader

    AFImageDownloader 这个类对写DownloadManager有很大的借鉴意义.在平时的开发中,当我们使用UIImageView加载一个网络上的图片时,其原理就是把图片下载下来,然后再赋 ...

  4. AFNetworking 3.0 源码解读(七)之 AFAutoPurgingImageCache

    这篇我们就要介绍AFAutoPurgingImageCache这个类了.这个类给了我们临时管理图片内存的能力. 前言 假如说我们要写一个通用的网络框架,除了必备的请求数据的方法外,必须提供一个下载器来 ...

  5. AFNetworking 3.0 源码解读(六)之 AFHTTPSessionManager

    AFHTTPSessionManager相对来说比较好理解,代码也比较短.但却是我们平时可能使用最多的类. AFNetworking 3.0 源码解读(一)之 AFNetworkReachabilit ...

  6. AFNetworking 3.0 源码解读(四)之 AFURLResponseSerialization

    本篇是AFNetworking 3.0 源码解读的第四篇了. AFNetworking 3.0 源码解读(一)之 AFNetworkReachabilityManager AFNetworking 3 ...

  7. AFNetworking 3.0 源码解读(五)之 AFURLSessionManager

    本篇是AFNetworking 3.0 源码解读的第五篇了. AFNetworking 3.0 源码解读(一)之 AFNetworkReachabilityManager AFNetworking 3 ...

  8. AFNetworking 3.0 源码解读 总结(干货)(下)

    承接上一篇AFNetworking 3.0 源码解读 总结(干货)(上) 21.网络服务类型NSURLRequestNetworkServiceType 示例代码: typedef NS_ENUM(N ...

  9. AFNetworking 3.0 源码解读(三)之 AFURLRequestSerialization

    这篇就讲到了跟请求相关的类了 关于AFNetworking 3.0 源码解读 的文章篇幅都会很长,因为不仅仅要把代码进行详细的的解释,还会大概讲解和代码相关的知识点. 上半篇: URI编码的知识 关于 ...

随机推荐

  1. 干货分享:SQLSERVER使用裸设备

    干货分享:SQLSERVER使用裸设备 这篇文章也适合ORACLE DBA和MYSQL DBA 阅读 裸设备适用于Linux和Windows 在ORACLE和MYSQL里也是支持裸设备的!! 介绍 大 ...

  2. Entity Framework 6 Recipes 2nd Edition 译 -> 目录 -持续更新

    因为看了<Entity Framework 6 Recipes 2nd Edition>这本书前面8章的翻译,感谢china_fucan. 从第九章开始,我是边看边译的,没有通读,加之英语 ...

  3. 4.Windows Server2012 R2里面部署 MVC 的网站

    网站部署之~Windows Server | 本地部署:http://www.cnblogs.com/dunitian/p/4822808.html#iis 后期会在博客首发更新:http://dnt ...

  4. redux学习

    redux学习: 1.应用只有一个store,用于保存整个应用的所有的状态数据信息,即state,一个state对应一个页面的所需信息 注意:他只负责保存state,接收action, 从store. ...

  5. SignalR SelfHost实时消息,集成到web中,实现服务器消息推送

    先前用过两次SignalR,但是中途有段时间没弄了,今天重新弄,发现已经忘得差不多了,做个笔记! 首先创建一个控制台项目Nuget添加引用联机搜索:Microsoft.AspNet.SignalR.S ...

  6. git提交项目到已存在的远程分支

    今天想提交项目到github的远程分支上,那个远程分支是之前就创建好的,而我的本地关联分支还没创建.   之前从未用github提交到远程分支过,弄了半个钟,看了几篇博文,终于折腾出来.现在把步骤整理 ...

  7. 元素绝对居中终极办法兼容IE8

    <!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title> ...

  8. .NET Core 1.0.1 升级汇总

    ASP.NET Core BUG fix: ASP.NET Routing Port fix for "Request not matching route with defaults&qu ...

  9. 一步步开发自己的博客 .NET版(3、注册登录功能)

    前言 这次开发的博客主要功能或特点:    第一:可以兼容各终端,特别是手机端.    第二:到时会用到大量html5,炫啊.    第三:导入博客园的精华文章,并做分类.(不要封我)    第四:做 ...

  10. 说说DOM的那些事儿

    引子 先来一颗栗子: <img src="/sub/123.jpg" alt="test" /> <script type="tex ...