AFNetworking 3.0 源码解读(十)之 UIActivityIndicatorView/UIRefreshControl/UIImageView + AFNetworking

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

前言

这篇我们会介绍 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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,761评论 5 460
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,953评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,998评论 0 320
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,248评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,130评论 4 356
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,145评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,550评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,236评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,510评论 1 291
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,601评论 2 310
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,376评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,247评论 3 313
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,613评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,911评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,191评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,532评论 2 342
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,739评论 2 335

推荐阅读更多精彩内容