AFNetworking框架分析(五)——响应的序列化AFURL
这一篇将分析网络请求收到数据时的响应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,也从侧面建议不要直接使用父类。
所以,当需要响应具体不同类型的数据序列化操作时,都是由其对应的子类来完成任务。
- (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工具,了解一下,可以更快速地解压与渲染图片,节省系统资源。