AFNetworking框架分析(四)——请求的序列化AFURLRequestSerialization分析

时间:2022-06-19
本文章向大家介绍AFNetworking框架分析(四)——请求的序列化AFURLRequestSerialization分析,主要内容包括其使用实例、应用技巧、基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。

之前用了两篇篇幅分析了下AFN的核心类AFURLSessionManager在网络请求之前、请求中、以及请求结束时,做了哪些工作。接下来,将用两篇文章的篇幅来分析一下AFN中网络请求AFURLRequestSerialization与响应AFURLResponseSerialization的序列化。

首先是AFURLRequestSerialization请求的序列化。 查看AFURLRequestSerialization的头文件中,实现了AFURLRequestSerialization协议,协议中只有一个方法- (nullable NSURLRequest *)requestBySerializingRequest:(NSURLRequest *)request withParameters:(nullable id)parameters error:(NSError * _Nullable __autoreleasing *)error NS_SWIFT_NOTHROW;,用于返回一个原始request的copy对象,将参数根据指定的编码格式进行处理。 AFHTTPRequestSerializer作为请求序列化的一个根类,作为AFN的默认设置。AFJSONRequestSerializerAFPropertyListRequestSerializer作为子类都继承自AFHTTPRequestSerializer。 头文件中还存在AFMultipartFormData协议,主要用于多部分表单的处理,之后将以表单形式POST请求为例,来分析其中的工作流程。 AFURLRequestSerialization协议,继承自<NSObject, NSSecureCoding, NSCopying>三个协议。其中NSSecureCoding协议,主要用于在解码时要同时指定key和要解码的对象的类,如果要求的类和从文件中解码出的对象的类不匹配,NSCoder则会抛出异常并通知数据已经被篡改。NSCopying协议是为了能够让当前类支持拷贝功能。 以POST请求为例,提交的数据都是放到请求体body中,但并未规定编码方式,那么就需要设置Content-Type告知后台服务数据的格式。

数据格式

