关于AFNetworking的简单总结。
这部分代码是AFN提供的便捷的扩展(分类),主要是下面的四块功能。
一、控件设置网络图片
AFN对UIButton和UIImageView增加了设置网络图片的接口,虽然现在都是使用SDWebImage设置的,但是这块代码也是值得一看。
1,接口
针对UIButton,提供了设置配图、占位图和背景图的接口,并且有取消下载图片的接口和设置下载完成回调的接口。对于UIImageView也类似,可以根据URL设置网络图片和取消下载任务。实际上这些看起来很常见的扩展,但是内部实现也是非常巧妙的,与SDWebImage一致,这些设置图片的接口是有缓存逻辑的。不同的是,AFN的拓展是图片内存缓存+请求缓存(NSURLCache),而SDWebImage具有图片内存缓存和图片磁盘缓存。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42
| @interface UIButton (AFNetworking)
+ (void)setSharedImageDownloader:(AFImageDownloader *)imageDownloader; + (AFImageDownloader *)sharedImageDownloader;
- (void)setImageForState:(UIControlState)state withURLRequest:(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; - (void)setImageForState:(UIControlState)state withURL:(NSURL *)url; - (void)setImageForState:(UIControlState)state withURL:(NSURL *)url placeholderImage:(nullable UIImage *)placeholderImage;
- (void)setBackgroundImageForState:(UIControlState)state withURLRequest:(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; - (void)setBackgroundImageForState:(UIControlState)state withURL:(NSURL *)url; - (void)setBackgroundImageForState:(UIControlState)state withURL:(NSURL *)url placeholderImage:(nullable UIImage *)placeholderImage;
- (void)cancelImageDownloadTaskForState:(UIControlState)state; - (void)cancelBackgroundImageDownloadTaskForState:(UIControlState)state; @end
@interface UIImageView (AFNetworking)
+ (void)setSharedImageDownloader:(AFImageDownloader *)imageDownloader; + (AFImageDownloader *)sharedImageDownloader; - (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;
- (void)cancelImageDownloadTask; @end
|
2,实现
核心逻辑
以UIImageView为例,可以窥探一下设置网络图片的逻辑。辅助图片下载的有三个关键的类:管理图片缓存的AFAutoPurgingImageCache、处理下载任务的AFImageDownloader,AFImageDownloader每次创建一个下载任务后会返回一个下载凭据AFImageDownloadReceipt。AFAutoPurgingImageCache根据图片URL作为缓存id,把下载的图片缓存到内存中,并根据LRU算法(最近最少使用)适时清理。AFImageDownloader负责存取图片,借助AFAutoPurgingImageCache优先从缓存中取出图片,如若没有则创建下载任务并在下载完毕后存储到缓存中。AFImageDownloadReceipt用于关联下载任务,用于取消下载任务和避免重复下载任务等。
设置网络图片时,在经过URL空值判断、重复下载判断、取消已有的下载任务后,尝试从Cache中取出图片,如若没有,下载器AFImageDownloader会创建一个下载任务并返回下载凭据,UIImageView利用关联属性保存这个下载凭据。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83
| - (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 { if ([urlRequest URL] == nil) { self.image = placeholderImage; if (failure) { NSError *error = [NSError errorWithDomain:NSURLErrorDomain code:NSURLErrorBadURL userInfo:nil]; failure(urlRequest, nil, error); } return; } if ([self isActiveTaskURLEqualToURLRequest:urlRequest]) { return; } [self cancelImageDownloadTask];
AFImageDownloader *downloader = [[self class] sharedImageDownloader]; id <AFImageRequestCache> imageCache = downloader.imageCache;
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 *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; } }
|
取消下载任务时,取出之前保存的下载凭据,让下载器AFImageDownloader取消下载凭据对应的下载任务,并清除保存的下载凭据。
1 2 3 4 5 6 7 8 9 10
| - (void)cancelImageDownloadTask { if (self.af_activeImageDownloadReceipt != nil) { [[self.class sharedImageDownloader] cancelTaskForImageDownloadReceipt:self.af_activeImageDownloadReceipt]; [self clearActiveDownloadInformation]; } }
- (void)clearActiveDownloadInformation { self.af_activeImageDownloadReceipt = nil; }
|
缓存的设计
各个模块逻辑相对独立,先看一下缓存的实现。AFImageCache协议定义了最基本的缓存必须具备的方法:通过缓存标识符(identifier)进行增、删、查操作。
1 2 3 4 5 6 7 8 9 10
| @protocol AFImageCache <NSObject>
- (void)addImage:(UIImage *)image withIdentifier:(NSString *)identifier;
- (BOOL)removeImageWithIdentifier:(NSString *)identifier; - (BOOL)removeAllImages;
- (nullable UIImage *)imageWithIdentifier:(NSString *)identifier; @end
|
AFImageRequestCache协议继承了AFImageCache协议,它添加了与NSURLRequest有关的方法,表示缓存标识符可以是NSURLRequest类型。
1 2 3 4 5 6 7 8
| @protocol AFImageRequestCache <AFImageCache>
- (BOOL)shouldCacheImage:(UIImage *)image forRequest:(NSURLRequest *)request withAdditionalIdentifier:(nullable NSString *)identifier; - (void)addImage:(UIImage *)image forRequest:(NSURLRequest *)request withAdditionalIdentifier:(nullable NSString *)identifier; - (BOOL)removeImageforRequest:(NSURLRequest *)request withAdditionalIdentifier:(nullable NSString *)identifier; - (nullable UIImage *)imageforRequest:(NSURLRequest *)request withAdditionalIdentifier:(nullable NSString *)identifier; @end
|
AFAutoPurgingImageCache遵守AFImageRequestCache协议,是最终缓存逻辑的实现类。使用LRU算法处理缓存对象,对外提供缓存容量、已缓存量等接口。默认内存缓存容量为100MB。
LRU是Least Recently Used的缩写,即最近最少使用,是一种常用的页面置换算法,选择最近最久未使用的页面予以淘汰。
该算法赋予每个页面一个访问字段,用来记录一个页面自上次被访问以来所经历的时间t,当须淘汰一个页面时,选择现有页面中其t值最大的,即最近最少使用的页面予以淘汰。
1 2 3 4 5 6 7 8 9 10 11 12
| @interface AFAutoPurgingImageCache : NSObject <AFImageRequestCache>
@property (nonatomic, assign) UInt64 memoryCapacity;
@property (nonatomic, assign) UInt64 preferredMemoryUsageAfterPurge;
@property (nonatomic, assign, readonly) UInt64 memoryUsage;
- (instancetype)init; - (instancetype)initWithMemoryCapacity:(UInt64)memoryCapacity preferredMemoryCapacity:(UInt64)preferredMemoryCapacity; @end
|
实现LRU算法,缓存单元必须具备时间属性。AFCachedImage作为缓存的基本单元,保存最近一次访问日期和UIImage实例,记录占用的存储空间并给出缓存的唯一标识符。每次访问这个缓存单元时,更新访问的时间。可以看到,计算一个UIImage对象占用存储空间大小的方法是:图片尺寸 * 每个像素占用的内存(RGBA共四个字节),计算图片尺寸时要考虑屏幕缩放比例。也即占用的内存为:
1
| (图片宽度 * 屏幕缩放比例) * (图片高度 * 屏幕缩放比例) * 4
|
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| @interface AFCachedImage : NSObject @property (nonatomic, strong) UIImage *image; @property (nonatomic, copy) NSString *identifier;
@property (nonatomic, assign) UInt64 totalBytes;
@property (nonatomic, strong) NSDate *lastAccessDate;
@property (nonatomic, assign) UInt64 currentMemoryUsage; @end @implementation AFCachedImage - (instancetype)initWithImage:(UIImage *)image identifier:(NSString *)identifier { if (self = [self init]) { self.image = image; self.identifier = identifier; CGSize imageSize = CGSizeMake(image.size.width * image.scale, image.size.height * image.scale); CGFloat bytesPerPixel = 4.0; CGFloat bytesPerSize = imageSize.width * imageSize.height; self.totalBytes = (UInt64)bytesPerPixel * (UInt64)bytesPerSize; self.lastAccessDate = [NSDate date]; } return self; }
- (UIImage *)accessImage { self.lastAccessDate = [NSDate date]; return self.image; } @end
|
在对外的接口中,NSURLRequest作为缓存单元的标识,内部处理默认是直接取出NSURLRequest对应的URL作为缓存ID。
1 2 3 4 5 6 7
| - (NSString *)imageCacheKeyFromURLRequest:(NSURLRequest *)request withAdditionalIdentifier:(NSString *)additionalIdentifier { NSString *key = request.URL.absoluteString; if (additionalIdentifier != nil) { key = [key stringByAppendingString:additionalIdentifier]; } return key; }
|
AFAutoPurgingImageCache内部使用可变字典(NSMutableDictionary <NSString* , AFCachedImage*> *cachedImages
)保存缓存对象,当添加图片时主要有两块逻辑:
(1)根据缓存标识符创建缓存对象AFCachedImage,如果缓存池中已经有这个标识符对应的实例,那就移除这个旧缓存,把新的AFCachedImage添加到缓存池中。
(2)缓存对象添加完毕,判断缓存总容量是否超过阈值,当超过设定的阈值时,按照访问时间由远到近,开始逐个清除缓存对象,直到达到设定的临界线(preferredMemoryUsageAfterPurge)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
| - (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; } }); }
|
清除缓存时,则先从cachedImages中移除,再更新缓存容量currentMemoryUsage,并返回清除操作的结果。
1 2 3 4 5 6 7 8 9 10 11 12
| - (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 }
|
获取缓存对象时,直接根据标识符从字典cachedImages中取:
1 2 3 4 5 6 7 8
| - (nullable UIImage *)imageWithIdentifier:(NSString *)identifier { __block UIImage *image = nil; dispatch_sync(self.synchronizationQueue, ^{ AFCachedImage *cachedImage = self.cachedImages[identifier]; image = [cachedImage accessImage]; }); return image; }
|
分析以上代码,synchronizationQueue是并发队列,增加这个方法不要求返回值,所以使用异步函数,所以增加操作是在子线程。删除和查询这两个方法有返回值,所以必须使用同步函数,因而是在”当前线程“执行。查询方法可以并发查询;删除方法和增加方法使用栅栏函数,实现删除操作和其他操作互斥、增加操作和其他操作互斥,从而保证线程安全。
下载器
AFImageDownloader的下载工作是依赖AFHTTPSessionManager完成的,每次创建下载任务后,会返回一个下载凭据,这个凭据跟下载任务关联,外界可以利用这个凭据取消对应的下载任务。它内部利用一个字典存储所有下载任务,key为下载任务对应的请求URL。为了限制同时下载的最大数量,AFImageDownloader还维护一个下载任务队列queuedMergedTasks,根据下载任务的优先级,下载任务被添加到队列的前面或后面。在每次下载任务完成时,若当前正在下载的任务数量小于阈值(maximumActiveDownloads,默认为4),AFImageDownloader会取出队列中最前面的下载任务,执行下载操作。
AFImageDownloader会利用AFAutoPurgingImageCache缓存网络图片,而下载任务的Response将被缓存到NSURLCache中。NSURLCache的内存缓存容量是20MB,磁盘缓存容量是150MB。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
| @interface AFImageDownloader : NSObject
@property (nonatomic, strong, nullable) id <AFImageRequestCache> imageCache;
@property (nonatomic, strong) AFHTTPSessionManager *sessionManager;
@property (nonatomic, assign) AFImageDownloadPrioritization downloadPrioritization;
+ (instancetype)defaultInstance;
+ (NSURLCache *)defaultURLCache;
+ (NSURLSessionConfiguration *)defaultURLSessionConfiguration;
- (instancetype)init;
- (instancetype)initWithSessionConfiguration:(NSURLSessionConfiguration *)configuration;
- (instancetype)initWithSessionManager:(AFHTTPSessionManager *)sessionManager downloadPrioritization:(AFImageDownloadPrioritization)downloadPrioritization maximumActiveDownloads:(NSInteger)maximumActiveDownloads imageCache:(nullable id <AFImageRequestCache>)imageCache;
- (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;
- (void)cancelTaskForImageDownloadReceipt:(AFImageDownloadReceipt *)imageDownloadReceipt; @end
|
AFImageDownloader的mergedTasks存储所有的下载任务,queuedMergedTasks是待下载任务的队列。在创建下载任务时,首先是URL校验,接着校验下载任务是否已经在mergedTasks中,对于已经在mergedTasks中的任务, 会把外界的成功回调和失败回调”拼接到“对应的已有的下载任务中。也就是说,一个下载链接对应一个下载任务,而一个下载任务是可以对应多个下载回调的。当外界尝试针对同一资源(URL)创建多个下载任务时,这个下载任务不会被添加到下载队列中,但是外界传入的回调却会被追加保存。当这个下载任务完成,它对应的所有回调将按照顺序被执行。
而对于未在mergedTasks中的任务,则判断这个任务对应的图片缓存是否存在,若能拿到图片缓存,就直接返回,否则就创建这个下载任务对应的NSURLSessionDataTask,并把这个下载任务存储到mergedTasks中。如果当前正在下载的任务数量小于阈值,就开始下载,否则把这个下载任务放入待下载队列queuedMergedTasks中。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122
| - (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; } 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; }
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; }
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 safelyGetMergedTask: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]; }); }];
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; 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; } }
|
在一个任务下载完毕的时候,更新活跃任务(正在下载的任务)数量,并判断是否小于阈值(maximumActiveDownloads),条件满足时,取出待下载任务进入下载状态。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| - (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; } } } }); }
- (void)startMergedTask:(AFImageDownloaderMergedTask *)mergedTask { [mergedTask.task resume]; ++self.activeRequestCount; }
- (BOOL)isActiveRequestCountBelowMaximumLimit { return self.activeRequestCount < self.maximumActiveDownloads; }
|
二、状态栏的网络指示器
1,接口
AFNetworkActivityIndicatorManager用于管理iPhone状态栏网络活动指示器,可以实现有网络请求的时候,指示器处于”旋转“状态,给用户较好的感知体验。一般情况下,我们使用AFN发送网络请求,只需要打开这个Manager的开关即可,不需要其他操作。因为AFNetworkActivityIndicatorManager内部监听了AFNetworkingTaskDidResumeNotification、AFNetworkingTaskDidSuspendNotification和AFNetworkingTaskDidCompleteNotification通知,自动处理网络请求计数器的逻辑。但是如果我们用了原生的NSURLSession做网络请求工作,就需要在适当时候增加和减少网络计数器的值(incrementActivityCount或者incrementActivityCount)。
它内部维护一个activityCount计数器,当值大于0的时候说明当前有活跃的网络请求,为0的时候此刻说明没有网络请求。通过增加和减小这个计数器的值,实现网络状态指示器适时的显示(计数器值大于0)和隐藏(计数器值等于0)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| @interface AFNetworkActivityIndicatorManager : NSObject
@property (nonatomic, assign, getter = isEnabled) BOOL enabled;
@property (readonly, nonatomic, assign, getter=isNetworkActivityIndicatorVisible) BOOL networkActivityIndicatorVisible;
@property (nonatomic, assign) NSTimeInterval activationDelay;
@property (nonatomic, assign) NSTimeInterval completionDelay;
+ (instancetype)sharedManager;
- (void)incrementActivityCount;
- (void)incrementActivityCount;
- (void)setNetworkingActivityActionWithBlock:(nullable void (^)(BOOL networkActivityIndicatorVisible))block; @end
|
2,实现
默认情况下,AFNetworkActivityIndicatorManager给网络状态指示器的显示和隐藏加了延迟逻辑,也即当有网络请求的时候,默认延迟1秒才会展示状态指示器,当没有网络请求的时候,默认延迟0.17秒才隐藏状态指示器。这样做可以避免状态指示器过于突兀,显示过于频繁,也是细小之处优化用户体验。延迟的逻辑是通过定时器实现的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| - (void)setCurrentState:(AFNetworkActivityManagerState)currentState { @synchronized(self) { if (_currentState != currentState) { _currentState = currentState; switch (currentState) { case AFNetworkActivityManagerStateNotActive: [self cancelActivationDelayTimer]; [self cancelCompletionDelayTimer]; [self setNetworkActivityIndicatorVisible:NO]; break; case AFNetworkActivityManagerStateDelayingStart: [self startActivationDelayTimer]; break; case AFNetworkActivityManagerStateActive: [self cancelCompletionDelayTimer]; [self setNetworkActivityIndicatorVisible:YES]; break; case AFNetworkActivityManagerStateDelayingEnd: [self startCompletionDelayTimer]; break; } } } }
|
当延迟展示的时候,初始化定时器,倒计时结束,判断网络请求的数量,若还是大于0说明有网络请求,设置状态指示器的展示。延迟隐藏同理。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| - (void)startActivationDelayTimer { self.activationDelayTimer = [NSTimer timerWithTimeInterval:self.activationDelay target:self selector:@selector(activationDelayTimerFired) userInfo:nil repeats:NO]; [[NSRunLoop mainRunLoop] addTimer:self.activationDelayTimer forMode:NSRunLoopCommonModes]; }
- (void)activationDelayTimerFired { if (self.networkActivityOccurring) { [self setCurrentState:AFNetworkActivityManagerStateActive]; } else { [self setCurrentState:AFNetworkActivityManagerStateNotActive]; } }
- (BOOL)isNetworkActivityOccurring { @synchronized(self) { return self.activityCount > 0; } }
|
因为一般都是在子线程操作,所以对计数器需要加锁,实现增值、减值、取值 三个操作同一时间只能执行一个,是线程安全的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| - (BOOL)isNetworkActivityOccurring { @synchronized(self) { return self.activityCount > 0; } }
- (void)incrementActivityCount { @synchronized(self) { self.activityCount++; } dispatch_async(dispatch_get_main_queue(), ^{ [self updateCurrentStateForNetworkActivityChange]; }); }
- (void)decrementActivityCount { @synchronized(self) { self.activityCount = MAX(_activityCount - 1, 0); } dispatch_async(dispatch_get_main_queue(), ^{ [self updateCurrentStateForNetworkActivityChange]; }); }
|
三、NSURLSessionTask与控件状态同步
AFN给UIProgressView和UIRefreshControl加了拓展,实现NSURLSessionTask和控件的状态同步。
1 2 3 4 5 6 7 8 9 10
| @interface UIProgressView (AFNetworking) - (void)setProgressWithUploadProgressOfTask:(NSURLSessionUploadTask *)task animated:(BOOL)animated; - (void)setProgressWithDownloadProgressOfTask:(NSURLSessionDownloadTask *)task animated:(BOOL)animated; @end
@interface UIRefreshControl (AFNetworking) - (void)setRefreshingWithStateOfTask:(NSURLSessionTask *)task; @end
|
二者的实现原理类似,以UIRefreshControl为例,UIRefreshControl自身有两种状态:默认态和刷新态,我们通过beginRefreshing和endRefreshing切换这两种状态。显而易见,我们想实现的效果是,当NSURLSessionTask在运行的时候,UIRefreshControl处于刷新态;当NSURLSessionTask任务结束的时候,UIRefreshControl处于默认态;所以需要把UIRefreshControl的状态绑定到NSURLSessionTask上。
具体怎么做呢,对于AFN内部的NSURLSessionTask,它的状态改变时会发出通知。所以作者监听这个NSURLSessionTask的三种状态通知(Resume、Suspend和Complete),接收到通知的时候,更新UIRefreshControl的状态(调用beginRefreshing或者endRefreshing)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| - (void)setRefreshingWithStateOfTask:(NSURLSessionTask *)task { NSNotificationCenter *notificationCenter = [NSNotificationCenter defaultCenter]; [notificationCenter removeObserver:self name:AFNetworkingTaskDidResumeNotification object:nil]; [notificationCenter removeObserver:self name:AFNetworkingTaskDidSuspendNotification object:nil]; [notificationCenter removeObserver:self name:AFNetworkingTaskDidCompleteNotification object:nil]; if (task) { UIRefreshControl *refreshControl = self.refreshControl; if (task.state == NSURLSessionTaskStateRunning) { [refreshControl beginRefreshing]; [notificationCenter addObserver:self selector:@selector(af_beginRefreshing) name:AFNetworkingTaskDidResumeNotification object:task]; [notificationCenter addObserver:self selector:@selector(af_endRefreshing) name:AFNetworkingTaskDidCompleteNotification object:task]; [notificationCenter addObserver:self selector:@selector(af_endRefreshing) name:AFNetworkingTaskDidSuspendNotification object:task]; } else { [refreshControl endRefreshing]; } } }
- (void)af_beginRefreshing { dispatch_async(dispatch_get_main_queue(), ^{ [self.refreshControl beginRefreshing]; }); }
- (void)af_endRefreshing { dispatch_async(dispatch_get_main_queue(), ^{ [self.refreshControl endRefreshing]; }); }
|
为了接口的简便,作者给UIRefreshControl增加了关联属性,并增加一个AFRefreshControlNotificationObserver类全权处理所有逻辑,可以看一下关联属性标准的懒加载写法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| @implementation UIRefreshControl (AFNetworking) - (AFRefreshControlNotificationObserver *)af_notificationObserver { AFRefreshControlNotificationObserver *notificationObserver = objc_getAssociatedObject(self, @selector(af_notificationObserver)); if (notificationObserver == nil) { notificationObserver = [[AFRefreshControlNotificationObserver alloc] initWithActivityRefreshControl:self]; objc_setAssociatedObject(self, @selector(af_notificationObserver), notificationObserver, OBJC_ASSOCIATION_RETAIN_NONATOMIC); } return notificationObserver; }
- (void)setRefreshingWithStateOfTask:(NSURLSessionTask *)task { [[self af_notificationObserver] setRefreshingWithStateOfTask:task]; } @end
|
四、获取WKWebView请求的进度和回调
使用WKWebView加载请求(loadRequest)的时候,我们只能在WKWebView的代理中获取到结果回调,而无法知晓进度。AFN给WKWebView加了扩展,支持异步加载Request,获取进度(NSProgress)和回调。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| @interface WKWebView (AFNetworking)
- (void)loadRequest:(NSURLRequest *)request navigation:(WKNavigation * _Nonnull)navigation progress:(NSProgress * _Nullable __autoreleasing * _Nullable)progress success:(nullable NSString * (^)(NSHTTPURLResponse *response, NSString *HTML))success failure:(nullable void (^)(NSError *error))failure;
- (void)loadRequest:(NSURLRequest *)request navigation:(WKNavigation * _Nonnull)navigation MIMEType:(nullable NSString *)MIMEType textEncodingName:(nullable NSString *)textEncodingName progress:(NSProgress * _Nullable __autoreleasing * _Nullable)progress success:(nullable NSData * (^)(NSHTTPURLResponse *response, NSData *data))success failure:(nullable void (^)(NSError *error))failure; @end
|
内部实现也比较简单,直接使用作为单例的AFHTTPSessionManager请求相应的Request,开始请求的时候手动触发WKWebView的didStartProvisionalNavigation回调,请求加载完毕的时候手动触发WKWebView的didFinishNavigation回调,并把AFHTTPSessionManager的进度(NSProgress)直接透传给外界。在Request加载完毕的时候,调用WKWebView的loadData方法加载Request的结果(responseObject),把内容展示在WebView上。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40
| - (void)loadRequest:(NSURLRequest *)request navigation:(WKNavigation * _Nonnull)navigation MIMEType:(nullable NSString *)MIMEType textEncodingName:(nullable NSString *)textEncodingName progress:(NSProgress * _Nullable __autoreleasing * _Nullable)progress success:(nullable NSData * (^)(NSHTTPURLResponse *response, NSData *data))success failure:(nullable void (^)(NSError *error))failure { if (self.af_URLSessionTask.state == NSURLSessionTaskStateRunning || self.af_URLSessionTask.state == NSURLSessionTaskStateSuspended) { [self.af_URLSessionTask cancel]; } self.af_URLSessionTask = nil; __weak __typeof(self)weakSelf = self; __block NSURLSessionDataTask *dataTask; __strong __typeof(weakSelf) strongSelf = weakSelf; __strong __typeof(weakSelf.navigationDelegate) strongSelfDelegate = strongSelf.navigationDelegate; dataTask = [self.sessionManager dataTaskWithRequest:request uploadProgress:nil downloadProgress:nil completionHandler:^(NSURLResponse * _Nonnull response, id _Nullable responseObject, NSError * _Nullable error) { if (error) { if (failure) failure(error); } else { if (success) success((NSHTTPURLResponse *)response, responseObject); [strongSelf loadData:responseObject MIMEType:MIMEType characterEncodingName:textEncodingName baseURL:[dataTask.currentRequest URL]]; if ([strongSelfDelegate respondsToSelector:@selector(webView:didFinishNavigation:)]) { [strongSelfDelegate webView:strongSelf didFinishNavigation:navigation]; } } }]; self.af_URLSessionTask = dataTask; if (progress != nil) { *progress = [self.sessionManager downloadProgressForTask:dataTask]; } [self.af_URLSessionTask resume]; if ([strongSelfDelegate respondsToSelector:@selector(webView:didStartProvisionalNavigation:)]) { [strongSelfDelegate webView:self didStartProvisionalNavigation:navigation]; } }
|
五、总结
AFN的分类主要就是上面所说的四种能力,代码逻辑比较清晰,也没有啥难点。比较好玩的其实是LRU的实现。下面可以说是标准写法了,synchronizationQueue是并发队列;删除方法和增加方法使用栅栏函数,实现删除操作和其他操作互斥、增加操作和其他操作互斥,从而保证线程安全。
尤其要注意的是增加操作内部有两块逻辑:更新已有缓存和超过缓存容量清除最久远资源。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61
| - (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; }
- (nullable UIImage *)imageWithIdentifier:(NSString *)identifier { __block UIImage *image = nil; dispatch_sync(self.synchronizationQueue, ^{ AFCachedImage *cachedImage = self.cachedImages[identifier]; image = [cachedImage accessImage]; }); return image; }
|