AFNetworking框架分析网络

AFNetworking框架分析(五)——响应的序列化AFURL

2018-06-22  本文已影响173人  我只不过是出来写写代码

这一篇将分析网络请求收到数据时的响应AFURLResponseSerialization序列化过程。
当AFURLRequestSerialization类将所有的请求数据处理完成发送请求之后,当收到返回的数据信息时,这时就要靠AFURLResponseSerialization类来完成不同类型返回数据的序列化操作。
从AFURLResponseSerialization头文件中,可以看出与AFURLRequestSerialization类的结构非常相似。从上往下,首先声明了AFURLResponseSerialization协议,协议中只有一个方法,将response解码成指定的相关数据,这是所有响应类都需要遵循的协议。之后声明了一个AFHTTPResponseSerializer类,作为响应类的根类。再往下的类,都是继承自AFHTTPResponseSerializer的子类,分别是AFJSONResponseSerializer(JSON格式数据响应,默认)、AFXMLParserResponseSerializer(iOS端XML数据解析响应)、AFXMLDocumentResponseSerializer(MAC OS端XML数据解析响应)、AFPropertyListResponseSerializer(PList格式数据解析响应)、AFImageResponseSerializer(图片数据解析响应)和AFCompoundResponseSerializer(复合式数据解析响应)
在父类AFHTTPResponseSerializer中,遵循的协议方法不做任何事情 只做一次response的验证。实现方法中,只有[self validateResponse:(NSHTTPURLResponse *)response data:data error:error]验证response是否合规的方法。而且初始化init方法中,父类只是设置编码格式为UTF-8,设置http状态码为200-299,表示只有这些状态码获得了有效的响应,而不在接受范围内的状态码和内容类型会在数据解析时发生错误。而且其中一句代码self.acceptableContentTypes = nil;,本身acceptableContentTypes用于设置可接受的contentType,这里置为nil,也从侧面建议不要直接使用父类。

父类AFHTTPResponseSerializer初始化方法
所以,当需要响应具体不同类型的数据序列化操作时,都是由其对应的子类来完成任务。
- (BOOL)validateResponse:(NSHTTPURLResponse *)response
                    data:(NSData *)data
                   error:(NSError * __autoreleasing *)error
{
    BOOL responseIsValid = YES;
    NSError *validationError = nil;

    if (response && [response isKindOfClass:[NSHTTPURLResponse class]]) {
        if (self.acceptableContentTypes && ![self.acceptableContentTypes containsObject:[response MIMEType]] &&
            !([response MIMEType] == nil && [data length] == 0)) {

            if ([data length] > 0 && [response URL]) {
                NSMutableDictionary *mutableUserInfo = [@{
                                                          NSLocalizedDescriptionKey: [NSString stringWithFormat:NSLocalizedStringFromTable(@"Request failed: unacceptable content-type: %@", @"AFNetworking", nil), [response MIMEType]],
                                                          NSURLErrorFailingURLErrorKey:[response URL],
                                                          AFNetworkingOperationFailingURLResponseErrorKey: response,
                                                        } mutableCopy];
                if (data) {
                    mutableUserInfo[AFNetworkingOperationFailingURLResponseDataErrorKey] = data;
                }

                validationError = AFErrorWithUnderlyingError([NSError errorWithDomain:AFURLResponseSerializationErrorDomain code:NSURLErrorCannotDecodeContentData userInfo:mutableUserInfo], validationError);
            }

            responseIsValid = NO;
        }

        if (self.acceptableStatusCodes && ![self.acceptableStatusCodes containsIndex:(NSUInteger)response.statusCode] && [response URL]) {
            NSMutableDictionary *mutableUserInfo = [@{
                                               NSLocalizedDescriptionKey: [NSString stringWithFormat:NSLocalizedStringFromTable(@"Request failed: %@ (%ld)", @"AFNetworking", nil), [NSHTTPURLResponse localizedStringForStatusCode:response.statusCode], (long)response.statusCode],
                                               NSURLErrorFailingURLErrorKey:[response URL],
                                               AFNetworkingOperationFailingURLResponseErrorKey: response,
                                       } mutableCopy];

            if (data) {
                mutableUserInfo[AFNetworkingOperationFailingURLResponseDataErrorKey] = data;
            }

            validationError = AFErrorWithUnderlyingError([NSError errorWithDomain:AFURLResponseSerializationErrorDomain code:NSURLErrorBadServerResponse userInfo:mutableUserInfo], validationError);

            responseIsValid = NO;
        }
    }

    if (error && !responseIsValid) {
        *error = validationError;
    }

    return responseIsValid;
}

