关于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)
// 图片下载管理器,默认使用[AFImageDownloader defaultInstance]
+ (void)setSharedImageDownloader:(AFImageDownloader *)imageDownloader;
+ (AFImageDownloader *)sharedImageDownloader;
// 设置UIButton的图片,若有缓存直接使用缓存,否则立即设置占位图片并开始下载。调用这个方法会立即取消原先的下载请求。一旦自己设置了success回调, 那需要自己对UIButton设置图片
- (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;
// 取消下载的task
- (void)cancelImageDownloadTaskForState:(UIControlState)state;
- (void)cancelBackgroundImageDownloadTaskForState:(UIControlState)state;
@end


@interface UIImageView (AFNetworking)
// 默认是[AFImageDownloader defaultInstance]
+ (void)setSharedImageDownloader:(AFImageDownloader *)imageDownloader;
+ (AFImageDownloader *)sharedImageDownloader;
- (void)setImageWithURL:(NSURL *)url;
// 默认情况下: Accept的值为"image / *", 缓存策略为NSURLCacheStorageAllowed, 超时时间为30秒, 没有设置cookie策略
// Accept 表示请求方希望的资源类型,或者能解析识别的类型;Content-Type 表示实际发送的资源类型
- (void)setImageWithURL:(NSURL *)url placeholderImage:(nullable UIImage *)placeholderImage;
// 设置UIImageView的图片,若有缓存直接使用缓存,否则立即设置占位图片并开启下载。调用这个方法会立即取消原先的下载请求。一旦自己设置了success回调, 那需要自己对UIButton设置图片
- (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 {
// URL空值处理
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;

// 根据urlRequest的url做id, 寻找对应的缓存
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;
// 随机生成下载凭据id (UUID(通用唯一标识符)是128位值。NSUUID创建的UUID符合RFC 4122版本4,并使用随机字节创建。)
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;
// 校验下载凭据id的一致性
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
// 定义作为Cache最基础的方法
@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
// 扩展了Cache, 添加了与NSURLRequest有关的方法
@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;
// 初始化一个默认配置的实例, memoryCapcity为100MB, preferredMemoryUsageAfterPurge为60MB
- (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;
// 计算一个image占用的内存
CGSize imageSize = CGSizeMake(image.size.width * image.scale, image.size.height * image.scale);
CGFloat bytesPerPixel = 4.0;// RGBA
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 {
// 异步直接返回,栅栏: 等queue中任务执行完毕后, 再执行添加图片操作
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];
// 根据lastAccessDate正向排序
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
// 缓存实例, 默认是使用AFAutoPurgingImageCache
@property (nonatomic, strong, nullable) id <AFImageRequestCache> imageCache;
// 根据AFImageResponseSerializer配置, 使用NSURLCache缓存的 的sessionManager
@property (nonatomic, strong) AFHTTPSessionManager *sessionManager;
// 下载顺序, 默认是FIFO 先进先出
@property (nonatomic, assign) AFImageDownloadPrioritization downloadPrioritization;
// 单例
+ (instancetype)defaultInstance;
// NSURLCache缓存 内存缓存20MB 磁盘缓存150MB
+ (NSURLCache *)defaultURLCache;
// 默认的配置
+ (NSURLSessionConfiguration *)defaultURLSessionConfiguration;
// 初始化
- (instancetype)init;
// 指定NSURLSessionConfiguration初始化
- (instancetype)initWithSessionConfiguration:(NSURLSessionConfiguration *)configuration;
// 指定AFHTTPSessionManager初始化 maximumActiveDownloads是任意时间最大下载数量, 推荐为4
- (instancetype)initWithSessionManager:(AFHTTPSessionManager *)sessionManager
downloadPrioritization:(AFImageDownloadPrioritization)downloadPrioritization
maximumActiveDownloads:(NSInteger)maximumActiveDownloads
imageCache:(nullable id <AFImageRequestCache>)imageCache;
// 创建AFImageDownloadReceipt, 如果相同的data task已经在queue中运行, 那么成功和失败的回调会被追加并按照顺序执行。如果返回的AFImageDownloadReceipt为空, 说明图片已经在缓存中了。
// 成功的回调: 如果response为空, responseObject有值, 说明是从缓存中获取到的
- (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;
// 创建AFImageDownloadReceipt, 如果相同的data task已经在queue中运行, 那么成功和失败的回调会被追加并按照顺序执行。receiptID是下载凭据的唯一标识。如果返回的AFImageDownloadReceipt为空, 说明图片已经在缓存中了。
// 成功的回调: 如果response为空, responseObject有值, 说明是从缓存中获取到的
- (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;
// 因为要拿到task并做后续处理, 所以是同步执行
dispatch_sync(self.synchronizationQueue, ^{
NSString *URLIdentifier = request.URL.absoluteString;
// URL校验
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;
}
// 原先已经存在的task, 拼接上成功和失败回调
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: {
// 尝试从Cache中加载图片
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]; /* Create a new autoreleased NSUUID with RFC 4122 version 4 random bytes */
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) {
// 下载完成后, 在并发队列responseQueue中处理
dispatch_async(self.responseQueue, ^{
__strong __typeof__(weakSelf) strongSelf = weakSelf;
AFImageDownloaderMergedTask *mergedTask = [strongSelf safelyGetMergedTask:URLIdentifier];
// 校验task的一致性
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];
// 开启下一次task
[strongSelf safelyStartNextTaskIfNecessary];
});
}];

// 获取回调handler
AFImageDownloaderResponseHandler *handler = [[AFImageDownloaderResponseHandler alloc] initWithUUID:receiptID
success:success
failure:failure];
// 创建task
AFImageDownloaderMergedTask *mergedTask = [[AFImageDownloaderMergedTask alloc]
initWithURLIdentifier:URLIdentifier
identifier:mergedTaskIdentifier
task:createdTask];
// 配置handler到task
[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;
}
}
}
});
}

