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

这一篇将分析网络请求收到数据时的响应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工具,了解一下,可以更快速地解压与渲染图片,节省系统资源。


该文章首次发表在 简书:我只不过是出来写写代码 博客,并自动同步至 腾讯云:我只不过是出来写写iOS 博客

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

推荐阅读更多精彩内容