一直想写关于NSURLSession的总结,但是这些天有点忙,忙着了解一些新的方案。对于NSURLSession是怎样使用的,我想大家都很熟了,我就不在多说了。但是还是需要写些代码,下面看看创建NSURLSession代码,有三个参数,1、指定配置,2、设置代理 3、队列代理。那么干嘛还要设置delegateQueue呢?带着这个疑问往下走吧。
NSURLSessionConfiguration *sessionConfig = [NSURLSessionConfiguration defaultSessionConfiguration];
NSURLSession session = [NSURLSession sessionWithConfiguration:sessionConfig delegate:self delegateQueue:nil];
session.sessionDescription = @"My NSURLSession";
下载一章图片,这个没什么可说的。这段代码最好写成一个函数,方便下面的测试。
NSString *url = @"http://farm3.staticflickr.com/2846/9823925914_78cd653ac9_b_d.jpg";
NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:url]];
NSURLSessionDataTask *task = [session downloadTaskWithRequest:request];
[task resume];
接下来在下载完成的回调里面打印线程。
- (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didFinishDownloadingToURL:(NSURL *)location
{
NSLog(@"%@",[NSThread currentThread]);
}
多启动几个Task,看如下打印结果。
2016-04-15 08:26:25.800 BackgroundDownload[570:7122] <NSThread: 0x7ff691b42c60>{number = 5, name = (null)}
2016-04-15 08:26:27.203 BackgroundDownload[570:7242] <NSThread: 0x7ff69062d3d0>{number = 6, name = (null)}
2016-04-15 08:26:28.437 BackgroundDownload[570:7258] <NSThread: 0x7ff69062dca0>{number = 7, name = (null)}
2016-04-15 08:26:29.739 BackgroundDownload[570:7242] <NSThread: 0x7ff69062d3d0>{number = 6, name = (null)}
2016-04-15 08:26:30.965 BackgroundDownload[570:7122] <NSThread: 0x7ff691b42c60>{number = 5, name = (null)}
2016-04-15 08:26:39.583 BackgroundDownload[570:7258] <NSThread: 0x7ff69062dca0>{number = 7, name = (null)}
2016-04-15 08:26:40.824 BackgroundDownload[570:7122] <NSThread: 0x7ff691b42c60>{number = 5, name = (null)}
2016-04-15 08:26:41.737 BackgroundDownload[570:7256] <NSThread: 0x7ff690608740>{number = 8, name = (null)}
2016-04-15 08:26:42.842 BackgroundDownload[570:7241] <NSThread: 0x7ff69064ff90>{number = 4, name = (null)}
2016-04-15 08:26:44.071 BackgroundDownload[570:7242] <NSThread: 0x7ff69062d3d0>{number = 6, name = (null)}
发现NSURLSession的并发由系统控制了,至于并发数是多少呢?或者说线程池怎样控制呢?,我们不知道,只知道系统会控制好的。不用自己操心当然是好事啦,但是并不是每次都是好事,有时候我们需要调优,怎么办呢?例如在做图片下载的时候,我们是需要控制并发的,不然内存占用会比较高,甚至崩溃了。
下面看看AFN的是怎样做的。部分代码如下:
self.operationQueue = [[NSOperationQueue alloc] init];
self.operationQueue.maxConcurrentOperationCount = 1;
self.session = [NSURLSession sessionWithConfiguration:self.sessionConfiguration delegate:self delegateQueue:self.operationQueue];
AFN统一为代理配置了一个单线程队列,然后通过GCD并发处理响应完成的数据。当然你也可以设置主线程队列为代理队列,这样就可以在代理里面操作UI了。但是AFN为什么要统一配置一个单线程代理队列呢?我猜是方便任务管理。
- (void)URLSession:(__unused NSURLSession *)session
task:(NSURLSessionTask *)task
didCompleteWithError:(NSError *)error
{
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wgnu"
__strong AFURLSessionManager *manager = self.manager;
__block id responseObject = nil;
__block NSMutableDictionary *userInfo = [NSMutableDictionary dictionary];
userInfo[AFNetworkingTaskDidCompleteResponseSerializerKey] = manager.responseSerializer;
//Performance Improvement from #2672
NSData *data = nil;
if (self.mutableData) {
data = [self.mutableData copy];
//We no longer need the reference, so nil it out to gain back some memory.
self.mutableData = nil;
}
if (self.downloadFileURL) {
userInfo[AFNetworkingTaskDidCompleteAssetPathKey] = self.downloadFileURL;
} else if (data) {
userInfo[AFNetworkingTaskDidCompleteResponseDataKey] = data;
}
if (error) {
userInfo[AFNetworkingTaskDidCompleteErrorKey] = error;
dispatch_group_async(manager.completionGroup ?: url_session_manager_completion_group(), manager.completionQueue ?: dispatch_get_main_queue(), ^{
if (self.completionHandler) {
self.completionHandler(task.response, responseObject, error);
}
dispatch_async(dispatch_get_main_queue(), ^{
[[NSNotificationCenter defaultCenter] postNotificationName:AFNetworkingTaskDidCompleteNotification object:task userInfo:userInfo];
});
});
} else {
dispatch_async(url_session_manager_processing_queue(), ^{
NSError *serializationError = nil;
responseObject = [manager.responseSerializer responseObjectForResponse:task.response data:data error:&serializationError];
if (self.downloadFileURL) {
responseObject = self.downloadFileURL;
}
if (responseObject) {
userInfo[AFNetworkingTaskDidCompleteSerializedResponseKey] = responseObject;
}
if (serializationError) {
userInfo[AFNetworkingTaskDidCompleteErrorKey] = serializationError;
}
dispatch_group_async(manager.completionGroup ?: url_session_manager_completion_group(), manager.completionQueue ?: dispatch_get_main_queue(), ^{
if (self.completionHandler) {
self.completionHandler(task.response, responseObject, serializationError);
}
dispatch_async(dispatch_get_main_queue(), ^{
[[NSNotificationCenter defaultCenter] postNotificationName:AFNetworkingTaskDidCompleteNotification object:task userInfo:userInfo];
});
});
});
}
#pragma clang diagnostic pop
}
从上面代码可以看出,AFN用了大量的异步处理,事实上iOS很多框架都是使用大量的异步处理的。说了那么多,都忘记了正题了,NSURLSession怎样控制并发呢?继续看代码吧,还是AFN框架里面的代码--图片下载器(AFImageDownloader)。
- (instancetype)init {
NSURLSessionConfiguration *defaultConfiguration = [self.class defaultURLSessionConfiguration];
AFHTTPSessionManager *sessionManager = [[AFHTTPSessionManager alloc] initWithSessionConfiguration:defaultConfiguration];
sessionManager.responseSerializer = [AFImageResponseSerializer serializer];
return [self initWithSessionManager:sessionManager
downloadPrioritization:AFImageDownloadPrioritizationFIFO
maximumActiveDownloads:4
imageCache:[[AFAutoPurgingImageCache alloc] init]];
}
- (instancetype)initWithSessionManager:(AFHTTPSessionManager *)sessionManager
downloadPrioritization:(AFImageDownloadPrioritization)downloadPrioritization
maximumActiveDownloads:(NSInteger)maximumActiveDownloads
imageCache:(id <AFImageRequestCache>)imageCache {
if (self = [super init]) {
self.sessionManager = sessionManager;
self.downloadPrioritizaton = downloadPrioritization;
self.maximumActiveDownloads = maximumActiveDownloads;
self.imageCache = imageCache;
self.queuedMergedTasks = [[NSMutableArray alloc] init];
self.mergedTasks = [[NSMutableDictionary alloc] init];
self.activeRequestCount = 0;
NSString *name = [NSString stringWithFormat:@"com.alamofire.imagedownloader.synchronizationqueue-%@", [[NSUUID UUID] UUIDString]];
self.synchronizationQueue = dispatch_queue_create([name cStringUsingEncoding:NSASCIIStringEncoding], DISPATCH_QUEUE_SERIAL);
name = [NSString stringWithFormat:@"com.alamofire.imagedownloader.responsequeue-%@", [[NSUUID UUID] UUIDString]];
self.responseQueue = dispatch_queue_create([name cStringUsingEncoding:NSASCIIStringEncoding], DISPATCH_QUEUE_CONCURRENT);
}
return self;
}
+ (instancetype)defaultInstance {
static AFImageDownloader *sharedInstance = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
sharedInstance = [[self alloc] init];
});
return sharedInstance;
}
在上面的代码可以看到maximumActiveDownloads:4,最大的活动下载数是4,那么是这样控制的呢?看看如下方法的
- (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 {
...
// 5) Either start the request or enqueue it depending on the current active request count
if ([self isActiveRequestCountBelowMaximumLimit]) {
[self startMergedTask:mergedTask];
} else {
[self enqueueMergedTask:mergedTask];
}
...
}
原来是这样控制并发的,自己判断是否大于4来选择启动任务或加入等待队列。这样看来官方并没有为提够NSURLSession控制并发的设置,或者是苹果太自信了吧,认为系统控制是最好的吧。下面继续看PINRemoteImage是怎样使用NSURLSession的吧!
- (instancetype)initWithSessionConfiguration:(NSURLSessionConfiguration *)configuration
{
if (self = [super init]) {
self.sessionManagerLock = [[NSLock alloc] init];
self.sessionManagerLock.name = @"PINURLSessionManager";
self.operationQueue = [[NSOperationQueue alloc] init];
self.operationQueue.name = @"PINURLSessionManager Operation Queue";
//queue must be serial to ensure proper ordering
[self.operationQueue setMaxConcurrentOperationCount:1];
self.session = [NSURLSession sessionWithConfiguration:configuration delegate:self delegateQueue:self.operationQueue];
self.completions = [[NSMutableDictionary alloc] init];
self.delegateQueues = [[NSMutableDictionary alloc] init];
}
return self;
}
其他代码就贴了,思想上和AFN差不多,PINRemoteImage只是NSMutableDictionary来管理队列而已,而AFN用了GCD的group来控制罢了。
下面是PINRemoteImage下载器的默认配置,最大10个并发任务。而异步操作UI的队列没有最大限制。
- (instancetype)initWithSessionConfiguration:(NSURLSessionConfiguration *)configuration
{
if (self = [super init]) {
self.cache = [self defaultImageCache];
if (!configuration) {
configuration = [NSURLSessionConfiguration defaultSessionConfiguration];
}
_callbackQueue = dispatch_queue_create("PINRemoteImageManagerCallbackQueue", DISPATCH_QUEUE_CONCURRENT);
_lock = [[PINRemoteLock alloc] initWithName:@"PINRemoteImageManager"];
_concurrentOperationQueue = [[NSOperationQueue alloc] init];
_concurrentOperationQueue.name = @"PINRemoteImageManager Concurrent Operation Queue";
_concurrentOperationQueue.maxConcurrentOperationCount = NSOperationQueueDefaultMaxConcurrentOperationCount;
if ([[self class] supportsQOS]) {
_concurrentOperationQueue.qualityOfService = NSQualityOfServiceUtility;
}
_urlSessionTaskQueue = [[NSOperationQueue alloc] init];
_urlSessionTaskQueue.name = @"PINRemoteImageManager Concurrent URL Session Task Queue";
_urlSessionTaskQueue.maxConcurrentOperationCount = 10;
self.sessionManager = [[PINURLSessionManager alloc] initWithSessionConfiguration:configuration];
self.sessionManager.delegate = self;
self.estimatedRemainingTimeThreshold = 0.0;
self.timeout = PINRemoteImageManagerDefaultTimeout;
_highQualityBPSThreshold = 500000;
_lowQualityBPSThreshold = 50000; // approximately edge speeds
_shouldUpgradeLowQualityImages = NO;
_shouldBlurProgressive = YES;
_maxProgressiveRenderSize = CGSizeMake(1024, 1024);
self.tasks = [[NSMutableDictionary alloc] init];
self.canceledTasks = [[NSMutableSet alloc] init];
self.taskQOS = [[NSMutableArray alloc] initWithCapacity:5];
}
return self;
}
正常情况我们用好开源框架就好了,但是特殊情况需要我们自己定制或者修改开源库的时候,我们就需要了解原理和知道别人是怎样做的了。假如系统的NSURLConnection出现了bug,使用SDWebImage的同学紧急情况可能就要自己修改了。
网友评论
事实上,afurlsessionmanager, 内建有task delegate handle 对象,也并不需要串行回调。。