AFNetWorking(3.0)中NSURLSession的使用

AFNetWoking(3.0)是一个著名的iOS第三方网络通信框架,当前最新的3.0版的实现完全基于NSURLSession进行了封装,作为一个刚刚学习NSURLSession的小白,本文主要关注AFNetWorking(3.0)中使用NSURLSession进行网络通信的逻辑。

NSURLSession是苹果公司在iOS7.0后推出用来替代NSURLConnection进行网络请求的API,有关NSURLSession的使用在我对官方文档翻译后总结的NSURLSession的使用中有介绍和说明。

AFNetWorking(3.0)中进行网络通信的类

在AFNetWorking(3.0)中,实现网络通信功能的类主要有三个

  1. AFHTTPSessionManager
  2. AFURLSessionManager
  3. AFURLSessionManagerTaskDelegate

其中AFHTTPSessionManager是AFURLSessionManager的子类,它封装了用户对于HTTP请求参数的操作,用户通过调用相关方法就能进行GET、POST、PUT等方法;
AFURLSessionManager是AFNetWorking(3.0)的核心,它封装了几乎所有与NSURLSession相关的方法,并实现了NSURLSessionDelegate、 NSURLSessionTaskDelegate,、NSURLSessionDataDelegate、NSURLSessionDownloadDelegate等委托的方法。
AFURLSessionManagerTaskDelegate也实现委托的方法,不过仅仅实现了部分,它的作用主要是管理上传与下载任务的进度。

AFNetWorking(3.0)的网络通信流程

1. 创建一个AFURLSessionManager对象

AFURLSessionManager封装了关于NSURLSession对象的操作,因此通过创建一个AFURLSessionManager对象就简化了创建NSURLSession的流程。

- (instancetype)initWithSessionConfiguration:(NSURLSessionConfiguration *)configuration {
    self = [super init];
    if (!self) {
        return nil;
    }
    //1、config为空设为默认config
    if (!configuration) {
        configuration = [NSURLSessionConfiguration defaultSessionConfiguration];
    }
    self.sessionConfiguration = configuration;

    //2、创建操作队列,设置最大并发数
    self.operationQueue = [[NSOperationQueue alloc] init];
    self.operationQueue.maxConcurrentOperationCount = 1;

    //3、创建session
    self.session = [NSURLSession sessionWithConfiguration:self.sessionConfiguration delegate:self delegateQueue:self.operationQueue];

    //4、创建响应序列化,安全策略,任务识别关键字
    self.responseSerializer = [AFJSONResponseSerializer serializer];

    self.securityPolicy = [AFSecurityPolicy defaultPolicy];

    #if !TARGET_OS_WATCH
    self.reachabilityManager = [AFNetworkReachabilityManager sharedManager];
    #endif

    self.mutableTaskDelegatesKeyedByTaskIdentifier = [[NSMutableDictionary alloc] init];
    
    //5、创建锁
    self.lock = [[NSLock alloc] init];
    self.lock.name = AFURLSessionManagerLockName;

    //6、为session管理的所有任务设置关联的AFURLSessionManagerTaskDelegate对象
    [self.session getTasksWithCompletionHandler:^(NSArray *dataTasks, NSArray *uploadTasks, NSArray *downloadTasks) {
        //1、数据任务
        for (NSURLSessionDataTask *task in dataTasks) {
            [self addDelegateForDataTask:task uploadProgress:nil downloadProgress:nil completionHandler:nil];
        }
        //2、上传任务
        for (NSURLSessionUploadTask *uploadTask in uploadTasks) {
            [self addDelegateForUploadTask:uploadTask progress:nil completionHandler:nil];
        }
        //3、下载任务
        for (NSURLSessionDownloadTask *downloadTask in downloadTasks) {
            [self addDelegateForDownloadTask:downloadTask progress:nil destination:nil completionHandler:nil];
        }
    }];

    return self;
}

通过上述代码可以看到AFURLSessionManager的初始化做了以下工作:

  1. 初始化会话配置,默认的会话配置为defaultSessionConfiguration类型
  2. 初始化操作队列,队列并发数为1,即为串行队列
  3. 通过会话配置和操作队列创建NSURLSession对象,并将自身设为委托
  4. 创建网络响应序列化工具对象
  5. 定义了网络通信的安全策略
  6. 初始化映射数组,在进行多个网络请求任务时,每个task都对应着一个AFURLSessionManagerTaskDelegate对象
  7. 为session管理的所有任务设置关联的AFURLSessionManagerTaskDelegate对象

2. 封装网络通信请求参数