// 开始执行task, activeRequestCount自加1
- (void)startMergedTask:(AFImageDownloaderMergedTask *)mergedTask {
[mergedTask.task resume];
++self.activeRequestCount;
}

// 活跃task数量小于指定的限制, 表示可以从mergedTasks中取出task进行开始下载操作
- (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
// 是否开启功能, 默认NO
@property (nonatomic, assign, getter = isEnabled) BOOL enabled;
// 网络活动指示器是否在展示
@property (readonly, nonatomic, assign, getter=isNetworkActivityIndicatorVisible) BOOL networkActivityIndicatorVisible;
// 网络请求开始之后延迟activationDelay秒, 指示器开始展示. 默认1秒, 如果要设置为立即展示, activationDelay应该为0
// 当您的应用访问网络超过几秒钟时,显示网络活动指示器以提供反馈。如果操作比这更早完成,则不必显示网络活动指示器,因为该指示器很可能在用户注意到它的存在之前消失。
@property (nonatomic, assign) NSTimeInterval activationDelay;
// 网络请求结束之后延迟completionDelay秒, 指示器停止展示 默认值为 0.17 秒。
@property (nonatomic, assign) NSTimeInterval completionDelay;
// 单例
+ (instancetype)sharedManager;
// 网络请求计数器加一, 如果在调用该方法前计时器值为0, 将会开始展示网络活动指示器
- (void)incrementActivityCount;
// 网络请求计数器减一, 如果在调用该方法后计时器值为0, 将会停止展示网络活动指示器
- (void)incrementActivityCount;
// 网络状态指示器显示和隐藏的回调, 默认为系统管理: [[UIApplication sharedApplication] setNetworkActivityIndicatorVisible:networkActivityIndicatorVisible];
- (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
// start的状态
- (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) {
// 最小值不能小于0
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
// 给UIProgressView加了拓展, 可以方便的根据NSURLSessionUploadTask或者NSURLSessionDownloadTask的状态更新UIProgressView的进度
@interface UIProgressView (AFNetworking)
- (void)setProgressWithUploadProgressOfTask:(NSURLSessionUploadTask *)task animated:(BOOL)animated;
- (void)setProgressWithDownloadProgressOfTask:(NSURLSessionDownloadTask *)task animated:(BOOL)animated;
@end

// 给UIRefreshControl加了拓展, 可以方便的根据NSURLSessionTask的状态更新UIRefreshControl的状态
@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];
// 针对task对象, 设置开始刷新和结束刷新的时机回调
[notificationCenter addObserver:self selector:@selector(af_beginRefreshing) name:AFNetworkingTaskDidResumeNotification object:task];
[notificationCenter addObserver:self selector:@selector(af_endRefreshing) name:AFNetworkingTaskDidCompleteNotification object:task];
// Suspend也作为"结束刷新状态"
[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;
}

// 给关联属性AFRefreshControlNotificationObserver配置task
- (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)
// 异步加载request 相比较webview的loadRequest可以方便获取进度和回调
- (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;
// 指定MIME类型和text编码后, 异步加载request
- (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) {
// 回传NSProgress
*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];
// 根据lastAccessDate正向排序
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;
}