AFNetworking 3.0 源码解读(四)之 AFURLResponseSerialization

2022-11-08,,,

本篇是AFNetworking 3.0 源码解读的第四篇了。

AFNetworking 3.0 源码解读(一)之 AFNetworkReachabilityManager

AFNetworking 3.0 源码解读(二)之 AFSecurityPolicy

AFNetworking 3.0 源码解读(三)之 AFURLRequestSerialization

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

这是一个协议,只要遵守这个协议,就要实现NSSecureCoding/NSCopying这两个协议,还要实现

- (nullable id)responseObjectForResponse:(nullable NSURLResponse *)response
data:(nullable NSData *)data
error:(NSError * _Nullable __autoreleasing *)error NS_SWIFT_NOTHROW;

这个方法来返回序列化后的结果。不管是下边的AFHTTPResponseSerializer,还是它的子类,都遵守这个协议,也就是在各自的实现中实现了这个协议,然后返回了属于自身的一个结果。

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

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

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

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

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

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

来看看实现部分:

这个初始化,看起来还是很普通的,主要是初始化一些默认值,我们在这里花点篇幅讲解一下NSIndexSet这个集合的知识。

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

我们先看个例子:

    NSMutableIndexSet *indexSetM = [NSMutableIndexSet indexSet];
[indexSetM addIndex:];
[indexSetM addIndex:];
[indexSetM addIndex:];
[indexSetM addIndex:];
[indexSetM addIndexesInRange:NSMakeRange(, )]; [indexSetM enumerateIndexesUsingBlock:^(NSUInteger idx, BOOL * _Nonnull stop) {
NSLog(@"%lu",idx);
}];

打印结果如下:

-- ::00.826 qikeyunDemo[:]
-- ::00.827 qikeyunDemo[:]
-- ::00.827 qikeyunDemo[:]
-- ::00.827 qikeyunDemo[:]
-- ::00.827 qikeyunDemo[:]
-- ::00.828 qikeyunDemo[:]
-- ::00.828 qikeyunDemo[:]
-- ::00.828 qikeyunDemo[:]
-- ::00.828 qikeyunDemo[:]
-- ::00.828 qikeyunDemo[:]
-- ::00.828 qikeyunDemo[:]
-- ::00.828 qikeyunDemo[:]
-- ::00.828 qikeyunDemo[:]
-- ::00.829 qikeyunDemo[:]

这充分说明了一下几点:

1. 它是一个无符号整数的集合。

2. 使用addIndex方法可以添加单个整数值,使用addIndexesInRange可以添加一个范围,比如NSMakeRange(20, 10) 取20包括20后边十个整数。

3. 唯一性,集合内的整数不会重复出现。

具体用法就不再次做详细的解释了,有兴趣的朋友可以看看这篇文章: NSIndexSet 用法

