AFNetworking 3.0 源码解读之UIImageView

UIImageView

在使用AFNetworking显示网络图片的时候,用到的最关键的方法

- (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
{

    //1、首先,判断请求的图片地址是否有效,无效的话,取消图片下载任务,并将placeholderImage赋值,返回
    if ([urlRequest URL] == nil) {
        [self cancelImageDownloadTask];
        self.image = placeholderImage;
        return;
    }

   //2、接着再判断当前下载请求是否和urlRequest下载一样,一样就返回
    if ([self isActiveTaskURLEqualToURLRequest:urlRequest]){
        return;
    }
  //3、取消之前的下载任务
    [self cancelImageDownloadTask];

 //4、获取downloader,并获取downloader的imageCache图片缓存
    AFImageDownloader *downloader = [[self class] sharedImageDownloader];
    id <AFImageRequestCache> imageCache = downloader.imageCache;

    //5、如果图片缓存存在,就使用该缓存图片 Use the image from the image cache if it exists
    UIImage *cachedImage = [imageCache imageforRequest:urlRequest withAdditionalIdentifier:nil];
    if (cachedImage) {
   //5.1缓存图片存在的情况下,判断是否设置了success,设置了就调用block返回urlRequest和缓存图片,否则将缓存图片赋值给self.image。
        if (success) {
            success(urlRequest, nil, cachedImage);
        } else {
            self.image = cachedImage;
        }
        [self clearActiveDownloadInformation];
    } else {
        if (placeholderImage) {
            self.image = placeholderImage;
        }
   //6、如果图片缓存不存在,开始下载图片
        __weak __typeof(self)weakSelf = self;
        NSUUID *downloadID = [NSUUID UUID];
  //6.1、从该处可以发现,现在使用一个DownloadReceipt进行图片下载
 //通过[NSUUID UUID]来生成一个唯一的标识downloadID证明AFImageDownloadReceipt的身份。

        AFImageDownloadReceipt *receipt;
//6.2、在AFImageDownloader的downloadImageForURLRequest方法里,处理了图片下载的过程
        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、每次使用setImageWithURLRequest加载图片时,首先,判断请求的图片地址是否有效,无效的话,取消图片下载任务,将af_activeImageDownloadReceipt凭据置为nil,并将placeholderImage赋值给self.image,返回。
2、接着再判断当前下载请求是否和urlRequest下载一样,一样就返回
3、取消之前的下载任务
4、如果图片缓存存在,就使用该缓存图片;不存在就使用AFImageDownloader中的downloadImageForURLRequest方法请求网络图片,请求成功后,将请求到的图片赋值,并将将af_activeImageDownloadReceipt置为nil。

AFImageDownloader

接着,再来看下在图片下载过程中AFImageDownloader这个类进行了哪些操作。
/**
The AFImageDownloadReceipt is an object vended by the AFImageDownloader when starting a data task. It can be used to cancel active tasks running on the AFImageDownloader session. As a general rule, image data tasks should be cancelled using the AFImageDownloadReceipt instead of calling cancel directly on the task itself. The AFImageDownloader is optimized to handle duplicate task scenarios as well as pending versus active downloads.
*/
源码注释中写到,在开始图片数据任务时,AFImageDownloadReceipt是由AFImageDownloader提供的一个对象。AFImageDownloadReceipt用来取消运行在AFImageDownloader session中的现行任务。图片数据任务应该通过使用AFImageDownloadReceipt来取消,而不是直接cancel掉task。

- (nullable AFImageDownloadReceipt *)downloadImageForURLRequest:(NSURLRequest *)request
                                                  withReceiptID:(nonnull NSUUID *)receiptID
                                                        success:(nullable void (^)(NSURLRequest *request, NSHTTPURLResponse  * _Nullable response, UIImage *responseObject))success
                                                        failure:(nullable void (^)(NSURLRequest *request, NSHTTPURLResponse * _Nullable response, NSError *error))failure {
    __block NSURLSessionDataTask *task = nil;
    dispatch_sync(self.synchronizationQueue, ^{
        NSString *URLIdentifier = request.URL.absoluteString;
        if (URLIdentifier == nil) {
            if (failure) {
                NSError *error = [NSError errorWithDomain:NSURLErrorDomain code:NSURLErrorBadURL userInfo:nil];
                dispatch_async(dispatch_get_main_queue(), ^{
                    failure(request, nil, error);
                });
            }
            return;
        }

        // 1) Append the success and failure blocks to a pre-existing request if it already exists
        AFImageDownloaderMergedTask *existingMergedTask = self.mergedTasks[URLIdentifier];
        if (existingMergedTask != nil) {
            AFImageDownloaderResponseHandler *handler = [[AFImageDownloaderResponseHandler alloc] initWithUUID:receiptID success:success failure:failure];
            [existingMergedTask addResponseHandler:handler];
            task = existingMergedTask.task;
            return;
        }

        // 2) Attempt to load the image from the image cache if the cache policy allows it
        switch (request.cachePolicy) {
            case NSURLRequestUseProtocolCachePolicy:
            case NSURLRequestReturnCacheDataElseLoad:
            case NSURLRequestReturnCacheDataDontLoad: {
                UIImage *cachedImage = [self.imageCache imageforRequest:request withAdditionalIdentifier:nil];
                if (cachedImage != nil) {
                    if (success) {
                        dispatch_async(dispatch_get_main_queue(), ^{
                            success(request, nil, cachedImage);
                        });
                    }
                    return;
                }
                break;
            }
            default:
                break;
        }

        // 3) Create the request and set up authentication, validation and response serialization
        NSUUID *mergedTaskIdentifier = [NSUUID UUID];
        NSURLSessionDataTask *createdTask;
        __weak __typeof__(self) weakSelf = self;

        createdTask = [self.sessionManager
                       dataTaskWithRequest:request
                       uploadProgress:nil
                       downloadProgress:nil
                       completionHandler:^(NSURLResponse * _Nonnull response, id  _Nullable responseObject, NSError * _Nullable error) {
                           dispatch_async(self.responseQueue, ^{
                               __strong __typeof__(weakSelf) strongSelf = weakSelf;
                               AFImageDownloaderMergedTask *mergedTask = strongSelf.mergedTasks[URLIdentifier];
                               if ([mergedTask.identifier isEqual:mergedTaskIdentifier]) {
                                   mergedTask = [strongSelf safelyRemoveMergedTaskWithURLIdentifier:URLIdentifier];
                                   if (error) {
                                       for (AFImageDownloaderResponseHandler *handler in mergedTask.responseHandlers) {
                                           if (handler.failureBlock) {
                                               dispatch_async(dispatch_get_main_queue(), ^{
                                                   handler.failureBlock(request, (NSHTTPURLResponse*)response, error);
                                               });
                                           }
                                       }
                                   } else {
                                       if ([strongSelf.imageCache shouldCacheImage:responseObject forRequest:request withAdditionalIdentifier:nil]) {
                                           [strongSelf.imageCache addImage:responseObject forRequest:request withAdditionalIdentifier:nil];
                                       }

                                       for (AFImageDownloaderResponseHandler *handler in mergedTask.responseHandlers) {
                                           if (handler.successBlock) {
                                               dispatch_async(dispatch_get_main_queue(), ^{
                                                   handler.successBlock(request, (NSHTTPURLResponse*)response, responseObject);
                                               });
                                           }
                                       }
                                       
                                   }
                               }
                               [strongSelf safelyDecrementActiveTaskCount];
                               [strongSelf safelyStartNextTaskIfNecessary];
                           });
                       }];

        // 4) Store the response handler for use when the request completes
        AFImageDownloaderResponseHandler *handler = [[AFImageDownloaderResponseHandler alloc] initWithUUID:receiptID
                                                                                                   success:success
                                                                                                   failure:failure];
        AFImageDownloaderMergedTask *mergedTask = [[AFImageDownloaderMergedTask alloc]
                                                   initWithURLIdentifier:URLIdentifier
                                                   identifier:mergedTaskIdentifier
                                                   task:createdTask];
        [mergedTask addResponseHandler:handler];
        self.mergedTasks[URLIdentifier] = mergedTask;

        // 5) Either start the request or enqueue it depending on the current active request count
        if ([self isActiveRequestCountBelowMaximumLimit]) {
            [self startMergedTask:mergedTask];
        } else {
            [self enqueueMergedTask:mergedTask];
        }

        task = mergedTask.task;
    });
    if (task) {
        return [[AFImageDownloadReceipt alloc] initWithReceiptID:receiptID task:task];
    } else {
        return nil;
    }
}

流程:
1、首先创建NSURLSessionDataTask
__block NSURLSessionDataTask *task = nil;
2、然后需要验证AFImageDownloadReceipt,因为该方法最后返回的对象是AFImageDownloadReceipt类型的。
而AFImageDownloadReceipt对象中又有(NSURLSessionDataTask *task、NSUUID *receiptID)这两个属性。
于是,可以使用AFImageDownloadReceipt中的receiptID来证明它的身份。用request.URL.absoluteString,即请求图片的网络地址作为唯一标识符URLIdentifier。
验证URLIdentifier,如果不存在并且回调的failure为true,则在主线程dispatch_get_main_queue中回调请求的错误。
3、如果验证通过,继续设置task。
首先查看self.mergedTasks合并任务中是否存在key为URLIdentifier的任务。如果存在,则为这个existingMergedTask添加一个相应处理回调的AFImageDownloaderResponseHandler。然后跳出dispatch_sync这个线程。如果不存在,继续执行后面的代码。
(因为方法最后会根据URLIdentifier,将mergedTask放进字典self.mergedTasks 里面 AFImageDownloaderMergedTask *mergedTask = [[AFImageDownloaderMergedTask alloc]
initWithURLIdentifier:URLIdentifier
identifier:mergedTaskIdentifier
task:createdTask];
[mergedTask addResponseHandler:handler];
self.mergedTasks[URLIdentifier] = mergedTask;)
4、如果self.mergedTasks中没有这个task,需要先检查下内存中是否有这个链接的图片缓存存在,如果存在,回调success。

 // 2) Attempt to load the image from the image cache if the cache policy allows it
        switch (request.cachePolicy) {
            case NSURLRequestUseProtocolCachePolicy:
            case NSURLRequestReturnCacheDataElseLoad:
            case NSURLRequestReturnCacheDataDontLoad: {
                UIImage *cachedImage = [self.imageCache imageforRequest:request withAdditionalIdentifier:nil];//关于如何在内存中查找缓存图片,需要看AFAutoPurgingImageCache这个类。
                if (cachedImage != nil) {
                    if (success) {
                        dispatch_async(dispatch_get_main_queue(), ^{
                            success(request, nil, cachedImage);
                        });
                    }
                    return;
                }
                break;
            }
            default:
                break;
        }

5、如果内存中不存在图片,则需要开始创建NSURLSessionDataTask,请求图片网络资源。

 NSURLSessionDataTask *createdTask;
 // 创建task
 createdTask = [self.sessionManager
                dataTaskWithRequest:request
                completionHandler:^(NSURLResponse * _Nonnull response, id  _Nullable responseObject, NSError * _Nullable error) {
                 
                    // 5.1因为请求数据的任务都放进AFImageDownloaderMergedTask中,用图片地址作为key关联,所以,在请求之前,先从self.mergedTask中获取URLIdentifier对于的task

                   // 5.2检查URLIdentifier和mergedTaskIdentifier对应的mergedTask是否是同一个,如果是,则从self.mergedTasks[URLIdentifier]中取出 mergedTask后,移除self.mergedTasks字典中的key为URLIdentifier对应的对象(因为该URLIdentifier地址的图片网络请求已经完成,这个task可以从self.mergedTasks中移除)
                
                  // 5.3如果有error,处理错误block,
                           如果没有error,将图片放进缓存中,然后遍历mergedTask.responseHandlers中的successBlock

                 // 5.4将当前的activeRequestCount请求数减一
                 // 5.4 resume开启 self.queuedMergedTasks中的下一个task
             
                }];
  });

6、设置self.mergedTasks[URLIdentifier]

 // 4) Store the response handler for use when the request completes
        AFImageDownloaderResponseHandler *handler = [[AFImageDownloaderResponseHandler alloc] initWithUUID:receiptID
                                                                                                   success:success
                                                                                                   failure:failure];
        AFImageDownloaderMergedTask *mergedTask = [[AFImageDownloaderMergedTask alloc]
                                                   initWithURLIdentifier:URLIdentifier
                                                   identifier:mergedTaskIdentifier
                                                   task:createdTask];
        [mergedTask addResponseHandler:handler];
        self.mergedTasks[URLIdentifier] = mergedTask;

7、判断当前请求task的数量是否在最大请求数范围内,如果在范围内,发起请求,如果不在范围内,根据task的优先级对其进行排序
// 5) Either start the request or enqueue it depending on the current active request count
if ([self isActiveRequestCountBelowMaximumLimit]) {
[self startMergedTask:mergedTask];
} else {
[self enqueueMergedTask:mergedTask];
}
8、最后返回这个AFImageDownloadReceipt


关于使用AFNetworking的UIImageView经常被问的一个问题,
关于同一个网络地址图片,多处使用,如何处理?(比如3个cell,都显示同一个url的图片)
解释:
AFImageDownloader中有一个属性,NSMutableDictionary mergedTasks用来存储合并任务的。当有图片请求的时候,首先查看self.mergedTasks合并任务中是否存在key为URLIdentifier的任务。如果存在,则为这个existingMergedTask添加一个相应处理回调的AFImageDownloaderResponseHandler。等资源下载完成后回调图片。(AFImageDownloaderMergedTask对象有一个属性NSMutableArray <AFImageDownloaderResponseHandler> *responseHandlers;数组用来存放同一url需要的回调处理)
【相当于根据url的key值创建对应的existingMergedTask,同一url对应一个task, 有多少次调用就为这个task增加多少个回调,每一个回调放主线程dispatch_get_main_queue中进行】

参考文章:
AFNetworking 3.0 源码解读(八)之 AFImageDownloader

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