AFNetworking网路请求源码精讲(五) — AFAutoPurgingImageCache / UIImage+AFNetworking(图片缓存)

UIImage+AFNetworking图片缓存下载

前面四篇主要讲解了一下关于AFNetworking的常用请求的使用,这篇文章,缓和一下气氛,我们来关注一下AFNetworking提供的图片缓存机制。

使用方法

在项目中导入UIKit+AFNetworking/UIImageView+AFNetworking.h来调用。
通过头文件名称,我们不难看出这是一个分类,是AFNetworking对UIImage方法拓展的操作。

UIImageView *imageV = [[UIImage alloc] init];
    [imageV setImageWithURL:<#(nonnull NSURL *)#> placeholderImage:<#(nullable UIImage *)#>];
使用场景分析

我们通常,在项目中会用到大量的图片,如果这些图片都是来自本地的还好说,只需要每次加载即可,但是如果大部分图片都是来自网络中,或者用户浏览朋友圈中的图片,难道我们要每次都通过AFNetworking向图片URL发送请求吗???
答案当然不是,这样不仅会消耗用户的流量,还会咋网络不佳的情况,使得软件的体验度大打折扣,因此,提前缓存就成为了当务之急的处理方式,但是,缓存是有限度的,不能一直缓存,到某种限度就需要清理缓存,带着这个思路,我们来看一下AFNetworking具体是如何设计在线图片处理操作的。

分析参数

进入源码中,我们看到这个方法需要传递两个参数:

  1. 请求图片的路径;
  2. 占位图片(当网络请求不佳的状态时候,可以通过占位符来替代,使得界面不会一片空白);
- (void)setImageWithURL:(NSURL *)url
       placeholderImage:(UIImage *)placeholderImage{ ... }
    NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];

    [request addValue:@"image/*"   forHTTPHeaderField:@"Accept"];

    [self setImageWithURLRequest:request placeholderImage:placeholderImage success:nil failure:nil];

在这里需要注意,将请求的图片网址封装成为可变的网络请求。