因此 self.acceptableStatusCodes = [NSIndexSet indexSetWithIndexesInRange:NSMakeRange(200, 100)]; 这个方法设置了默认接受的状态码范围为200 ~ 299.

 - (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都不能为空
if (self.acceptableContentTypes && ![self.acceptableContentTypes containsObject:[response MIMEType]] &&
!([response MIMEType] == nil && [data length] == )) { if ([data length] > && [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;
}

这个方法就是检测响应的有效性的。默认是YES。整个方法中比较值得关注的是对NSError的使用。在这里不对它做详细的介绍,大概解释下最长用的一些东东。

1. 我们关注下它的NSDictionary *userInfo这个属性,错误信息一般都在这个字典中获得。因为是一个字典,所以我们在给NSError的userInfo赋值的时候,会用到key。我们看看系统自带的key和含义有哪些?

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

在上边的那个方法中,有可能会出现两个错误,在self.acceptableContentTypes和self.acceptableStatusCodes这两个判断中,如果都出现错误怎么办呢?

这就用到了NSUnderlyingErrorKey 这个字段,它标示一个优先的错误,value为NSErro对象。

通过下边的这个函数进行了转换和赋值:

 #pragma mark - NSSecureCoding

 + (BOOL)supportsSecureCoding {
return YES;
} - (instancetype)initWithCoder:(NSCoder *)decoder {
self = [self init];
if (!self) {
return nil;
} self.acceptableStatusCodes = [decoder decodeObjectOfClass:[NSIndexSet class] forKey:NSStringFromSelector(@selector(acceptableStatusCodes))];
self.acceptableContentTypes = [decoder decodeObjectOfClass:[NSIndexSet class] forKey:NSStringFromSelector(@selector(acceptableContentTypes))]; return self;
} - (void)encodeWithCoder:(NSCoder *)coder {
[coder encodeObject:self.acceptableStatusCodes forKey:NSStringFromSelector(@selector(acceptableStatusCodes))];
[coder encodeObject:self.acceptableContentTypes forKey:NSStringFromSelector(@selector(acceptableContentTypes))];
} #pragma mark - NSCopying - (instancetype)copyWithZone:(NSZone *)zone {
AFHTTPResponseSerializer *serializer = [[[self class] allocWithZone:zone] init];
serializer.acceptableStatusCodes = [self.acceptableStatusCodes copyWithZone:zone];
serializer.acceptableContentTypes = [self.acceptableContentTypes copyWithZone:zone]; return serializer;
}

这几个是对 NSCopying NSSecureCoding 这两个协议的实现部分,算是固定写法吧。为了节省篇幅 ,在下边的各个分类中,就不对这些代码进行说明了。

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

我们来看看这个Json序列化的头文件。

这个选项可以设置json的读取选项,我们点进去可以看到:

NSJSONReadingMutableContainers    这个解析json成功后返回一个容器。

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

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

我们举个例子说明一下NSJSONReadingMutableContainers:

 NSString *str = @"{\"name\":\"zhangsan\"}";

     NSMutableDictionary *dict = [NSJSONSerialization JSONObjectWithData:[str dataUsingEncoding:NSUTF8StringEncoding] options: error:nil];
// 应用崩溃,不选用NSJSONReadingOptions,则返回的对象是不可变的,NSDictionary
[dict setObject:@"male" forKey:@"sex"]; NSMutableDictionary *dict = [NSJSONSerialization JSONObjectWithData:[str dataUsingEncoding:NSUTF8StringEncoding] options:NSJSONReadingMutableContainers error:nil];
// 没问题,使用NSJSONReadingMutableContainers,则返回的对象是可变的,NSMutableDictionary
[dict setObject:@"male" forKey:@"sex"]; NSLog(@"%@", dict);

如果服务器返回的json的最外层并不是以NSArray 或者 NSDictionary ,而是一个有效的json fragment ,比如 就返回了一个@"abc"。 那么我们使用NSJSONReadingAllowFragments这个选项也能够解析出来。

这个属性用来设置是否过滤NSNull。

通过初始化方法可以看出来,AFNetworking支持的ContentType有:

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;
}
} id responseObject = nil;
NSError *serializationError = 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:]];
if (data.length > && !isSpace) {
responseObject = [NSJSONSerialization JSONObjectWithData:data options:self.readingOptions error:&serializationError];
} else {
return nil;
} if (self.removesKeysWithNullValues && responseObject) {
responseObject = AFJSONObjectByRemovingKeysWithNullValues(responseObject, self.readingOptions);
} if (error) {
*error = AFErrorWithUnderlyingError(serializationError, *error);
} return responseObject; }

json转化的和新方法,这个方法中除了加了一些必要的判断之外,新出现了两个函数,函数的实现也比较好理解,就不做详细介绍了,把代码贴出来:

 // 检测错误或者有限错误中是否匹配code和domain
static BOOL AFErrorOrUnderlyingErrorHasCodeInDomain(NSError *error, NSInteger code, NSString *domain) { // error中的domain和code相同,直接返回YES
if ([error.domain isEqualToString:domain] && error.code == code) {
return YES; // 否则检测error中的NSUnderlyingErrorKey
} else if (error.userInfo[NSUnderlyingErrorKey]) {
return AFErrorOrUnderlyingErrorHasCodeInDomain(error.userInfo[NSUnderlyingErrorKey], code, domain);
} return NO;
}
 // 该方法用于删除一个对象中的NUll