有了NSURLSession对象之后,便可以进行网络通信了,iOS提供了两种方式,一种是通过URL直接进行网络请求,一种是采用NSURLRequest对象进行请求;AFNetWorking将请求统一封装成NSURLRequest对象。
使用NSURLSession进行网络请求,需要选择相应的任务类型:数据任务类型(NSURLSessionDataTask)、上传任务类型(NSURLSessionUploadTask)和下载任务类型(NSURLSessionDownloadTask)。
对于数据任务类型,AFNetWorking(3.0)通过建立子类AFHTTPSessionManager对AFURLSessionManager类进行了进一步的抽象和封装。这样用户在使用时,针对请求方法的不同选择不同的网络请求方法,只需传入以下参数便可进行网络请求:

  • 请求地址
  • 请求参数
  • 处理请求成功的block
  • 处理请求失败的block
  • 处理上传进度block
  • 处理下载进度block

虽然AFHTTPSessionMananger继承于AFURLSessionManager,但由于有关NSURLSession的配置与操作全部封装在AFURLSessionManager中,所以AFHTTPSessionMananger所要做的工作其实并不多,主要是

  1. 根据传入的URL和请求参数创建和修改NSURLResquest对象
  2. 根据用户选择的网络请求方法,调用NSURLSessionManager对应的方法,并对返回的NSURLSessionDataTask进行操作,所以AFHTTPSessionManager主要是用来进行数据任务的,有关上传和下载的任务并不AFHTTPSessionManager中进行。

在AFHTTPSessionManager中仅包含三个属性:

  1. NSString类型的URL
  2. 对请求进行序列化操作的AFHTTPRequestSerializer对象,封装了对NSURLResquest对象进行操作的方法
  3. 对响应进行序列化操作的AFHTTPResponseSerializer对象,封装了对NSURLResponse对象进行操作的方法
@property (readonly, nonatomic, strong, nullable) NSURL *baseURL;
@property (nonatomic, strong) AFHTTPRequestSerializer <AFURLRequestSerialization> * requestSerializer;
@property (nonatomic, strong) AFHTTPResponseSerializer <AFURLResponseSerialization> * responseSerializer;

AFHTTPSessionMananger提供了一些初始化的方法,这些方法都是基于如下方法:

- (instancetype)initWithBaseURL:(NSURL *)url sessionConfiguration:(NSURLSessionConfiguration *)configuration

该方法对AFHTTPSessionMananger进行初始化操作,并传入参数configuration给AFURLSessionManager的初始化方法

AFHTTPSessionMananger提供了常用的网络请求方法,如:GET、POST、HEAD、PUT、PATCH、DELETE方法,用户所能调用方法都基于AFHTTPSessionMananger的以下两个方法:

//进行网络请求操作
- (NSURLSessionDataTask *)dataTaskWithHTTPMethod:(NSString *)method
                                       URLString:(NSString *)URLString
                                      parameters:(id)parameters
                                  uploadProgress:(nullable void (^)(NSProgress *uploadProgress)) uploadProgress
                                downloadProgress:(nullable void (^)(NSProgress *downloadProgress)) downloadProgress
                                         success:(void (^)(NSURLSessionDataTask *, id))success
                                         failure:(void (^)(NSURLSessionDataTask *, NSError *))failure;
                                         
//进行请求体包含block的POST请求
- (NSURLSessionDataTask *)POST:(NSString *)URLString
                    parameters:(id)parameters
     constructingBodyWithBlock:(void (^)(id <AFMultipartFormData> formData))block
                      progress:(nullable void (^)(NSProgress * _Nonnull))uploadProgress
                       success:(void (^)(NSURLSessionDataTask *task, id responseObject))success
                       failure:(void (^)(NSURLSessionDataTask *task, NSError *error))failure;                                                                                                                           

对于上传和下载任务,开发者可以采用同样的方式对请求参数进行封装,或者直接调用AFURLSessionManager中的请求方法。

//上传文件请求
- (NSURLSessionUploadTask *)uploadTaskWithRequest:(NSURLRequest *)request
                                         fromFile:(NSURL *)fileURL
                                         progress:(void (^)(NSProgress *uploadProgress)) uploadProgressBlock
                                completionHandler:(void (^)(NSURLResponse *response, id responseObject, NSError *error))completionHandler;
//上传数据类型                                
- (NSURLSessionUploadTask *)uploadTaskWithRequest:(NSURLRequest *)request
                                         fromData:(NSData *)bodyData
                                         progress:(void (^)(NSProgress *uploadProgress)) uploadProgressBlock
                                completionHandler:(void (^)(NSURLResponse *response, id responseObject, NSError *error))completionHandler;
//上传数据流
- (NSURLSessionUploadTask *)uploadTaskWithStreamedRequest:(NSURLRequest *)request
                                                 progress:(void (^)(NSProgress *uploadProgress)) uploadProgressBlock
                                        completionHandler:(void (^)(NSURLResponse *response, id responseObject, NSError *error))completionHandler;