该验证方法中,默认设置返回BOOL值为YES,后续只处理数据无效的各种情况。首先,根据初始化的acceptableContentTypes 判断MIME媒体类型是否合法;其次,根据初始化的acceptableStatusCodes 判断状态码是否有效。
代码实现中,NSLocalizedDescriptionKey是NSError头文件中预定义的键,标识错误的本地化描述.可以通过NSError的localizedDescription方法获得对应的值信息,而NSURLErrorFailingURLErrorKey相应的值是包含导致加载失败的URL的NSURL。生成错误信息字典,会返回unacceptable content-type的信息,并将错误信息记录在了mutableUserInfo中。
因此,如果content-type不满足,那么产生的validationError就是Domain为AFURLResponseSerializationErrorDomain,code为NSURLErrorCannotDecodeContentData;如果MIME type不满足,那么产生的validationError就是Domain为AFURLResponseSerializationErrorDomain,code为NSURLErrorBadServerResponse。
这里需要注意一个error处理逻辑,不管是判断媒体类型还是状态码,都用到了validationError = AFErrorWithUnderlyingError([NSError errorWithDomain:AFURLResponseSerializationErrorDomain code:NSURLErrorBadServerResponse userInfo:mutableUserInfo], validationError);函数。查看此函数的实现,可以发现AFN已经处理好了当两种错误同时出现的情况以及优先级显示。将媒体类型的error信息放入至状态码error中userInfo字典的NSUnderlyingErrorKey值中去。

// 设置一个underlyingError为error的附属
static NSError * AFErrorWithUnderlyingError(NSError *error, NSError *underlyingError) {
   // 是否传入了error
    if (!error) {
        return underlyingError;
    }
    // 是否已经有附属
    if (!underlyingError || error.userInfo[NSUnderlyingErrorKey]) {
        return error;
    }
    // 取出error的userInfo
    NSMutableDictionary *mutableUserInfo = [error.userInfo mutableCopy];
    mutableUserInfo[NSUnderlyingErrorKey] = underlyingError;

    return [[NSError alloc] initWithDomain:error.domain code:error.code userInfo:mutableUserInfo];
}

以JSON格式的数据为例,当响应到JSON格式的数据时,就需要AFJSONResponseSerializer子类去完成response序列化工作。
首先在初始化方法init中,设置了acceptableContentTypes的集合内容self.acceptableContentTypes = [NSSet setWithObjects:@"application/json", @"text/json", @"text/javascript", nil];(小提示:json格式是 js 代码的一个子集。也就是说 json 格式的数据,也是 js 代码,也会被浏览器的js引擎执行,从而生成 json 对象)
接下来,AFJSONResponseSerializer类遵循的协议方法会对JSON格式的数据进行删除空数据处理,利用遍历与递归将value值为空的key进行删除操作。类似的,AFXMLParserResponseSerializer、AFXMLDocumentResponseSerializer、AFPropertyListResponseSerializer都是将返回数据进行对应格式类型的数据转换,并删除其中无效的key,最终返回出response。AFCompoundResponseSerializer类型的,会进行所有支持数据类型的遍历,以匹配哪种类型的数据可以进行数据解析。

