AFN 3.0学习总结(四)

参考:AFNetworking 3.0 源码解读(四)之 AFURLResponseSerialization

说明:很多内容都是摘抄原文,只是根据自己的需要进行摘抄或者总结,如有不妥请及时指出,谢谢。

这次主要讲AFURLResponseSerialization(HTTP响应)这一个类的知识。

一、AFURLResponseSerialization

1

这是一个协议,只要遵循这个协议,就必须实现NSSecureCoding、NSCoping这两个协议,同时还要实现上图中的第二个方法。

第二个方法返回序列化后的结果。不管是AFHttpResponseSerialize,还是他的子类,都遵循这个协议,也就是在各自的实现中实现了这个协议,然后返回了属于自己的结果。

ps:根据这个协议,我有了一些启发。当我们在设计一个网络框架的时候,因为业务不同,返回的数据也有很多种,通常的一种做法是直接返回服务器响应的数据,由业务人员自己实现业务。但是如果业务繁杂,这样写出的代码也会很乱,我们不妨采用类似这种协议的设计模式,这样做有两个好处:

1. 业务人员和数据人员可以分开。 数据提前约定好名称和内容,写数据人员实现数据部分,写业务人员实现业务部分。

2. 左右的数据转换放到协议实现方法中,出现问题,更容易查找问题。

由于这个类有很多的子类,我们先来看看这些类的组成,然后逐一对每个子类的代码进行解读

=============================  分割线==============================

我们还是先来看看AFHTTPResponseSerializer的头文件组成部分

来看看实现部分

这里对两个属性进行的初始化操作,下面说一下NSIndexSet这个集合

NSIndexSet定义:是一个有序的,唯一的,无符号整数的集合。

先看个列子:

打印结果如下:

这充分说明了NSIndexSet的以下几点特征:

是有序的、唯一的、无符号整数合集。

因此下面定义的状态码为200~299

self.acceptableStatusCodes = [NSIndexSet indexSetWithIndexesInRange:NSMakeRange(200, 100)];

- (BOOL)validateResponse:(NSHTTPURLResponse *)response

                    data:(NSData *)data

                  error:(NSError * __autoreleasing *)error

{

    //1、默认responseIsValid = YES

    BOOL responseIsValid = YES;

    NSError *validationError = nil;

    //2、假如response存在,且类型是NSHTTPURLResponse

    if (response && [response isKindOfClass:[NSHTTPURLResponse class]]) {

        //2.1 self.acceptableContentTypes存在,但是不包含服务器返回的MIMEType,并且MIMEType和data都不能为nil

        if (self.acceptableContentTypes && ![self.acceptableContentTypes containsObject:[response MIMEType]] &&

            !([response MIMEType] == nil && [data length] == 0)) {

            if ([data length] > 0 && [response URL]) {

                //2.1.1生成错误信息

                NSMutableDictionary *mutableUserInfo = [@{

                                                          NSLocalizedDescriptionKey: [NSString stringWithFormat:NSLocalizedStringFromTable(@"Request failed: unacceptable content-type: %@", @"AFNetworking", nil), [response MIMEType]],

                                                          NSURLErrorFailingURLErrorKey:[response URL],

                                                          AFNetworkingOperationFailingURLResponseErrorKey: response,

                                                        } mutableCopy];

                //2.1.2 包含data的错误信息

                if (data) {

                    mutableUserInfo[AFNetworkingOperationFailingURLResponseDataErrorKey] = data;

                }

                //2.1.3生成NSError

                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;

            }

            //设置错误,通过AFErrorWithUnderlyingError设置validationError的NSUnderlyingErrorKey

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

            responseIsValid = NO;

        }

    }

    //赋值

    if (error && !responseIsValid) {

        *error = validationError;

    }

    return responseIsValid;

}

这个方法是检测响应的有效性的,这里主要对NSError进行以下介绍。

