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

前言

这篇我们会介绍 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将一个文件组的数据移动到另一个文件组 有经验的大侠可以直接忽视这篇文章~ 这个问题有经验的人都知道怎麽做,因为我们公司的数据量不大没有这个需求,也不知道怎麽做实验 今天求助了QQ群里 ...

  2. 简单有效的kmp算法

    以前看过kmp算法,当时接触后总感觉好深奥啊,抱着数据结构的数啃了一中午,最终才大致看懂,后来提起kmp也只剩下“奥,它是做模式匹配的”这点干货.最近有空,翻出来算法导论看看,原来就是这么简单(先不说 ...

  3. 学习ASP.NET Core,怎能不了解请求处理管道[1]: 中间件究竟是个什么东西?

    ASP.NET Core管道虽然在结构组成上显得非常简单,但是在具体实现上却涉及到太多的对象,所以我们在 "通过重建Hosting系统理解HTTP请求在ASP.NET Core管道中的处理流 ...

  4. PHP 获取 特定时间范围 类

    目录  前序 用途 功能及事项 使用方法 代码及注释 前序: 总体来说,我更应该是一个 android 移动开发者,而不是一个 phper,如果说只做移动端的 APP ,我也不会学这么多,这 2年来, ...

  5. 代码的坏味道(19)——狎昵关系(Inappropriate Intimacy)

    坏味道--狎昵关系(Inappropriate Intimacy) 特征 一个类大量使用另一个类的内部字段和方法. 问题原因 类和类之间应该尽量少的感知彼此(减少耦合).这样的类更容易维护和复用. 解 ...

  6. PHP static静态属性和静态方法

    这里分析了php面向对象中static静态属性和静态方法的调用.关于它们的调用(能不能调用,怎么样调用),需要弄明白了他们在内存中存放位置,这样就非常容易理解了.静态属性.方法(包括静态与非静态)在内 ...

  7. 解决:SharePoint当中的STP网站列表模板没有办法导出到其它语言环境中使用

    首在在你的英文版本上,导出列表或是网站的模板,这个文件可能是这样滴:template.stp 把这个文件 template.stp 命名为 template.cab 解压 这个 *.cab 文件 在解 ...

  8. Oracle Standard Error 列表

    今天,我特意从网上找了一些,以及自己平时总结的,关于错误编号和说明,平时我们在写项目的时候,往往是可能会出现下面这些错误,例如:违反唯一约束,无效的会话ID,等等.希望对大家有点帮助!可以看看,如果有 ...

  9. 【教程】SQLite数据库修复

    SQLite 大家都知道,就不多说了. 有时候数据量大了,或者存储过程中出现异常,数据库就可能会出问题. 这是以前公司产品出现过的问题,导致软件都打不开了,我花了不少时间才解决的,趁现在有空贡献出来. ...

  10. opengl 笔记(2)

    /*- * Opengl Demo Test * * Fredric : 2016-7-10 */ #include <GLUT/GLUT.h> #include <stdlib.h ...