思考:为什么设计成为可变网络请求,为什么不是NSURLRequest???
从下面设置不难发现,对于图片我们需要重新设置Requst请求设置,因为默认请求是不能接受图片类型的URL,如果不设置会报错。
设置: [request addValue:@“image/*” forHTTPHeaderField:@“Accept”];

系统定义:

/*!
@method addValue:forHTTPHeaderField:
@abstract Adds an HTTP header field in the current header
dictionary.
@discussion This method provides a way to add values to header
fields incrementally. If a value was previously set for the given
header field, the given value is appended to the previously-existing
value. The appropriate field delimiter, a comma in the case of HTTP,
is added by the implementation, and should not be added to the given
value by the caller. Note that, in keeping with the HTTP RFC, HTTP
header field names are case-insensitive.
@param value the header field value.
@param field the header field name (case-insensitive).
*/
-(void)addValue:(NSString *)value forHTTPHeaderField:(NSString *)field;

封装完成,继续将参数往另一个方法封装。

下载图片
- (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{ ... }

AFNetworking做了以下的判断:

  1. URL为空,使用占位符
 if ([urlRequest URL] == nil) {
        self.image = placeholderImage;
        if (failure) {
            NSError *error = [NSError errorWithDomain:NSURLErrorDomain code:NSURLErrorBadURL userInfo:nil];
            failure(urlRequest, nil, error);
        }
        return;
}
  1. 正在有下载任务
    如果有下载任务,什么都不操作,直接取消之前的下载操作。

    if ([self isActiveTaskURLEqualToURLRequest:urlRequest]) {
        return;
    }
    [self cancelImageDownloadTask];
  1. 缓存图片
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) {
        if (success) {
            success(urlRequest, nil, cachedImage);
        } else {
            self.image = cachedImage;
        }
        [self clearActiveDownloadInformation];
    } else {
        
        /* 先赋值一个占位图 */
        if (placeholderImage) {
            self.image = placeholderImage;
        }

        __weak __typeof(self)weakSelf = self;
        
        /* 通过[NSUUID UUID]生成下载ID */
        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. 查看缓存图片是否存在
if (cachedImage) {
        if (success) {
            success(urlRequest, nil, cachedImage);
        } else {
            self.image = cachedImage;
        }
        [self clearActiveDownloadInformation];
}

这里如果有了缓存照片,就不需要下载了,就需要讲缓存照片赋给要显示图片的位置,然后清除下载任务。

  1. 无缓存,需下载
    下载需要时间,根据图片大小不同,因此,为了不让用户单独等待,需要现在下载时候,先使用占位图片来让用户知道,这里即将显示一个图片。
if (placeholderImage) {
   self.image = placeholderImage;
}

思考:如何记录缓存图片,是通过URL来标识图片吗???
如果初步设计,利用URL标识图片显得天衣无缝,但是,当图片URL发生改变而图片内容不变时候,URL就显得不那么可靠了,因此,在下载一个图片,我们给这个图片一个UUID来唯一标识图片,这样图片就变成唯一的了,也便于查找

取消下载操作

前面我们看到了如果有下载任务,我们需要先取消下载任务,在重新开启下载任务,来看一下到底是如何停止操作的。

- (void)cancelImageDownloadTask {
    if (self.af_activeImageDownloadReceipt != nil) {
        [[self.class sharedImageDownloader] cancelTaskForImageDownloadReceipt:self.af_activeImageDownloadReceipt];

        [self clearActiveDownloadInformation];
     }
}

清空操作,判断当前是否有下在任务,如果有则取消下载任务。

- (void)cancelTaskForImageDownloadReceipt:(AFImageDownloadReceipt *)imageDownloadReceipt {
    dispatch_sync(self.synchronizationQueue, ^{
        NSString *URLIdentifier = imageDownloadReceipt.task.originalRequest.URL.absoluteString;
        AFImageDownloaderMergedTask *mergedTask = self.mergedTasks[URLIdentifier];
        NSUInteger index = [mergedTask.responseHandlers indexOfObjectPassingTest:^BOOL(AFImageDownloaderResponseHandler * _Nonnull handler, __unused NSUInteger idx, __unused BOOL * _Nonnull stop) {
            return handler.uuid == imageDownloadReceipt.receiptID;
        }];

        if (index != NSNotFound) {
            AFImageDownloaderResponseHandler *handler = mergedTask.responseHandlers[index];
            [mergedTask removeResponseHandler:handler];
            NSString *failureReason = [NSString stringWithFormat:@"ImageDownloader cancelled URL request: %@",imageDownloadReceipt.task.originalRequest.URL.absoluteString];
            NSDictionary *userInfo = @{NSLocalizedFailureReasonErrorKey:failureReason};
            NSError *error = [NSError errorWithDomain:NSURLErrorDomain code:NSURLErrorCancelled userInfo:userInfo];
            if (handler.failureBlock) {
                dispatch_async(dispatch_get_main_queue(), ^{
                    handler.failureBlock(imageDownloadReceipt.task.originalRequest, nil, error);
                });
            }
        }

        if (mergedTask.responseHandlers.count == 0 && mergedTask.task.state == NSURLSessionTaskStateSuspended) {
            [mergedTask.task cancel];
            [self removeMergedTaskWithURLIdentifier:URLIdentifier];
        }
    });
}

同步线程中,根据下载路径找到图片下载任务标识,然后清除。
最后清除下载信息。

AFAutoPurgingImageCache

图片缓存,我们分析一下AFAutoPurgingImageCache,来看一下如何缓存的。

@property (nonatomic, assign) UInt64 memoryCapacity;
@property (nonatomic, assign) UInt64 preferredMemoryUsageAfterPurge;
@property (nonatomic, assign, readonly) UInt64 memoryUsage;
  1. 总共缓存内存设置;
  2. 用户偏好设置使用内存限制额度;
  3. 当前内存使用;
- (instancetype)init {
    return [self initWithMemoryCapacity:100 * 1024 * 1024 preferredMemoryCapacity:60 * 1024 * 1024];
}

初始化,默认情况,AFNetworking提供缓存的空间是100MB,缓存上限是60MB,如果超过60MB就会清除缓存的图片。

self.memoryCapacity = memoryCapacity;
        /* 给定的内存界限 -- AFN默认给的是内存界限为60MB */
        self.preferredMemoryUsageAfterPurge = preferredMemoryCapacity;
        self.cachedImages = [[NSMutableDictionary alloc] init];
  1. 初始化内存和内存上限;
  2. 利用字典存储缓存图片;
- (void)addImage:(UIImage *)image withIdentifier:(NSString *)identifier { ... }

添加图片操作。

dispath_barrier_async用来创建一个栅栏线程,当图片很多时候可以很好的防止线程相互干扰,因此,使用栅栏起到阻碍作用

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

判断当前内存使用和内存限制,如果上面需要缓存的图片导致上限超过了限定值,就需要清除缓存操作。
我们要先计算需要清除的内存(取整个图片)

UInt64 bytesToPurge = self.currentMemoryUsage - self.preferredMemoryUsageAfterPurge;

使用LRU,时间最长删除算法进行删除操作。
通过时间戳,进行排序缓存图片。

NSMutableArray <AFCachedImage*> *sortedImages = [NSMutableArray arrayWithArray:self.cachedImages.allValues];

然后,遍历删除缓存图片,释放内存。

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

总结

到这里AFNetworking大部分常用的源码就已经介绍完毕了,下面我们会分析另一个板块的源代码。

中文源码下载

AFNetworking中文源码下载: GitHub.

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值