NSError有个属性NSDictionary *userInfo,错误信息一般都可以从这个字典中获得,因为是一个字典,所以我们会用到很多的key,我们看看系统自带的key和含义都有哪些

当然我们也可以自定义key来操作NSError。

在上面的那个方法中,有可能会出现两个错误,self.acceptableContentTypes和self.acceptableStatusCodes

这两个错误如果都出现了,怎么办?这里就用到了NSUnderlyingErrorKey这个字段,它表示一个优先的错误,value为NSError对象。通过下面这两个函数进行赋值和转换。

二、AFJSONResponseSerializer

json的读取项目,默认为0,看看具体的定义

1、NSJSONReadingMutableContainers json解析成功后返回一个容器

2、NSJSONReadingMutableLeaves 返回的json对象中的字符串为NSMutableString

3、NSJSONReadingAllowFragments 允许json字符串最外层既不是NSArray也不是NSDictonrary,但必须是有效的Json Fragment。例如使用这个选项可以解析@“123”这样的字符串

对NSJSONReadingMutableContainers进行举例说明

从初始化这里可以看出,AFN默认支持的Content-Type类型有@"application/json", @"text/json", @"text/javascript"

- (id)responseObjectForResponse:(NSURLResponse *)response

                          data:(NSData *)data

                          error:(NSError *__autoreleasing *)error

{

    //判断处理,如果验证结果失败,在没有error或者 错误中code:NSURLErrorCannotDecodeContentData的情况下,是不能解析数据的,直接返回nil

    if (![self validateResponse:(NSHTTPURLResponse *)response data:data error:error]) {

        if (!error || AFErrorOrUnderlyingErrorHasCodeInDomain(*error, NSURLErrorCannotDecodeContentData, AFURLResponseSerializationErrorDomain)) {

            return nil;

        }

    }

    // Workaround for behavior of Rails to return a single space for `head :ok` (a workaround for a bug in Safari), which is not interpreted as valid input by NSJSONSerialization.

    // See https://github.com/rails/rails/issues/1742

    BOOL isSpace = [data isEqualToData:[NSData dataWithBytes:" " length:1]];


    if (data.length == 0 || isSpace) {

        return nil;

    }


    NSError *serializationError = nil;


    id responseObject = [NSJSONSerialization JSONObjectWithData:data options:self.readingOptions error:&serializationError];

    if (!responseObject)

    {

        if (error) {

            *error = AFErrorWithUnderlyingError(serializationError, *error);

        }

        return nil;

    }


    if (self.removesKeysWithNullValues) {

        return AFJSONObjectByRemovingKeysWithNullValues(responseObject, self.readingOptions);

    }

    return responseObject;

}

上面是json转换的核心方法,很好理解,里面只出现了两个函数,具体如下://检测错误或者优先错误中是否匹配code以及domain

static BOOL AFErrorOrUnderlyingErrorHasCodeInDomain(NSError *error, NSInteger code, NSString *domain) {

    if ([error.domain isEqualToString:domain] && error.code == code) {

        return YES;

    } else if (error.userInfo[NSUnderlyingErrorKey]) {

        return AFErrorOrUnderlyingErrorHasCodeInDomain(error.userInfo[NSUnderlyingErrorKey], code, domain);

    }

    return NO;

}