static id AFJSONObjectByRemovingKeysWithNullValues(id JSONObject, NSJSONReadingOptions readingOptions) { // 1.数组
if ([JSONObject isKindOfClass:[NSArray class]]) { //1.1 创建一个可变的数组,为了提高性能,使用Capacity创建
NSMutableArray *mutableArray = [NSMutableArray arrayWithCapacity:[(NSArray *)JSONObject count]]; // 1.2 遍历数组,通过迭代的手段清空数组内的null
for (id value in (NSArray *)JSONObject) {
[mutableArray addObject:AFJSONObjectByRemovingKeysWithNullValues(value, readingOptions)];
} // 1.3 readingOptions == NSJSONReadingMutableContainers 返回可变容器,其他返回不可变容器
return (readingOptions & NSJSONReadingMutableContainers) ? mutableArray : [NSArray arrayWithArray:mutableArray];
} // 2. 思想同上
else if ([JSONObject isKindOfClass:[NSDictionary class]]) {
NSMutableDictionary *mutableDictionary = [NSMutableDictionary dictionaryWithDictionary:JSONObject];
for (id <NSCopying> 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;
}

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

这个分类是解析XML数据的。

支持的ContentType:

application/xml

text/xml

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

注: 这个子类只在mac os x上使用

支持的ContentType:

application/xml

text/xml

=============================== 分隔线 =================================

这个分类用来把json解析成PropertyList:

支持的ContentType:

application/x-plist

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

这是一个UIImage分类,添加了一个安全的NData转UIImage的方法;那么什么叫安全的呢?当我们访问或者操纵数据的时候,由于数据还可能被别人操纵,这就有可能出现不安全的情况,为了解决这个问题,引入NSLock这个锁对象 。

使用方法:

创建一个单例NSLock *lock
[lock lock]
do something...
[lock unlock]

这个私有方法,返回一个按照scale收缩的图片。这就使用到了上边的那个安全转换数据的方法了。但看这个方法我们应该知道下边两点知识:

image.images   images这个属性的用法,下边我们举例说明

- (instancetype)initWithCGImage:(CGImageRef)cgImage scale:(CGFloat)scale orientation:(UIImageOrientation)orientation NS_AVAILABLE_IOS(4_0);   这个是创建UIImage的一个方法,稍微注意下需要哪些参数。

 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];

效果图如下:

那么这个images属性就可以应用到很多地方了,我们可以使用这个属性来生成一个类似gif的效果或者简单的动画是最常用的场景。

 // 根据响应结果和scale返回一张图片
static UIImage * AFInflatedImageFromResponseWithDataAtScale(NSHTTPURLResponse *response, NSData *data, CGFloat scale) { // 1. 判空
if (!data || [data length] == ) {
return nil;
} // 2. 但凡带CG开头的,标示是CoreGraphics
CGImageRef imageRef = NULL; // 3. CGDataProviderRef 可以细节为CoreGraphics中对data的包装
CGDataProviderRef dataProvider = CGDataProviderCreateWithCFData((__bridge CFDataRef)data); // 判断响应返回的MIMEType类型,
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 > * || bitsPerComponent > ) {
CGImageRelease(imageRef); return image;
} // CGImageGetBytesPerRow() calculates incorrectly in iOS 5.0, so defer to CGBitmapContextCreate
size_t bytesPerRow = ;
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的知识,这个还不是特别了解。稍后会补一补这方面的知识。

上边的代码有一点不明白的地方,:

   // 这行代码不太明白什么意思。。
if (width * height > * || bitsPerComponent > ) {
CGImageRelease(imageRef); return image;
}

后来看到了一篇文章 把原话粘贴出来

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可以做这个事。

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

.本来以为调用imageWithData方法只是持有了数据,没有做解压相关的事,后来看到调用堆栈发现已经做了一些解压操作,从调用名字看进行了huffman解码,不知还会继续做到解码jpg的哪一步。
UIImage_jpg .以上图片手动解压方式都是在CPU进行的,如果不进行手动解压,把图片放进layer里,让底层自动做这个事,是会用GPU进行的解压的。不知用GPU解压与用CPU解压速度会差多少,如果GPU速度很快,就算是在主线程做解压,也变得可以接受了,就不需要手动解压这样的优化了,不过目前没找到方法检测GPU解压的速度。

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

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

支持的ContentType:

image/tiff

image/jpeg

image/gif

image/png

image/ico

image/x-icon

image/bmp

image/x-bmp

image/x-xbitmap

image/x-win-bitmap

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

这个复合型的子类有一个数组属性,里边装着多种序列化类型。看其实现方法也是,遍历数组中的内容,只要能转化成功,就返回数据。

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

好了,这篇就到此为止了,我们学到了

通过一个协议来得到不同转换的结果。
知道了AFNetworking响应结果支持的各种类型。
大体了解了NSIndexSet的使用方法
如果创建一个NSError 和 带有优先错误的NSUnderlyingErrorKey
服务器返回的图片是压缩格式,要进行解压
使用images来实现gif效果

AFNetworking 3.0 源码解读(四)之 AFURLResponseSerialization的相关教程结束。

《AFNetworking 3.0 源码解读(四)之 AFURLResponseSerialization.doc》

下载本文的Word格式文档,以方便收藏与打印。