//下载请求
- (NSURLSessionDownloadTask *)downloadTaskWithRequest:(NSURLRequest *)request
                                             progress:(void (^)(NSProgress *downloadProgress)) downloadProgressBlock
                                          destination:(NSURL * (^)(NSURL *targetPath, NSURLResponse *response))destination
                                    completionHandler:(void (^)(NSURLResponse *response, NSURL *filePath, NSError *error))completionHandler;
//恢复下载请求
- (NSURLSessionDownloadTask *)downloadTaskWithResumeData:(NSData *)resumeData
                                                progress:(void (^)(NSProgress *downloadProgress)) downloadProgressBlock
                                             destination:(NSURL * (^)(NSURL *targetPath, NSURLResponse *response))destination
                                       completionHandler:(void (^)(NSURLResponse *response, NSURL *filePath, NSError *error))completionHandler;

3. NSURLSessionTask的创建

要使用NSURLSession对象进行网络通信,需要用该对象调用请求方法返回一个NSURLSessionTask对象,通过task对象开启网络请求。AFURLSessionManager会在task开启网络请求之前,将task与AFURLSessionManagerTaskDelegate对象通过字典mutableTaskDelegatesKeyedByTaskIdentifier进行映射,以确保每一个task对象都会有一个taskDelegate对象。
AFURLSessionManagerTaskDelegate类的定义在AFURLSessionManager中,它的作用主要是管理网络请求任务的进度,因此也实现了NSURLSessionTaskDelegate、 NSURLSessionDataDelegate、NSURLSessionDownloadDelegate的部分方法。task对象与taskDelegate对象进行映射的过程如下:

  1. 调用NSURLSession对象的方法返回一个task对象
  2. 创建一个taskDelegate对象,将传入的有关进度和出路请求完成的block赋给taskDelegate对象
  3. 以task对象的taskIdentifier标识符为key,将taskDelegate对象添加到字典中

这里有一个问题需要解决,就是如何保证task对象的标识符是唯一的?
在第一节中初始化AFURLSessionManager对象时,创建session对象时的委托是self,也就是AFURLSessionManager对象本身,AFNetWorking(3.0)在进行网络请求时,所有的task对象都由该session对象产生,而由同一个session产生的多个task,它们的标识符都是唯一的;不同的session产生的task,task的标识符则不一定唯一。这样做就能够保证在进行多个网络请求时,对每个网络请求的task进行单独管理,例如多个下载任务在执行,用户可以决定哪个任务暂停,哪个任务继续下载。字典添加映射代码如下,以下载为例:

- (void)addDelegateForDownloadTask:(NSURLSessionDownloadTask *)downloadTask
                          progress:(void (^)(NSProgress *downloadProgress)) downloadProgressBlock
                       destination:(NSURL * (^)(NSURL *targetPath, NSURLResponse *response))destination
                 completionHandler:(void (^)(NSURLResponse *response, NSURL *filePath, NSError *error))completionHandler
{
    //1、创建一个AFURLSessionManagerTaskDelegate对象,将block参数赋给delegate
    AFURLSessionManagerTaskDelegate *delegate = [[AFURLSessionManagerTaskDelegate alloc] init];
    delegate.manager = self;
    delegate.completionHandler = completionHandler;
    //2、返回task对象
    if (destination) {
        delegate.downloadTaskDidFinishDownloading = ^NSURL * (NSURLSession * __unused session, NSURLSessionDownloadTask *task, NSURL *location) {
            return destination(location, task.response);
        };
    }

    downloadTask.taskDescription = self.taskDescriptionForSessionTasks;
    //3、将task对象与delegate进行关联
    [self setDelegate:delegate forTask:downloadTask];

    delegate.downloadProgressBlock = downloadProgressBlock;
}
- (void)setDelegate:(AFURLSessionManagerTaskDelegate *)delegate
            forTask:(NSURLSessionTask *)task
{
    NSParameterAssert(task);
    NSParameterAssert(delegate);

    [self.lock lock];
    //以task的标识符为key,将delegate放入字典
    self.mutableTaskDelegatesKeyedByTaskIdentifier[@(task.taskIdentifier)] = delegate;
    [delegate setupProgressForTask:task];
    [self addNotificationObserverForTask:task];
    [self.lock unlock];
}

这里又出现了另一个问题,既然所有的task都是由同一个session产生,而该session对象的委托确是AFURLSessionManager对象本身,网络请求的执行会调用AFURLSessionManager实现的方法,如何通过AFURLSessionManagerTaskDelegate对象来控制网络请求任务的进行?
前面说过,AFURLSessionManagerTaskDelegate实现了委托的部分方法,这部分方法控制着网络请求的任务进度,而在AFURLSessionManager中也实现了这些方法,当系统调用AFURLSessionManager中的实现方法时,AFURLSessionManager根据task的标识符在字典中找到对应的AFURLSessionManagerTaskDelegate对象,若AFURLSessionManagerTaskDelegate对象中实现了该方法,则调用AFURLSessionManagerTaskDelegate中实现的方法。

