UIImagView+AFNetworking实现分析

总览

UIImagView+AFNetworking是我们常用的一个扩展,用它一行代码就可以实现图片的加载和自动缓存,非常的方便。下面我们来看下它是怎么实现的:

首先来看它的类图:

class.png

其主要由以上几个类和protocol组成。下面我们大致说一下各个类的作用:

  • UIimageView+AFNetworking
    这个类是我们使用的扩展类,里面主要是调度了图片的加载相关操作,比如查找缓存以及下载图片。
  • AFImageDownloader
    这个类是真正的下载管理类,下载使用AFHTTPSessionManager来进行。每个下载以task的方式运行。
  • AFAutoPurgingImageCache
    这个类是缓存类,里面管理了整个缓存内容。
  • AFCachedImage
    这个类是图片数据类,对图片数据和图片标示进行包装。
  • AFImageDownloadReceipt
    这个类主要用来做任务的取消。
  • AFImageDownloaderMergedTask
    这个类是下载任务类,封装了任务相关信息。
  • AFImageDownloaderResponseHandler
    这个类回调类,主要封装了一个成功和一个失败回调。

执行过程

1.首先在UIImageView+AFNetworking中,利用setImage系列方法来加载图片:

- (void)setImageWithURL:(NSURL *)url;

- (void)setImageWithURL:(NSURL *)url
       placeholderImage:(nullable UIImage *)placeholderImage;

- (void)setImageWithURLRequest:(NSURLRequest *)urlRequest
              placeholderImage:(nullable UIImage *)placeholderImage
                       success:(nullable void (^)(NSURLRequest *request, NSHTTPURLResponse * _Nullable response, UIImage *image))success
                       failure:(nullable void (^)(NSURLRequest *request, NSHTTPURLResponse * _Nullable response, NSError *error))failure;

前两个方法最终其实是调用第三个方法,所以我们直接看第三个方法。这个方法首先会做一些必要的检查工作,之后会去调用AFImageRequestCache类查找本地是否已经有了此缓存。如果找到直接返回,如果找不到,就去服务端请求图片。

2.缓存是存在AFImageDownloader中的imageChche属性里面。找缓存API为:

- (nullable UIImage *)imageforRequest:(NSURLRequest *)request withAdditionalIdentifier:(nullable NSString *)identifier;

在这个方法里面,会根据真正存储图片的类AFCachedImage里面的identifier来确定是否是需要的图片,identifier是由url加一个additionalIdentifier组成。其拼接为一个完成key的方法为:

- (NSString *)imageCacheKeyFromURLRequest:(NSURLRequest *)request withAdditionalIdentifier:(NSString *)additionalIdentifier {
    NSString *key = request.URL.absoluteString;
    if (additionalIdentifier != nil) {
        key = [key stringByAppendingString:additionalIdentifier];
    }
    return key;
}

3.如果没找到缓存,就会去服务端下载图片。在AFImageDownloader中有两个方法提供下载功能:

- (nullable AFImageDownloadReceipt *)downloadImageForURLRequest:(NSURLRequest *)request
                                                        success:(nullable void (^)(NSURLRequest *request, NSHTTPURLResponse  * _Nullable response, UIImage *responseObject))success
                                                        failure:(nullable void (^)(NSURLRequest *request, NSHTTPURLResponse * _Nullable response, NSError *error))failure;

- (nullable AFImageDownloadReceipt *)downloadImageForURLRequest:(NSURLRequest *)request
                                                 withReceiptID:(NSUUID *)receiptID
                                                        success:(nullable void (^)(NSURLRequest *request, NSHTTPURLResponse  * _Nullable response, UIImage *responseObject))success
                                                        failure:(nullable void (^)(NSURLRequest *request, NSHTTPURLResponse * _Nullable response, NSError *error))failure;

第一个方法会去调第二个方法。在这个方法中,首先会去做一些基础的检查工作,之后会去检查要请求的图片是否已在任务中,所有任务都放在AFImageDownloader的mergedTasks字典属性中,key是url。如果发现任务已存在,就在任务中多添加一个回调,回调放在AFImageDownloaderResponseHandler类中。如果没有找到则根据初始设定的缓存规则来确定是不是需要读取缓存。

// 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;
        }

这里一共有三种缓存策略可以使用缓存:

  • NSURLRequestUseProtocolCachePolicy。 对特定的 URL 请求使用网络协议中实现的缓存逻辑
  • NSURLRequestReturnCacheDataElseLoad。无论缓存是否过期,先使用本地缓存数据。如果缓存中没有请求所对应的数据,那么从原始地址加载数据。
  • NSURLRequestReturnCacheDataDontLoad。 无论缓存是否过期,先使用本地缓存数据。如果缓存中没有请求所对应的数据,那么放弃从原始地址加载数据,请求视为失败。
    如果不允许使用缓存或者没找到缓存,就要走正常的请求流程,真正的去请求数据了。请求到的数据好会添加到缓存中,以供下次使用。

请求及缓存处理中的多线程

首先来看请求相关部分:

NSString *name = [NSString stringWithFormat:@"com.alamofire.imagedownloader.synchronizationqueue-%@", [[NSUUID UUID] UUIDString]];
self.synchronizationQueue = dispatch_queue_create([name cStringUsingEncoding:NSASCIIStringEncoding], DISPATCH_QUEUE_SERIAL);

name = [NSString stringWithFormat:@"com.alamofire.imagedownloader.responsequeue-%@", [[NSUUID UUID] UUIDString]];
self.responseQueue = dispatch_queue_create([name cStringUsingEncoding:NSASCIIStringEncoding], DISPATCH_QUEUE_CONCURRENT);