这里单独拿出AFImageResponseSerializer类进行分析,此类用于接收处理图片类型的数据。在解析图片数据时,用到了函数static UIImage * AFInflatedImageFromResponseWithDataAtScale(NSHTTPURLResponse *response, NSData *data, CGFloat scale)来手动解压图片。根据response和scale(scale大小为屏幕宽高)转换为位图bitmap,这里因为解码的过程都是在网络请求回来调用的,避免了系统将在主线程进行解码,从而显示图片的时候直接绘制,节省GPU开销。此函数中主要涉及到了CoreGraphics内容。
对CoreGraphics有兴趣了解的,可以看下阿里云对其介绍链接在此
首先将图片data封装至CGDataProviderRef对象中,然后只针对jpg与png格式的图片数据来单独给CGImageRef对象赋值,以此来创建CGImage用于表示data中的图片是压缩格式。在jpg格式中,AFN单独判断了CMKY类型的图片不支持转换为位图

            // CGImageCreateWithJPEGDataProvider does not properly handle CMKY, so fall back to AFImageWithDataAtScale
            if (imageColorSpaceModel == kCGColorSpaceModelCMYK) {
                CGImageRelease(imageRef);
                imageRef = NULL;
            }

接下来,根据图片data数据创建一个UIImage对象,然后根据上面的CGImageRef对象判断是否为压缩格式图片。若非压缩格式图片且不为空,则直接把原图片返回出去,为空时直接返回nil。接下来就到了处理压缩格式的图片流程

    size_t width = CGImageGetWidth(imageRef);
    size_t height = CGImageGetHeight(imageRef);
    size_t bitsPerComponent = CGImageGetBitsPerComponent(imageRef);

    if (width * height > 1024 * 1024 || bitsPerComponent > 8) {
        CGImageRelease(imageRef);

        return image;
    }

    // CGImageGetBytesPerRow() calculates incorrectly in iOS 5.0, so defer to CGBitmapContextCreate
    size_t bytesPerRow = 0;
    CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
    CGColorSpaceModel colorSpaceModel = CGColorSpaceGetModel(colorSpace);
    CGBitmapInfo bitmapInfo = CGImageGetBitmapInfo(imageRef);

    if (colorSpaceModel == kCGColorSpaceModelRGB) {
        uint32_t alpha = (bitmapInfo & kCGBitmapAlphaInfoMask);
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wassign-enum"
        if (alpha == kCGImageAlphaNone) {
            bitmapInfo &= ~kCGBitmapAlphaInfoMask;
            bitmapInfo |= kCGImageAlphaNoneSkipFirst;
        } else if (!(alpha == kCGImageAlphaNoneSkipFirst || alpha == kCGImageAlphaNoneSkipLast)) {
            bitmapInfo &= ~kCGBitmapAlphaInfoMask;
            bitmapInfo |= kCGImageAlphaPremultipliedFirst;
        }
#pragma clang diagnostic pop
    }

    CGContextRef context = CGBitmapContextCreate(NULL, width, height, bitsPerComponent, bytesPerRow, colorSpace, bitmapInfo);

    CGColorSpaceRelease(colorSpace);

    if (!context) {
        CGImageRelease(imageRef);

        return image;
    }

    CGContextDrawImage(context, CGRectMake(0.0f, 0.0f, width, height), imageRef);
    CGImageRef inflatedImageRef = CGBitmapContextCreateImage(context);

    CGContextRelease(context);

    UIImage *inflatedImage = [[UIImage alloc] initWithCGImage:inflatedImageRef scale:scale orientation:image.imageOrientation];

    CGImageRelease(inflatedImageRef);
    CGImageRelease(imageRef);

    return inflatedImage;

按照个人理解,首先是通过CoreGraphics框架来获取图片CGImageRef对象的宽高、以及每个颜色的比特数。当宽高像素大于1024*1024像素,或者每个颜色的比特数大于8时,表明图片过大直接返回原图出去。接下来,获取到图片的各种信息,来创建一个CGContextRef类型对象context,也就是bitmap的上下文。继续把context渲染到画布上,根据context生成一个bitmap格式的图片。然后将图片转换成UIImage格式的图片作为response数据返回给AFURLSessionManager类。最终通过block返回出图片数据。
小插曲:Apple官方更推荐使用png格式的压缩图片进行网络传输返回至手机端。pngcrush工具,了解一下,可以更快速地解压与渲染图片,节省系统资源。

上一篇 下一篇

猜你喜欢

热点阅读