1.整体架构
2.核心类 AFURLSessionManager
2.1.初始化方法 init
self.operationQueue = [[NSOperationQueue alloc] init];
//queue并发线程数设置为1
self.operationQueue.maxConcurrentOperationCount = 1;
1)首先我们要明确一个概念,这里的并发数仅仅是回调代理的线程并发数。而不是请求网络的线程并发数。请求网络是由NSUrlSession来做的,它内部维护了一个线程池,用来做网络请求。它调度线程,基于底层的CFSocket去发送请求和接收数据。这些线程是并发的。
2)明确了这个概念之后,我们来梳理一下AF3.x的整个流程和线程的关系:
我们一开始初始化sessionManager
的时候,一般都是在主线程,(当然不排除有些人喜欢在分线程初始化...)
- 然后我们调用get
或者post
等去请求数据,接着会进行request
拼接,AF代理的字典映射,progress
的KVO
添加等等,到NSUrlSession
的resume
之前这些准备工作,仍旧是在主线程中的。- 然后我们调用NSUrlSession
的resume
,接着就跑到NSUrlSession
内部去对网络进行数据请求了,在它内部是多线程并发的去请求数据的。- 紧接着数据请求完成后,回调回来在我们一开始生成的并发数为1的NSOperationQueue
中,这个时候会是多线程串行的回调回来的。- 然后我们到返回数据解析那一块,我们自己又创建了并发的多线程,去对这些数据进行了各种类型的解析。
最后我们如果有自定义的completionQueue
,则在自定义的queue
中回调回来,也就是分线程回调回来,否则就是主队列,主线程中回调结束。3)最后我们来解释解释为什么回调Queue要设置并发数为1:
我认为AF这么做有以下两点原因:
1)众所周知,AF2.x所有的回调是在一条线程,这条线程是AF的常驻线程,而这一条线程正是AF调度request的思想精髓所在,所以第一个目的就是为了和之前版本保持一致。
2)因为跟代理相关的一些操作AF都使用了NSLock。所以就算Queue的并发数设置为n,因为多线程回调,锁的等待,导致所提升的程序速度也并不明显。反而多task回调导致的多线程并发,平白浪费了部分性能。而设置Queue的并发数为1,(注:这里虽然回调Queue的并发数为1,仍然会有不止一条线程,但是因为是串行回调,所以同一时间,只会有一条线程在操作AFUrlSessionManager的那些方法。)至少回调的事件,是不需要多线程并发的。回调没有了NSLock的等待时间,所以对时间并没有多大的影响。(注:但是还是会有多线程的操作的,因为设置刚开始调起请求的时候,是在主线程的,而回调则是串行分线程。)
// 设置存储NSURL task与AFURLSessionManagerTaskDelegate的词典(重点,在AFNet中,每一个task都会被匹配一个AFURLSessionManagerTaskDelegate 来做task的delegate事件处理)
这个是用来让每一个请求task和我们自定义的AF代理来建立映射用的,其实AF对task的代理进行了一个封装,并且转发代理到AF自定义的代理
===============
self.mutableTaskDelegatesKeyedByTaskIdentifier = [[NSMutableDictionary alloc] init];
2.2.生成request中,参数序列化
AFQueryStringFromParameters
@{
@"name" : @"bang",
@"phone": @{@"mobile": @"xx", @"home": @"xx"},
@"families": @[@"father", @"mother"],
@"nums": [NSSet setWithObjects:@"1", @"2", nil]
}
->
@[
field: @"name", value: @"bang",
field: @"phone[mobile]", value: @"xx",
field: @"phone[home]", value: @"xx",
field: @"families[]", value: @"father",
field: @"families[]", value: @"mother",
field: @"nums", value: @"1",
field: @"nums", value: @"2",
]
->
name=bang&phone[mobile]=xx&phone[home]=xx&families[]=father&families[]=mother&nums=1&num=2
紧接着这个方法还根据该request中请求类型,来判断参数字符串应该如何设置到request中去。如果是GET、HEAD、DELETE,则把参数quey是拼接到url后面的。而POST、PUT是把query拼接到http body中的:
2.3.请求数据成功后可以不回调在主线程
if (serializationError) {
if (failure) {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wgnu"
//如果解析错误,直接返回
dispatch_async(self.completionQueue ?: dispatch_get_main_queue(), ^{
failure(nil, serializationError);
});
#pragma clang diagnostic pop
}
return nil;
}
当解析错误,我们直接调用传进来的fauler的Block失败返回了,这里有一个self.completionQueue,这个是我们自定义的,这个是一个GCD的Queue如果设置了那么从这个Queue中回调结果,否则从主队列回调。
实际上这个Queue还是挺有用的,之前还用到过。我们公司有自己的一套数据加解密的解析模式,所以我们回调回来的数据并不想是主线程,我们可以设置这个Queue,在分线程进行解析数据,然后自己再调回到主线程去刷新UI。
2.4.处理 task
- (NSURLSessionDataTask *)dataTaskWithRequest....
//第一件事,创建NSURLSessionDataTask,里面适配了Ios8以下taskIdentifiers,函数创建task对象。
//其实现应该是因为iOS 8.0以下版本中会并发地创建多个task对象,而同步有没有做好,导致taskIdentifiers 不唯一…这边做了一个串行处理
url_session_manager_create_task_safely(^{
dataTask = [self.session dataTaskWithRequest:request];
});
url_session_manager_create_task_safely
static void url_session_manager_create_task_safely(dispatch_block_t block) {
if (NSFoundationVersionNumber < NSFoundationVersionNumber_With_Fixed_5871104061079552_bug) {
// Fix of bug
// Open Radar:http://openradar.appspot.com/radar?id=5871104061079552 (status: Fixed in iOS8)
// Issue about:https://github.com/AFNetworking/AFNetworking/issues/2093
//理解下,第一为什么用sync,因为是想要主线程等在这,等执行完,在返回,因为必须执行完dataTask才有数据,传值才有意义。
//第二,为什么要用串行队列,因为这块是为了防止ios8以下内部的dataTaskWithRequest是并发创建的,
//这样会导致taskIdentifiers这个属性值不唯一,因为后续要用taskIdentifiers来作为Key对应delegate。
dispatch_sync(url_session_manager_creation_queue(), block);
} else {
block();
}
}
static dispatch_queue_t url_session_manager_creation_queue() {
static dispatch_queue_t af_url_session_manager_creation_queue;
static dispatch_once_t onceToken;
//保证了即使是在多线程的环境下,也不会创建其他队列
dispatch_once(&onceToken, ^{
af_url_session_manager_creation_queue = dispatch_queue_create("com.alamofire.networking.session.manager.creation", DISPATCH_QUEUE_SERIAL);
});
return af_url_session_manager_creation_queue;
}
原来这是为了适配iOS8的以下,创建session的时候,偶发的情况会出现session的属性taskIdentifier这个值不唯一,而这个taskIdentifier是我们后面来映射delegate的key,所以它必须是唯一的。
具体原因应该是NSURLSession内部去生成task的时候是用多线程并发去执行的。
2.5.- (void)setDelegate:
//断言,如果没有这个参数,debug下crash在这
NSParameterAssert(task);
NSParameterAssert(delegate);
//加锁保证字典线程安全
[self.lock lock];
// 将AF delegate放入以taskIdentifier标记的词典中(同一个NSURLSession中的taskIdentifier是唯一的)
self.mutableTaskDelegatesKeyedByTaskIdentifier[@(task.taskIdentifier)] = delegate;
// 为AF delegate 设置task 的progress监听
[delegate setupProgressForTask:task];
//添加task开始和暂停的通知
[self addNotificationObserverForTask:task];
[self.lock unlock];
加锁的原因是因为本身我们这个字典属性是mutable的,是线程不安全的。而我们对这些方法的调用,确实是会在复杂的多线程环境中
之前的setProgress和这个KVO监听,都是在我们AF自定义的delegate内的,是有一个task就会有一个delegate的。所以说我们是每个task都会去监听这些属性,分别在各自的AF代理内
2.6.session 代理
self.session = [NSURLSession sessionWithConfiguration:self.sessionConfiguration delegate:self delegateQueue:self.operationQueue];
我们把AFUrlSessionManager作为了所有的task的delegate。当我们请求网络的时候,这些代理开始调用了
而只转发了其中3条到AF自定义的delegate中:
AFUrlSessionManager对这一大堆代理做了一些公共的处理,而转发到AF自定义代理的3条,则负责把每个task对应的数据回调出去。
@property (readwrite, nonatomic, copy) AFURLSessionDownloadTaskDidResumeBlock downloadTaskDidResume;
各自对应这样的set方法
- (void)setSessionDidBecomeInvalidBlock:(void (^)(NSURLSession *session, NSError *error))block {
self.sessionDidBecomeInvalid = block;
}
设计思路:
1.作者用@property把这个些Block属性在.m文件中声明,然后复写了set方法。
2.然后在.h中去声明这些set方法:
- (void)setSessionDidBecomeInvalidBlock:(nullable void (^)(NSURLSession *session, NSError *error))block;
为什么要绕这么一大圈呢?原来这是为了我们这些用户使用起来方便,调用set方法去设置这些Block,能很清晰的看到Block的各个参数与返回值。
3.NSURLSessionDelegate
1.每个代理方法对应一个我们自定义的Block,如果Block被赋值了,那么就调用它。
2.在这些代理方法里,我们做的处理都是相对于这个sessionManager所有的request的。是公用的处理。
3.转发了3个代理方法到AF的deleagate中去了,AF中的deleagate是需要对应每个task去私有化处理的。
1.当前这个session已经失效时,该代理方法被调用。
/*
如果你使用finishTasksAndInvalidate函数使该session失效,
那么session首先会先完成最后一个task,然后再调用URLSession:didBecomeInvalidWithError:代理方法,
如果你调用invalidateAndCancel方法来使session失效,那么该session会立即调用上面的代理方法。
*/
- (void)URLSession:(NSURLSession *)session
didBecomeInvalidWithError:(NSError *)error
2.https认证
- (void)URLSession:(NSURLSession *)session
didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge
completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential *credential))completionHandler
15.用来做断点续传的代理方法
//当下载被取消或者失败后重新恢复下载时调用
- (void)URLSession:(NSURLSession *)session
downloadTask:(NSURLSessionDownloadTask *)downloadTask
didResumeAtOffset:(int64_t)fileOffset
expectedTotalBytes:(int64_t)expectedTotalBytes
这3个方法,被转调到AF自定义delegate中
8. didCompleteWithError
/*
task完成之后的回调,成功和失败都会回调这里
函数讨论:
注意这里的error不会报告服务期端的error,他表示的是客户端这边的eroor,比如无法解析hostname或者连不上host主机。
*/
- (void)URLSession:(NSURLSession *)session
task:(NSURLSessionTask *)task
didCompleteWithError:(NSError *)error
11. didReceiveData
//当我们获取到数据就会调用,会被反复调用,请求到的数据就在这被拼装完整
- (void)URLSession:(NSURLSession *)session
dataTask:(NSURLSessionDataTask *)dataTask
didReceiveData:(NSData *)data
13. NSURLSessionDownloadDelegate
//下载完成的时候调用
- (void)URLSession:(NSURLSession *)session
downloadTask:(NSURLSessionDownloadTask *)downloadTask
didFinishDownloadingToURL:(NSURL *)location
NSURLSessionConfiguration
NSURLSession对象的初始化需要使用NSURLSessionConfiguration,而NSURLSessionConfiguration有三个类工厂方法:
+defaultSessionConfiguration 返回一个标准的 configuration,这个配置实际上与 NSURLConnection 的网络堆栈(networking stack)是一样的,具有相同的共享 NSHTTPCookieStorage,共享 NSURLCache 和共享NSURLCredentialStorage。
+ephemeralSessionConfiguration 返回一个预设配置,这个配置中不会对缓存,Cookie 和证书进行持久性的存储。这对于实现像秘密浏览这种功能来说是很理想的。
+backgroundSessionConfiguration:(NSString *)identifier 的独特之处在于,它会创建一个后台 session。后台 session 不同于常规的,普通的 session,它甚至可以在应用程序挂起,退出或者崩溃的情况下运行上传和下载任务。初始化时指定的标识符,被用于向任何可能在进程外恢复后台传输的守护进程(daemon)提供上下文。
4. AFHTTPResponseSerializer 解析数据
// 判断是不是可接受类型和可接受code,不是则填充error
- (BOOL)validateResponse:(NSHTTPURLResponse *)response
data:(NSData *)data
error:(NSError * __autoreleasing *)error
可接受类型 self.acceptableContentTypes
可接受code self.acceptableStatusCodes
5. _AFURLSessionTaskSwizzling
在AFURLSessionManager中,有这么一个类:_AFURLSessionTaskSwizzling。这个类大概的作用就是替换掉NSUrlSession中的resume和suspend方法。正常处理原有逻辑的同时,多发送一个通知
AFNSURLSessionTaskDidResumeNotification
AFNSURLSessionTaskDidSuspendNotification
这是因为iOS7和iOS8的NSURLSessionTask的继承链不同导致的
目前我们所知的:
- NSURLSessionTasks是一组class的统称,如果你仅仅使用提供的API来获取NSURLSessionTask的class,并不一定返回的是你想要的那个(获取NSURLSessionTask的class目的是为了获取其resume方法)
- 简单地使用[NSURLSessionTask class]并不起作用。你需要新建一个NSURLSession,并根据创建的session再构建出一个NSURLSessionTask对象才行。
- iOS 7上,localDataTask(下面代码构造出的NSURLSessionDataTask类型的变量,为了获取对应Class)的类型是 NSCFLocalDataTask,NSCFLocalDataTask继承自NSCFLocalSessionTask,NSCFLocalSessionTask继承自__NSCFURLSessionTask。
- iOS 8上,localDataTask的类型为NSCFLocalDataTask,NSCFLocalDataTask继承自NSCFLocalSessionTask,NSCFLocalSessionTask继承自NSURLSessionTask
- iOS 7上,NSCFLocalSessionTask和NSCFURLSessionTask是仅有的两个实现了resume和suspend方法的类,另外NSCFLocalSessionTask中的resume和suspend并没有调用其父类(即NSCFURLSessionTask)方法,这也意味着两个类的方法都需要进行method swizzling。
- iOS 8上,NSURLSessionTask是唯一实现了resume和suspend方法的类。这也意味着其是唯一需要进行method swizzling的类
- 因为NSURLSessionTask并不是在每个iOS版本中都存在,所以把这些放在此处(即load函数中),比如给一个dummy class添加swizzled方法都会变得很方便,管理起来也方便。
6. AFNetworking 2.x
6.1.核心类
1)AFURLConnectionOperation
AF2.x是基于NSURLConnection来封装的,而NSURLConnection的创建以及数据请求,就被封装在AFURLConnectionOperation这个类中
2)AFHTTPRequestOperation
继承自AFURLConnectionOperation
3)AFHTTPRequestOperationManager
管理这些这些operation
6.2.init
+ (void)networkRequestThreadEntryPoint:(id)__unused object {
@autoreleasepool {
[[NSThread currentThread] setName:@"AFNetworking"];
NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
//添加端口,防止runloop直接退出
[runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
[runLoop run];
}
}
这是一个单例,用NSThread创建了一个线程,并且为这个线程添加了一个runloop,并且加了一个NSMachPort,来防止runloop直接退出。
这条线程就是AF用来发起网络请求,并且接受网络请求回调的线程,仅仅就这一条线程
6.3.- (void)setState:(AFOperationState)state
- (void)setState:(AFOperationState)state {
//判断从当前状态到另一个状态是不是合理,在加上现在是否取消。。大神的框架就是屌啊,这判断严谨的。。一层层
if (!AFStateTransitionIsValid(self.state, state, [self isCancelled])) {
return;
}
[self.lock lock];
//拿到对应的父类管理当前线程周期的key
NSString *oldStateKey = AFKeyPathFromOperationState(self.state);
NSString *newStateKey = AFKeyPathFromOperationState(state);
//发出KVO
[self willChangeValueForKey:newStateKey];
[self willChangeValueForKey:oldStateKey];
_state = state;
[self didChangeValueForKey:oldStateKey];
[self didChangeValueForKey:newStateKey];
[self.lock unlock];
}
这个方法改变state的时候,并且发送了KVO。大家了解NSOperationQueue就知道,如果对应的operation的属性finnished被设置为YES,则代表当前operation结束了,会把operation从队列中移除,并且调用operation的completionBlock。这点很重要,因为我们请求到的数据就是从这个completionBlock中传递回去的(下面接着讲这个完成Block,就能从这里对接上了)。
6.4.为什么AF2.x需要一条常驻线程?
首先如果我们用NSURLConnection,我们为了获取请求结果有以下三种选择:
1.在主线程调异步接口
2.每一个请求用一个线程,对应一个runloop,然后等待结果回调。
3.只用一条线程,一个runloop,所有结果回调在这个线程上。
很显然AF选择的是第3种方式,创建了一条常驻线程专门处理所有请求的回调事件.这个模型跟nodejs有点类似
7.总结
一. 首先我们需要明确一点的是:
相对于AFNetworking2.x,AFNetworking3.x确实没那么有用了。AFNetworking之前的核心作用就是为了帮我们去调度所有的请求。但是最核心地方却被苹果的NSURLSession给借鉴过去
二.除此之外
1.首先它帮我们做了各种请求方式request的拼接。
2.它还帮我们做了一些公用参数(session级别的),和一些私用参数(task级别的)的分离。
3.它帮我们做了自定义的https认证处理。
4.对于请求到的数据,AF帮我们做了各种格式的数据解析,并且支持我们设置自定义的code范围,自定义的数据方式。
5.对于成功和失败的回调处理。
6.绕开了很多的坑:回调线程数设置为1的问题,系统内部并行创建task导致id不唯一等等
8.UIImageView+AFNetworking
8.1. AFImageDownloader
AF自己控制的图片缓存用AFAutoPurgingImageCache,而NSUrlRequest的缓存由它自己内部根据策略去控制,用的是NSURLCache,不归AF处理,只需在configuration中设置上即可
- (instancetype)initWithSessionManager:(AFHTTPSessionManager *)sessionManager
downloadPrioritization:(AFImageDownloadPrioritization)downloadPrioritization
maximumActiveDownloads:(NSInteger)maximumActiveDownloads
imageCache:(id <AFImageRequestCache>)imageCache
//创建一个串行的queue
self.synchronizationQueue = dispatch_queue_create([name cStringUsingEncoding:NSASCIIStringEncoding], DISPATCH_QUEUE_SERIAL);
name = [NSString stringWithFormat:@"com.alamofire.imagedownloader.responsequeue-%@", [[NSUUID UUID] UUIDString]];
//创建并行queue
self.responseQueue = dispatch_queue_create([name cStringUsingEncoding:NSASCIIStringEncoding], DISPATCH_QUEUE_CONCURRENT);
这个串行queue,是用来做内部生成task等等一系列业务逻辑的。它保证了我们在这些逻辑处理中的线程安全问题(迷惑的接着往下看)。
这个并行queue,被用来做网络请求完成的数据回调。
8.2. AFAutoPurgingImageCache
- (instancetype)init {
//默认为内存100M,后者为缓存溢出后保留的内存
return [self initWithMemoryCapacity:100 * 1024 * 1024 preferredMemoryCapacity:60 * 1024 * 1024];
}
声明了一个默认的内存缓存大小100M,还有一个意思是如果超出100M之后,我们去清除缓存,此时仍要保留的缓存大小60M
- (instancetype)initWithMemoryCapacity:(UInt64)memoryCapacity preferredMemoryCapacity:(UInt64)preferredMemoryCapacity
//并行的queue
self.synchronizationQueue = dispatch_queue_create([queueName cStringUsingEncoding:NSASCIIStringEncoding], DISPATCH_QUEUE_CONCURRENT);
创建了一个并行queue,这个并行queue,这个类除了初始化以外,所有的方法都是在这个并行queue中调用的。
//移除所有图片
- (BOOL)removeAllImages {
__block BOOL removed = NO;
dispatch_barrier_sync(self.synchronizationQueue, ^{
if (self.cachedImages.count > 0) {
[self.cachedImages removeAllObjects];
self.currentMemoryUsage = 0;
removed = YES;
}
});
return removed;
}
1)这里我们可以看到使用了dispatch_barrier_sync,这里没有用锁,但是因为使用了dispatch_barrier_sync,不仅同步了synchronizationQueue队列,而且阻塞了当前线程,所以保证了里面执行代码的线程安全问题。
2)在这里其实使用锁也可以,但是AF在这的处理却是使用同步的机制来保证线程安全,或许这跟图片的加载缓存的使用场景,高频次有关系,在这里使用sync,并不需要在去开辟新的线程,浪费性能,只需要在原有线程,提交到synchronizationQueue队列中,阻塞的执行即可。这样省去大量的开辟线程与使用锁带来的性能消耗。
//添加image到cache里
- (void)addImage:(UIImage *)image withIdentifier:(NSString *)identifier {
//用dispatch_barrier_async,来同步这个并行队列
dispatch_barrier_async(self.synchronizationQueue, ^{
//生成cache对象
AFCachedImage *cacheImage = [[AFCachedImage alloc] initWithImage:image identifier:identifier];
//去之前cache的字典里取
AFCachedImage *previousCachedImage = self.cachedImages[identifier];
//如果有被缓存过
if (previousCachedImage != nil) {
//当前已经使用的内存大小减去图片的大小
self.currentMemoryUsage -= previousCachedImage.totalBytes;
}
//把新cache的image加上去
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;
//移除早期的cache bytesToPurge大小
for (AFCachedImage *cachedImage in sortedImages) {
[self.cachedImages removeObjectForKey:cachedImage.identifier];
bytesPurged += cachedImage.totalBytes;
if (bytesPurged >= bytesToPurge) {
break ;
}
}
//减去被清掉的内存
self.currentMemoryUsage -= bytesPurged;
}
});
}
当然在这里更需要说一说的是dispatch_barrier_async,这里整个类都没有使用dispatch_async,所以不存在是为了做一个栅栏,来同步上下文的线程。其实它在本类中的作用很简单,就是一个串行执行。
讲到这,小伙伴们又疑惑了,既然就是只是为了串行,那为什么我们不用一个串行queue就得了?非得用dispatch_barrier_async干嘛?其实小伙伴要是看的仔细,就明白了,上文我们说过,我们要用dispatch_barrier_sync来保证线程安全。如果我们使用串行queue,那么线程是极其容易死锁的。
8.3.流程
接下来我们来总结一下整个请求图片,缓存,然后设置图片的流程:
调用- (void)setImageWithURL:(NSURL *)url;时,我们生成
AFImageDownloader单例,并替我们请求数据。
而AFImageDownloader会生成一个AFAutoPurgingImageCache替我们缓存生成的数据。当然我们设置的时候,给session的configuration设置了一个系统级别的缓存NSUrlCache,这两者是互相独立工作的,互不影响的。
然后AFImageDownloader,就实现下载和协调AFAutoPurgingImageCache去缓存,还有一些取消下载的方法。然后通过回调把数据给到我们的类目UIImageView+AFNetworking,如果成功获取数据,则由类目设置上图片,整个流程结束。