请求队列synchronizationQueue是一个串行队列,这是因为请求处理需要做一些任务检查和添加操作,而这些操作都是属于共享的资源(这里的共享资源主要指activeRequestCount、mergedTasks等)。如果使用并行队列的话,需要对每个共享资源加锁,而这里的几个共享资源都是逻辑上相关的,所以很容易造成死锁,所以这里使用串行队列可以省很多麻烦。 关于使用串行队列来避免加锁我们可以举一个例子来看一下:

- (void)safelyDecrementActiveTaskCount {
    dispatch_sync(self.synchronizationQueue, ^{
        if (self.activeRequestCount > 0) {
            self.activeRequestCount -= 1;
        }
    });
}

这里将活跃请求的数量的递减放到了串行队列中,如果想通过队列显示数量的增减,那数量的增加必然也要放到同一个串行队列中,才可以在不用锁的情况下保证资源的正确性。所以有了下面的代码:

- (void)safelyStartNextTaskIfNecessary {
    dispatch_sync(self.synchronizationQueue, ^{
        if ([self isActiveRequestCountBelowMaximumLimit]) {
            while (self.queuedMergedTasks.count > 0) {
                AFImageDownloaderMergedTask *mergedTask = [self dequeueMergedTask];
                if (mergedTask.task.state == NSURLSessionTaskStateSuspended) {
                    [self startMergedTask:mergedTask];
                    break;
                }
            }
        }
    });
}

这里,资源的增加同样放到了synchronizationQueue队列中执行。这样就保证了共享资源的安全性。
返回队列responseQueue是一个并行队列。这是由于返回队列的一个重要作用就是快速分发返回结果,而这些并没有对共享资源做出修改,这样可以保证最快速的分发。

除了请求相关内容,在缓存管理上,也有一些值得说道的地方:

NSString *queueName = [NSString stringWithFormat:@"com.alamofire.autopurgingimagecache-%@", [[NSUUID UUID] UUIDString]];
self.synchronizationQueue = dispatch_queue_create([queueName cStringUsingEncoding:NSASCIIStringEncoding], DISPATCH_QUEUE_CONCURRENT);

这里缓存的管理是创建了一个并行队列synchronizationQueue。缓存的读取用了这样 一个方法:

- (nullable UIImage *)imageWithIdentifier:(NSString *)identifier {
    __block UIImage *image = nil;
    dispatch_sync(self.synchronizationQueue, ^{
        AFCachedImage *cachedImage = self.cachedImages[identifier];
        image = [cachedImage accessImage];
    });
    return image;
}

缓存的增删代码如下:

- (void)addImage:(UIImage *)image withIdentifier:(NSString *)identifier {
    dispatch_barrier_async(self.synchronizationQueue, ^{
        AFCachedImage *cacheImage = [[AFCachedImage alloc] initWithImage:image identifier:identifier];

        AFCachedImage *previousCachedImage = self.cachedImages[identifier];
        if (previousCachedImage != nil) {
            self.currentMemoryUsage -= previousCachedImage.totalBytes;
        }

        self.cachedImages[identifier] = cacheImage;
        self.currentMemoryUsage += cacheImage.totalBytes;
    });

    dispatch_barrier_async(self.synchronizationQueue, ^{
        if (self.currentMemoryUsage > self.memoryCapacity) {
            UInt64 bytesToPurge = self.currentMemoryUsage - self.preferredMemoryUsageAfterPurge;
            NSMutableArray <AFCachedImage*> *sortedImages = [NSMutableArray arrayWithArray:self.cachedImages.allValues];
            NSSortDescriptor *sortDescriptor = [[NSSortDescriptor alloc] initWithKey:@"lastAccessDate"
                                                                           ascending:YES];
            [sortedImages sortUsingDescriptors:@[sortDescriptor]];

            UInt64 bytesPurged = 0;

            for (AFCachedImage *cachedImage in sortedImages) {
                [self.cachedImages removeObjectForKey:cachedImage.identifier];
                bytesPurged += cachedImage.totalBytes;
                if (bytesPurged >= bytesToPurge) {
                    break ;
                }
            }
            self.currentMemoryUsage -= bytesPurged;
        }
    });
}

- (BOOL)removeImageWithIdentifier:(NSString *)identifier {
    __block BOOL removed = NO;
    dispatch_barrier_sync(self.synchronizationQueue, ^{
        AFCachedImage *cachedImage = self.cachedImages[identifier];
        if (cachedImage != nil) {
            [self.cachedImages removeObjectForKey:identifier];
            self.currentMemoryUsage -= cachedImage.totalBytes;
            removed = YES;
        }
    });
    return removed;
}

通过上面的一系列方法可以看到写入用了dispatch_barrier_sync和dispatch_barrier_async来保证执行的一致性。对于读取,使用了dispatch_sync来实现满足同时支持多个读取操作,也就是单一资源的多读单写(具体在这儿有介绍)。我想这也是cache操作使用并行而不是串行队列的原因。

参考

iOS - 关于NSURLCache
NSURLRequestCachePolicy—iOS缓存策略
底层并发 API
AFNetworking之UIKit扩展与缓存实现

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

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,596评论 18 139
  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,441评论 25 707
  • 从三月份找实习到现在,面了一些公司,挂了不少,但最终还是拿到小米、百度、阿里、京东、新浪、CVTE、乐视家的研发岗...
    时芥蓝阅读 42,178评论 11 349
  • 关于作者 勇埃德,新晋华裔科普作家,被称为是当代最优秀的科学记者。勇埃德供职于《大西洋月刊》,同时也是一个多产的科...
    蔚成阅读 1,124评论 0 0
  • 难得来到此学院,却遇傻子三条狗。 每天相处像美苏,气煞我也还得忍。 望得老天放口气,吹得三屌屁滚流。 ...
    可以再等阅读 120评论 0 1