Onelong

分享知识,与你一起进步......
RSS icon Home icon
  • 谈谈NSURLSession

    post by onelong / 2016-4-15 14:39 Friday [apple]

    一直想写关于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的同学紧急情况可能就要自己修改了。

    引用地址:
     

    我要评论