参考:AFNetworking 3.0 源码解读(四)之 AFURLResponseSerialization
说明:很多内容都是摘抄原文,只是根据自己的需要进行摘抄或者总结,如有不妥请及时指出,谢谢。
这次主要讲AFURLResponseSerialization(HTTP响应)这一个类的知识。
一、AFURLResponseSerialization
这是一个协议,只要遵循这个协议,就必须实现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解压的速度。