static id AFJSONObjectByRemovingKeysWithNullValues(id JSONObject, NSJSONReadingOptions readingOptions) {

//1、数组

if ([JSONObject isKindOfClass:[NSArray class]]) {

//创建了一个NSMutableArray,为了提高性能,使用了Capacity创建

NSMutableArray *mutableArray = [NSMutableArray arrayWithCapacity:[(NSArray *)JSONObject count]];

//遍历数组,通过递归方式删除对象中的NSNull

for (id value in (NSArray *)JSONObject) {

[mutableArray addObject:AFJSONObjectByRemovingKeysWithNullValues(value, readingOptions)]; }

//如果readingOptions==NSJSONReadingMutableContainers,返回可变的数组,否则是不可变的 return (readingOptions & NSJSONReadingMutableContainers) ? mutableArray : [NSArray arrayWithArray:mutableArray];

}

else if ([JSONObject isKindOfClass:[NSDictionary class]]) {

//意思同上 NSMutableDictionary *mutableDictionary = [NSMutableDictionary dictionaryWithDictionary:JSONObject];

for (id key in [(NSDictionary *)JSONObject allKeys]) {

            id value = (NSDictionary *)JSONObject[key];

            if (!value || [value isEqual:[NSNull null]]) {

                [mutableDictionary removeObjectForKey:key];

            } else if ([value isKindOfClass:[NSArray class]] || [value isKindOfClass:[NSDictionary class]]) {

                mutableDictionary[key] = AFJSONObjectByRemovingKeysWithNullValues(value, readingOptions);

            }

        }

        return (readingOptions & NSJSONReadingMutableContainers) ? mutableDictionary : [NSDictionary dictionaryWithDictionary:mutableDictionary];

    }

    return JSONObject;

}

三、AFXMLParserResponseSerializer

AFN默认支持的xml类型为@"application/xml", @"text/xml"

四、AFXMLDocumentResponseSerializer

这个类只在mac os x上使用,不过多介绍

五、AFPropertyListResponseSerializer

这个类把json转换成PropertyList,支持的Content-Type类型为application/x-plist

六、UIImage (AFNetworkingSafeImageLoading)

这个是UIImage的分类

这里对image.images这个属性进行简单说明

UIImage *image0 = [UIImage imageNamed:@"SenderVoiceNodePlaying001"]; UIImage *image1 = [UIImage imageNamed:@"SenderVoiceNodePlaying002"]; UIImage *image2 = [UIImage imageNamed:@"SenderVoiceNodePlaying003"]; self.imageView.image = [UIImage animatedImageWithImages:@[image0,image1,image2] duration:1.5];

效果如下:

可以看到,类似一个GIF图片的效果。

//根据响应结果,和scale返回一张图片

static UIImage * AFInflatedImageFromResponseWithDataAtScale(NSHTTPURLResponse *response, NSData *data, CGFloat scale) {

    if (!data || [data length] == 0) {

        return nil;

    }

    //只要是CG开头的,都是CoreGraphics

    CGImageRef imageRef = NULL;


    //CGDataProviderRef可以理解为是对data的包装,用完要进行释放

    CGDataProviderRef dataProvider = CGDataProviderCreateWithCFData((__bridge CFDataRef)data);

    //根据返回的类型,初始化imageRef

    if ([response.MIMEType isEqualToString:@"image/png"]) {

        imageRef = CGImageCreateWithPNGDataProvider(dataProvider,  NULL, true, kCGRenderingIntentDefault);

    } else if ([response.MIMEType isEqualToString:@"image/jpeg"]) {

        imageRef = CGImageCreateWithJPEGDataProvider(dataProvider, NULL, true, kCGRenderingIntentDefault);

        if (imageRef) {

            CGColorSpaceRef imageColorSpace = CGImageGetColorSpace(imageRef);

            CGColorSpaceModel imageColorSpaceModel = CGColorSpaceGetModel(imageColorSpace);

            // CGImageCreateWithJPEGDataProvider does not properly handle CMKY, so fall back to AFImageWithDataAtScale

            if (imageColorSpaceModel == kCGColorSpaceModelCMYK) {

                CGImageRelease(imageRef);

                imageRef = NULL;

            }

        }

    }

    //释放

    CGDataProviderRelease(dataProvider);

    UIImage *image = AFImageWithDataAtScale(data, scale);

    if (!imageRef) {

        if (image.images || !image) {

            return image;

        }

        imageRef = CGImageCreateCopy([image CGImage]);

        if (!imageRef) {

            return nil;

        }

    }

    //代码走到这里imageRef肯定是有值的,要么是根据response.MIMEType得到的,要么是根据image得到的

    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;

}