简单基本的网络请求过程,之前已经介绍过。在实际开发中避免不了与后台大文件传输,那么就要将上传或下载的大文件以数据流的形式进行传输。此时就需要用到AFN框架中的- (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方法,与AFN中基本的POST方法相比,多声明了一处参数constructingBodyWithBlock:(void (^)(id <AFMultipartFormData> formData))block。看看它又多做了什么处理。

实现方法

可以发现,通过声明一个AFMultipartFormData类型的formData来构建用于multipartForm请求体request。

- (NSMutableURLRequest *)multipartFormRequestWithMethod:(NSString *)method
                                              URLString:(NSString *)URLString
                                             parameters:(NSDictionary *)parameters
                              constructingBodyWithBlock:(void (^)(id <AFMultipartFormData> formData))block
                                                  error:(NSError *__autoreleasing *)error
{
    NSParameterAssert(method);
    NSParameterAssert(![method isEqualToString:@"GET"] && ![method isEqualToString:@"HEAD"]);

    NSMutableURLRequest *mutableRequest = [self requestWithMethod:method URLString:URLString parameters:nil error:error];

    __block AFStreamingMultipartFormData *formData = [[AFStreamingMultipartFormData alloc] initWithURLRequest:mutableRequest stringEncoding:NSUTF8StringEncoding];

    if (parameters) {
        for (AFQueryStringPair *pair in AFQueryStringPairsFromDictionary(parameters)) {
            NSData *data = nil;
            if ([pair.value isKindOfClass:[NSData class]]) {
                data = pair.value;
            } else if ([pair.value isEqual:[NSNull null]]) {
                data = [NSData data];
            } else {
                data = [[pair.value description] dataUsingEncoding:self.stringEncoding];
            }

            if (data) {
                [formData appendPartWithFormData:data name:[pair.field description]];
            }
        }
    }

    if (block) {
        block(formData);
    }

    return [formData requestByFinalizingMultipartFormData];
}

实现方法中,首先用断言判断了GET与HEAD类型的请求不能继续执行后续代码。在创建了mutableRequest之后,为了构建bodyStream,初始化了一个AFStreamingMultipartFormData类型的对象,并用__block修饰。在其init方法中,分别声明了实例变量请求request、字符串编码格式stringEncoding、分隔符boundary以及数据流bodyStream。 这里扩展一下,AFMultipartBodyStream类中声明了NSInputStream类型的对象。而NSInputStream是文件的读取流,是将本地的文件读取到内存中去 ,与之对应的就是NSOutputStream,文件的写入流,将内存中的文件数据写入到文件中。继续深入的话,网络请求都是基于coreFoundation的cfnetwork,而文件的读、写流,也分别对应着coreFoundation中的CFWriteStreamRef与CFWriteStreamRef相关的C函数。可以查看CoreFoundation框架中的CFStream头文件

CFStream头文件C函数方法

AFN中定义的分隔符方法,使用两个十六进制随机数拼接在Boundary后面来表示分隔符

static NSString * AFCreateMultipartFormBoundary() {
    return [NSString stringWithFormat:@"Boundary+%08X%08X", arc4random(), arc4random()];
}

创建完成AFStreamingMultipartFormData对象后,接下来的操作与基本POST请求相同,遍历parameters参数字典,将其转换成NSData并拼接进之前的AFStreamingMultipartFormData对象中。 而构造bodyStream最主要的实现,就在[formData appendPartWithFormData:data name:[pair.field description]]这行代码中,根据data和name来构建request的header与body。 在方法实现中,拼接成符合表单传输的格式,并添加至实例变量bodyStream中,也就是对应的body数据。

表单格式的数据结构示例图

接下来的,执行block(formData)代码块,就可以在代码实现的block中将图片添加至formData。 constructingBodyWithBlock:(void (^)(id <AFMultipartFormData> formData))block添加本地图片实现方法:

NSString *imgPath = [[NSBundle mainBundle] pathForResource:@"upload" ofType:@"png"];
[formData appendPartWithFileURL:[NSURL fileURLWithPath:imgPath] name:@"(后台指定的key名)" error:nil];

添加图片至body数据流中的实现方法,首先利用文件扩展名和C函数获取UTI统一类型标志符,再根据UTI获取contentType。

static inline NSString * AFContentTypeForPathExtension(NSString *extension) {
    NSString *UTI = (__bridge_transfer NSString *)UTTypeCreatePreferredIdentifierForTag(kUTTagClassFilenameExtension, (__bridge CFStringRef)extension, NULL);
    NSString *contentType = (__bridge_transfer NSString *)UTTypeCopyPreferredTagWithClass((__bridge CFStringRef)UTI, kUTTagClassMIMEType);
    if (!contentType) {
        return @"application/octet-stream";
    } else {
        return contentType;
    }

接着,检查fileURL是否合法以及文件是否存在。若文件存在,创建一个AFHTTPBodyPart对象,拼接成符合表单数据结构的字典并放入该对象的header中,完成后将AFHTTPBodyPart对象添加至body数据对象bodyStream。

表单中添加图片文件后的数据结构

走到这一步,表单中的参数拼接已经完成。但一个完整的表单格式的请求参数,还缺少基本的信息,而保证这些信息的完整性,最后由[formData requestByFinalizingMultipartFormData]方法实现,在表单的首尾添加分隔符、设置request的bodyStream为self.bodyStream(而非setBody方法)、设置Content-Type、设置Content-Length这四步操作。

完整的表单数据信息

针对表单形式的POST请求,request的初始化已经完成。之后task任务创建与处理,与普通的POST请求无异。 AFN框架在表单形式的POST请求中,帮我们做了添加分隔符、并将所有的传参data拼接在一起,作为一个完整的请求数据流发送给服务器等一系列工作。

这一篇通过举例较为复杂而且经典的表单形式POST请求,可以总结出AFURLRequestSerialization类的作用。 1.使用KVO以及KVC来动态监听并修改request属性 2.设置request的请求header 3.生成请求参数查询字符串 4.支持表单结构数据以数据流拼接分片上传