4. 网络请求的进度管理

网络请求的进度管理是由AFURLSessionManagerTaskDelegate负责的,AFURLSessionManagerTaskDelegate具有以下属性:

@property (nonatomic, weak) AFURLSessionManager *manager;  //指向AFURLSessionManager对象,能够调用AFURLSessionManager中的block方法
@property (nonatomic, strong) NSMutableData *mutableData;  //存放网络请求接收到的数据
@property (nonatomic, strong) NSProgress *uploadProgress;  //上传任务进度
@property (nonatomic, strong) NSProgress *downloadProgress;  //下载任务进度
@property (nonatomic, copy) AFURLSessionDownloadTaskDidFinishDownloadingBlock downloadTaskDidFinishDownloading; //下载完成回调
@property (nonatomic, copy) AFURLSessionTaskProgressBlock uploadProgressBlock; //上传任务进度回调
@property (nonatomic, copy) AFURLSessionTaskProgressBlock downloadProgressBlock; //下载任务进度回调
@property (nonatomic, copy) AFURLSessionTaskCompletionHandler completionHandler; //处理回调 

当网络进行网络通信时,AFURLSessionManagerTaskDelegate中实现的委托方法会返回进度信息并记录到uploadProgress和downloadProgress中,同时接收到的数据也会存放到mutableData中。为了能够响应进度信息的变化,AFURLSessionManagerTaskDelegate则通过KVO的对进度信息监听,一旦进度信息发生改变,则可以调用回调进行处理,例如可以在回调中刷新主界面。这些回调函数都是在作为网络请求的参数传入到AFURLSessionManager中,并由AFURLSessionManager创建任务时赋给AFURLSessionManagerTaskDelegate对象的。

//设置uploadProgress与downloadProgress,并添加观察者
- (void)setupProgressForTask:(NSURLSessionTask *)task {
    //#1、对downloadProgress和uploadProgress进行设置,代码略
    //#2、添加对downloadProgress和uploadProgress的KVO观察者  
    [self.downloadProgress addObserver:self
                            forKeyPath:NSStringFromSelector(@selector(fractionCompleted))
                               options:NSKeyValueObservingOptionNew
                               context:NULL];
    [self.uploadProgress addObserver:self
                          forKeyPath:NSStringFromSelector(@selector(fractionCompleted))
                             options:NSKeyValueObservingOptionNew
                             context:NULL];
}

//移除观察者
- (void)cleanUpProgressForTask:(NSURLSessionTask *)task {
    [self.downloadProgress removeObserver:self forKeyPath:NSStringFromSelector(@selector(fractionCompleted))];
    [self.uploadProgress removeObserver:self forKeyPath:NSStringFromSelector(@selector(fractionCompleted))];
}

//添加默认的监听方法
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSString *,id> *)change context:(void *)context {
   //调用downloadProgress的回调
   if ([object isEqual:self.downloadProgress]) {
        if (self.downloadProgressBlock) {
            self.downloadProgressBlock(object);
        }
    }
    //调用uploadProgress的回调
    else if ([object isEqual:self.uploadProgress]) {
        if (self.uploadProgressBlock) {
            self.uploadProgressBlock(object);
        }
    }
}

当NSURLSessionTask结束时,总会调用委托方法URLSession:task:didCompleteWithError: ,该方法告诉委托当前的网络数据通信已完成。在AFNetWorking(3.0)中,该方法的主要工作如下:

  1. 从manager中获取本次请求的response序列化信息,放入userInfo字典
  2. 将网络通信接收的数据放入userInfo字典
  3. 若网络通信错误,则将error放入userInfo字典,同时使用处理回调进行后续处理,并发送通知
  4. 若网络通信正常,则将response序列化信息进行解析,放入userInfo字典中,同时调用处理回调进行后续处理,并发送通知
- (void)URLSession:(__unused NSURLSession *)session
              task:(NSURLSessionTask *)task
didCompleteWithError:(NSError *)error
{
    __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];
                });
            });
        });
    }
}

以上就是我阅读AFNetWorking(3.0)的源码后做的关于NSURLSession的使用的总结。AFNetWorking(3.0)作为一个成熟、稳定的网络请求框架,所涉及的知识也不局限于NSURLSession的使用,还有许多内容,如网络状态的监测、网络请求的安全性处理,这些都非常值得我们花时间去学习。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,440评论 5 467
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,814评论 2 376
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,427评论 0 330
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,710评论 1 270
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,625评论 5 359
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,014评论 1 275
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,511评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,162评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,311评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,262评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,278评论 1 328
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,989评论 3 316
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,583评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,664评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,904评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,274评论 2 345
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,856评论 2 339

推荐阅读更多精彩内容