上面做这个转化的目的是为了尽量避免在主线程解压数据,因图像太大造成内存崩溃的问题。

七、AFImageResponseSerializer

八、AFCompoundResponseSerializer

这个符合类型有一个数组属性,内容是多种序列化的类型。由实现方法中也可以看出,遍历序列化类型,只要能转化成功,就返回数据。

九、总结

1、通过一个协议来得到不同的转换结果

2、知道AFN响应结果支持的各种类型

3、接触到了NSIndexSet的用法

4、如何创建一个NSError和一个带有优先错误的NSUnderlyingErrorKey

5、服务器返回的图片是压缩格式,要进行压缩

6、使用images来实现gif效果

其它的文章的总结

AFJSONResponseSerializer使用系统内置的NSJSONSerialization解析json,NSJSON只支持解析UTF8编码的数据(还有UTF-16LE之类的,都不常用),所以要先把返回的数据转成UTF8格式。这里会尝试用HTTP返回的编码类型和自己设置的stringEncoding去把数据解码转成字符串NSString,再把NSString用UTF8编码转成NSData,再用NSJSONSerialization解析成对象返回。

上述过程是NSData->NSString->NSData->NSObject,这里有个问题,如果你能确定服务端返回的是UTF8编码的json数据,那NSData->NSString->NSData这两步就是无意义的,而且这两步进行了两次编解码,很浪费性能,所以如果确定服务端返回utf8编码数据,就建议自己再写个JSONResponseSerializer,跳过这两个步骤。

此外AFJSONResponseSerializer专门写了个方法去除NSNull,直接把对象里值是NSNull的键去掉,还蛮贴心,若不去掉,上层很容易忽略了这个数据类型,判断了数据是否nil没判断是否NSNull,进行了错误的调用导致core。

图片解压

当我们调用UIImage的方法imageWithData:方法把数据转成UIImage对象后,其实这时UIImage对象还没准备好需要渲染到屏幕的数据,现在的网络图像PNG和JPG都是压缩格式,需要把它们解压转成bitmap后才能渲染到屏幕上,如果不做任何处理,当你把UIImage赋给UIImageView,在渲染之前底层会判断到UIImage对象未解压,没有bitmap数据,这时会在主线程对图片进行解压操作,再渲染到屏幕上。这个解压操作是比较耗时的,如果任由它在主线程做,可能会导致速度慢UI卡顿的问题。

AFImageResponseSerializer除了把返回数据解析成UIImage外,还会把图像数据解压,这个处理是在子线程(AFNetworking专用的一条线程,详见AFURLConnectionOperation),处理后上层使用返回的UIImage在主线程渲染时就不需要做解压这步操作,主线程减轻了负担,减少了UI卡顿问题。

具体实现上在AFInflatedImageFromResponseWithDataAtScale里,创建一个画布,把UIImage画在画布上,再把这个画布保存成UIImage返回给上层。只有JPG和PNG才会尝试去做解压操作,期间如果解压失败,或者遇到CMKY颜色格式的jpg,或者图像太大(解压后的bitmap太占内存,一个像素3-4字节,搞不好内存就爆掉了),就直接返回未解压的图像。

另外在代码里看到iOS才需要这样手动解压,MacOS上已经有封装好的对象NSBitmapImageRep可以做这个事。

关于图片解压,还有几个问题不清楚:

1.本来以为调用imageWithData方法只是持有了数据,没有做解压相关的事,后来看到调用堆栈发现已经做了一些解压操作,从调用名字看进行了huffman解码,不知还会继续做到解码jpg的哪一步。 

UIImage_jpg

2.以上图片手动解压方式都是在CPU进行的,如果不进行手动解压,把图片放进layer里,让底层自动做这个事,是会用GPU进行的解压的。不知用GPU解压与用CPU解压速度会差多少,如果GPU速度很快,就算是在主线程做解压,也变得可以接受了,就不需要手动解压这样的优化了,不过目前没找到方法检测GPU解压的速度。

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