AFURLRequestSerialization功能是在创建网络请求的时候帮我们创建一个NSURLRequest,并封装请求参数到NSURLRequest中。
让我们一起学习一下创建一个好的NSURLRequest都需要做些什么吧。
学习AFURLRequestSerialization之前,我们要准备好两个知识点:multipart/form-data请求和url编码。
multipart/form-data请求是这篇内容的重点,需要我们明白multipart/form-data请求和一般的请求的不同点在哪里,准备一下这方面的知识,我前面一篇文章已经讲过了,需要的可以看一下。
Url编码这里简单介绍一下,够我们用就行了。URI是统一资源标识的意思,通常我们所说的Url只是URI的一种。下面提到的Url编码,实际上应该指是URI编码。
为什么需要Url编码
因为Url中有些字符会引起歧义。例如Url参数字符串中使用key=value键值对这样的形式来传参,键值对之间以&符号分隔,如http://localhost:8080/a?q=abc&ie=utf-8,如果你的value字符串中包含了?、=或者&,那么势必会造成接收Url的服务器解析错误,因此必须将引起歧义的&和= 符号进行转义,也就是对其进行编码。
哪些会引起歧义
RFC3986文档规定,Url中只允许包含英文字母(a-zA-Z)、数字(0-9)、-_.~4个特殊字符以及所有保留字符。
RFC3986文档还规定了保留字符和不安全字符。
保留字符:
通用定界符:":", "#", "[", "]", "@", "?", "/";
子分隔符: "!", "$", "&", "'", "(", ")", "*", "+", ",", ";", "=";
不安全字符:
" ", """, "<", ">", "#", "%", "{", "}", "|", "", "^", "[", "]", "`", "~";
因此在你的url如果包含不安全字符,那么就需要进行编码,而如果你的参数中包含上述这些字符(保留字符和不安全字符)都需要进行编码。但是在In RFC 3986 - Section 3.4中规定"?"、"/"可以不需要编码。
怎么编码
! | * | " | ' | ( | ) | ; | : | @ | & |
---|---|---|---|---|---|---|---|---|---|
%21 | %2A | %22 | %27 | %28 | %29 | %3B | %3A | %40 | %26 |
= | + | $ | , | / | ? | % | # | [ | ] |
---|---|---|---|---|---|---|---|---|---|
%3D | %2B | %24 | %2C | %2F | %3F | %25 | %23 | %5B | %5D |
对于非ASCII字符,需要使用ASCII字符集的超集进行编码得到相应的字节,然后对每个字节执行百分号编码。 对于Unicode字符,RFC文档建议使用utf-8对其进行编码得到相应的字节,然后对每个字节执行百分号编码。如“中文”使用UTF-8字符集得到 的字节为0xE4 0xB8 0xAD 0xE6 0x96 0x87,经过Url编码之后得到“%E4%B8%AD%E6%96%87”。
如果某个字节对应着ASCII字符集中的某个非保留字符,则此字节无需使用百分号表示。 例如“Url编码”,使用UTF-8编码得到的字节是0x55 0x72 0x6C 0xE7 0xBC 0x96 0xE7 0xA0 0x81,由于前三个字节对应着ASCII中的非保留字符“Url”,因此这三个字节可以用非保留字符“Url”表示。最终的Url编码可以简化成 “Url%E7%BC%96%E7%A0%81” ,当然,如果你用"%55%72%6C%E7%BC%96%E7%A0%81”也是可以的。
准备知识到这儿就差不多了,下面进入我们这章的正题:AFURLRequestSerialization
/**
把请求参数按照前面讲的百分号编码方式进行编码
*/
FOUNDATION_EXPORT NSString * AFPercentEscapedStringFromString(NSString *string);
/**
请求参数序列化方法,会先把参数用用上面的方法编码,然后把这些参数照着key=value&key=value的方式设置。
*/
FOUNDATION_EXPORT NSString * AFQueryStringFromParameters(NSDictionary *parameters);
上面是两个序列化方面的函数,接下来就是两个协议
/**
这个协议主要是request对象用来设置请求头、请求体、查询字符串的时候使用
例如,一个JSON的request要将HTTP主体设置为JSON表示,并将HTTP的“Content-Type”头域设置为“application/ JSON”。
*/
@protocol AFURLRequestSerialization
/**
这个实现这个协议目的的方法
*/
- (nullable NSURLRequest *)requestBySerializingRequest:(NSURLRequest *)request
withParameters:(nullable id)parameters
error:(NSError * _Nullable __autoreleasing *)error NS_SWIFT_NOTHROW;
@end
/**
这个协议是在multipart/form-data请求中调用的,一般的get和post请求用不到。
主要是用于外部调用AFHTTPRequestSerializer 中multipartFormRequestWithMethod:URLString:parameters:constructingBodyWithBlock:方法获取request的时候,对constructingBodyWithBlock参数的支持。
实际上是AFStreamingMultipartFormData遵守了这个协议,所以就实现了下面这些方法,外部就可以通过constructingBodyWithBlock的参数的AFStreamingMultipartFormData对象调用下面这些方法。
*/
@protocol AFMultipartFormData
/**
根据文件路径和请求参数名来添加请求参数,文件名和MIME类型根据文件路径获取,出现错误就在error里描述。
设置请求头` Content-Disposition: file; filename=#{filename}; name=#{name}"` and `Content-Type: #{mimeType}`,后面都一样,就不写了。
*/
- (BOOL)appendPartWithFileURL:(NSURL *)fileURL
name:(NSString *)name
error:(NSError * _Nullable __autoreleasing *)error;
/**
根据文件路径、参数名、文件名、mimeType来添加请求参数
*/
- (BOOL)appendPartWithFileURL:(NSURL *)fileURL
name:(NSString *)name
fileName:(NSString *)fileName
mimeType:(NSString *)mimeType
error:(NSError * _Nullable __autoreleasing *)error;
/**
根据传入的流文件、参数名、文件名、长度、mimeType等参数来设置request请求参数。
*/
- (void)appendPartWithInputStream:(nullable NSInputStream *)inputStream
name:(NSString *)name
fileName:(NSString *)fileName
length:(int64_t)length
mimeType:(NSString *)mimeType;
/**
根据传入的data数据、参数名、文件名、mimeType等参数来设置request请求参数。
*/
- (void)appendPartWithFileData:(NSData *)data
name:(NSString *)name
fileName:(NSString *)fileName
mimeType:(NSString *)mimeType;
/**
根据传入的data数据、参数名等参数来设置request请求参数。
不设置filename和content-type,是这样的:`Content-Disposition: form-data; name=#{name}"`
*/
- (void)appendPartWithFormData:(NSData *)data
name:(NSString *)name;
/**
添加自定义的头文件和已经编码过的request body来设置请求参数
*/
- (void)appendPartWithHeaders:(nullable NSDictionary *)headers
body:(NSData *)body;
/**
限制数据包的大小和设置一个延时读取。
是为了防止你数据包内容过大,超过了请求体正文的大小,而导致失败。
*/
- (void)throttleBandwidthWithPacketSize:(NSUInteger)numberOfBytes
delay:(NSTimeInterval)delay;
@end
这一块就是针对multipart/form-data
请求时,添加文件、流、data等参数时调用的。AFStreamingMultipartFormData类会遵守这个协议,然后把AFStreamingMultipartFormData对象作为获取请求方法(例如:multipartFormRequestWithMethod:URLString:parameters:constructingBodyWithBlock:)的参数,然后AFStreamingMultipartFormData对象参数会把添加的参数组装成AFHTTPBodyPart对象添加到AFMultipartBodyStream对象中,然后把AFMultipartBodyStream对象设置为request的bodyStream。
所以,实质上实现了AFMultipartFormData协议的AFStreamingMultipartFormData对象是作为中间人,把参数添加到AFMultipartBodyStream对象中。
接着看AFHTTPRequestSerializer,这就是对NSMutableURLRequest的一些基本设置和获取NSMutableURLRequest的方法。
@interface AFHTTPRequestSerializer : NSObject
/**
编码方式
*/
@property (nonatomic, assign) NSStringEncoding stringEncoding;
/**
是否允许使用设备的蜂窝移动网络来创建request,默认为YES
*/
@property (nonatomic, assign) BOOL allowsCellularAccess;
/**
创建的request所使用的缓存策略,默认使用`NSURLRequestUseProtocolCachePolicy`,该策略表示
如果缓存不存在,直接从服务端获取。如果缓存存在,会根据response中的Cache-Control字段判断
下一步操作,如: Cache-Control字段为must-revalidata, 则 询问服务端该数据是否有更新,无更新话
直接返回给用户缓存数据,若已更新,则请求服务端.
*/
@property (nonatomic, assign) NSURLRequestCachePolicy cachePolicy;
/**
HTTPShouldHandleCookies表示是否应该给request设置cookie并随request一起发送出去,默认是YES
*/
@property (nonatomic, assign) BOOL HTTPShouldHandleCookies;
/**
HTTPShouldUsePipelining表示receiver(理解为iOS客户端)的下一个信息是否必须等到上一个请求回复才能发送,默认是NO。
*/
@property (nonatomic, assign) BOOL HTTPShouldUsePipelining;
/**
设定request的network service类型. 默认是`NSURLNetworkServiceTypeDefault`.
这个network service是为了告诉系统网络层这个request使用的目的
比如NSURLNetworkServiceTypeVoIP表示的就这个request是用来请求网际协议通话技术(Voice over IP)。
系统能根据提供的信息来优化网络处理,从而优化电池寿命,网络性能等等
*/
@property (nonatomic, assign) NSURLRequestNetworkServiceType networkServiceType;
/**
超时机制,默认60秒
*/
@property (nonatomic, assign) NSTimeInterval timeoutInterval;
///---------------------------------------
/// 请求头相关配置
///---------------------------------------
/**
获取http请求头的属性,默认情况下包含以下字段:
- `Accept-Language` 接收语言
- `User-Agent` 带有各种bundle标识符和操作系统名称的内容
只读属性,如果需要添加或者删除需要调用`setValue:
forHTTPHeaderField: `.
*/
@property (readonly, nonatomic, strong) NSDictionary *HTTPRequestHeaders;
/**
返回一个默认配置的序列化对象
*/
+ (instancetype)serializer;
/**
设置http请求头,如果value是nil,就删除已经存在的字段。
*/
- (void)setValue:(nullable NSString *)value
forHTTPHeaderField:(NSString *)field;
/**
获取http请求头对应的头域的值。
*/
- (nullable NSString *)valueForHTTPHeaderField:(NSString *)field;
/**
对用户名和密码用":"拼接,然后进行base 64编码,设置Basic Authorization头域
*/
- (void)setAuthorizationHeaderFieldWithUsername:(NSString *)username
password:(NSString *)password;
/**
清空Authorization的请求头内容
*/
- (void)clearAuthorizationHeader;
///-------------------------------------------------------
/// query字符串参数序列化的配置
///-------------------------------------------------------
/**
需要把参数拼接到url后面的方法集合,默认包括 `GET`, `HEAD`和 `DELETE`
*/
@property (nonatomic, strong) NSSet *HTTPMethodsEncodingParametersInURI;
/**
根据预定义的方式设置序列化的方法
*/
- (void)setQueryStringSerializationWithStyle:(AFHTTPRequestQueryStringSerializationStyle)style;
/**
设置一个自定义的序列化方法block,在序列化request的时候如果有自定义的方法,会优先调用,否则才调用默认的。
定义将参数编码到查询字符串过程中的块。此块返回查询字符串并接受三个参数:请求、要编码的参数,以及在试图为给定请求编码参数时发生的错误。
*/
- (void)setQueryStringSerializationWithBlock:(nullable NSString * (^)(NSURLRequest *request, id parameters, NSError * __autoreleasing *error))block;
///-------------------------------
/// 创建一个request对象
///-------------------------------
/**
根据指定的HTTP方法和URL字符串创建一个`NSMutableUrlRequest `对象。
如果HTTP方法是“get”、“head”或“delete”,则这些参数将用于构造一个URL编码的query字符串,该字符串被追加到请求的URL中。
否则,根据 `parameterencoding `属性的值进行编码,并设置为请求体。
*/
- (NSMutableURLRequest *)requestWithMethod:(NSString *)method
URLString:(NSString *)URLString
parameters:(nullable id)parameters
error:(NSError * _Nullable __autoreleasing *)error;
/**
通过构建`multipart/form-data`请求体来创建NSMutableURLRequest
@param block 一个带有实现了协议的对象的block,用于向`multipart/form-data`类型的请求体中添加数据。
上面讲AFMultipartFormData协议的时候就讲过了,实现了< AFMultipartFormData >协议的AFStreamingMultipartFormData类作为中间人,
把参数组装为AFHTTPBodyPart类,然后把AFHTTPBodyPart类的实体对象加到AFMultipartBodyStream中。AFMultipartBodyStream就是http请求体。
*/
- (NSMutableURLRequest *)multipartFormRequestWithMethod:(NSString *)method
URLString:(NSString *)URLString
parameters:(nullable NSDictionary *)parameters
constructingBodyWithBlock:(nullable void (^)(id formData))block
error:(NSError * _Nullable __autoreleasing *)error;
/**
这个方法会把request的HTTPBodyStream写如果指定文件,然后返回一个删除HTTPBodyStream之后的request。
这样做为了修复一个bug--在与Amazon S3服务器交互时无法发送Content-Length头域.
被写入HTTPBodyStream的文件将会被`AFURLSessionManager`的`uploadTaskWithRequest:fromFile:progress:completionHandler:`发送或者以data的形式作为HTTPBody被发送。
*/
- (NSMutableURLRequest *)requestWithMultipartFormRequest:(NSURLRequest *)request
writingStreamContentsToFile:(NSURL *)fileURL
completionHandler:(nullable void (^)(NSError * _Nullable error))handler;
@end
加下来就是正文了,看看前面这些方法都是怎么实现的,用到了哪些知识点。
NSString * AFPercentEscapedStringFromString(NSString *string) {
//需要做百分号编码处理的字符串,前面讲过的
static NSString * const kAFCharactersGeneralDelimitersToEncode = @":#[]@"; // RFC 3986文档允许"?"、"/"可以不用编码
static NSString * const kAFCharactersSubDelimitersToEncode = @"!$&'()*+,;=";
//获取系统中不需要百分号编码的字符集合
NSMutableCharacterSet * allowedCharacterSet = [[NSCharacterSet URLQueryAllowedCharacterSet] mutableCopy];
//删除RFC 3986文档规定要做编码的字符,剩下的就是最终不需要做编码的字符集合
[allowedCharacterSet removeCharactersInString:[kAFCharactersGeneralDelimitersToEncode stringByAppendingString:kAFCharactersSubDelimitersToEncode]];
// FIXME: https://github.com/AFNetworking/AFNetworking/pull/3028
// return [string stringByAddingPercentEncodingWithAllowedCharacters:allowedCharacterSet];
static NSUInteger const batchSize = 50;
NSUInteger index = 0;
NSMutableString *escaped = @"".mutableCopy;
while (index < string.length) {
NSUInteger length = MIN(string.length - index, batchSize);
NSRange range = NSMakeRange(index, length);
// 获取真正的range,防止特殊字符被分开编码
range = [string rangeOfComposedCharacterSequencesForRange:range];
NSString *substring = [string substringWithRange:range];
//对除去allowedCharacterSet字符集合之外的字符进行编码
NSString *encoded = [substring stringByAddingPercentEncodingWithAllowedCharacters:allowedCharacterSet];
[escaped appendString:encoded];
index += range.length;
}
return escaped;
}
大部分知识点注释都解释清楚了啊,有一个rangeOfComposedCharacterSequencesForRange :
不是很清楚啊,可以看一下我之前的关于字符串真正的range的讲解。
@interface AFQueryStringPair : NSObject
@property (readwrite, nonatomic, strong) id field;
@property (readwrite, nonatomic, strong) id value;
- (instancetype)initWithField:(id)field value:(id)value;
- (NSString *)URLEncodedStringValue;
@end
@implementation AFQueryStringPair
- (instancetype)initWithField:(id)field value:(id)value {
self = [super init];
if (!self) {
return nil;
}
self.field = field;
self.value = value;
return self;
}
- (NSString *)URLEncodedStringValue {
if (!self.value || [self.value isEqual:[NSNull null]]) {
return AFPercentEscapedStringFromString([self.field description]);
} else {
return [NSString stringWithFormat:@"%@=%@", AFPercentEscapedStringFromString([self.field description]), AFPercentEscapedStringFromString([self.value description])];
}
}
@end
这个内部类主要是为了方便把键值对进行百分号编码之后用"="连接起来,很简单,也没什么知识点,自己看看就好。
//把参数编码之后编码为用&符连接起来
NSString * AFQueryStringFromParameters(NSDictionary *parameters) {
NSMutableArray *mutablePairs = [NSMutableArray array];
for (AFQueryStringPair *pair in AFQueryStringPairsFromDictionary(parameters)) {
[mutablePairs addObject:[pair URLEncodedStringValue]];
}
//把数组中的元素用&链接起来
return [mutablePairs componentsJoinedByString:@"&"];
}
//把字典转化为AFQueryStringPair对象数组
NSArray * AFQueryStringPairsFromDictionary(NSDictionary *dictionary) {
return AFQueryStringPairsFromKeyAndValue(nil, dictionary);
}
//把key-value键值对转化为AFQueryStringPair对象数组
NSArray * AFQueryStringPairsFromKeyAndValue(NSString *key, id value) {
NSMutableArray *mutableQueryStringComponents = [NSMutableArray array];
//创建一个排序对象
NSSortDescriptor *sortDescriptor = [NSSortDescriptor sortDescriptorWithKey:@"description" ascending:YES selector:@selector(compare:)];
if ([value isKindOfClass:[NSDictionary class]]) {
NSDictionary *dictionary = value;
// 按照sortDescriptor排序所有的key,在反序列化有歧义的序列时时比较重要的
for (id nestedKey in [dictionary.allKeys sortedArrayUsingDescriptors:@[ sortDescriptor ]]) {
id nestedValue = dictionary[nestedKey];
if (nestedValue) {
[mutableQueryStringComponents addObjectsFromArray:AFQueryStringPairsFromKeyAndValue((key ? [NSString stringWithFormat:@"%@[%@]", key, nestedKey] : nestedKey), nestedValue)];
}
}
} else if ([value isKindOfClass:[NSArray class]]) {
NSArray *array = value;
for (id nestedValue in array) {
[mutableQueryStringComponents addObjectsFromArray:AFQueryStringPairsFromKeyAndValue([NSString stringWithFormat:@"%@[]", key], nestedValue)];
}
} else if ([value isKindOfClass:[NSSet class]]) {
NSSet *set = value;
for (id obj in [set sortedArrayUsingDescriptors:@[ sortDescriptor ]]) {
[mutableQueryStringComponents addObjectsFromArray:AFQueryStringPairsFromKeyAndValue(key, obj)];
}
} else {
[mutableQueryStringComponents addObject:[[AFQueryStringPair alloc] initWithField:key value:value]];
}
return mutableQueryStringComponents;
}
都很简单的知识点,这几个函数是用于普通的get或者set方法的参数的序列化,AFQueryStringPairsFromKeyAndValue函数把参数转化为AFQueryStringPair对象的数组,然后AFQueryStringFromParameters函数就调用间接AFQueryStringPairsFromKeyAndValue函数获取AFQueryStringPair对象数组,把每个对象进行url编码之后,用&符连接起来生成一个字符串就完成了。
static NSArray * AFHTTPRequestSerializerObservedKeyPaths() {
static NSArray *_AFHTTPRequestSerializerObservedKeyPaths = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
_AFHTTPRequestSerializerObservedKeyPaths = @[NSStringFromSelector(@selector(allowsCellularAccess)), NSStringFromSelector(@selector(cachePolicy)), NSStringFromSelector(@selector(HTTPShouldHandleCookies)), NSStringFromSelector(@selector(HTTPShouldUsePipelining)), NSStringFromSelector(@selector(networkServiceType)), NSStringFromSelector(@selector(timeoutInterval))];
});
return _AFHTTPRequestSerializerObservedKeyPaths;
}
这个就是指定了request请求序列化要观察的属性列表、是一个数组,里面有对蜂窝数据、缓存策略、cookie、管道、网络状态、超时这几个元素,返回一个数组对象。
- (instancetype)init {
self = [super init];
if (!self) {
return nil;
}
self.stringEncoding = NSUTF8StringEncoding;
self.mutableHTTPRequestHeaders = [NSMutableDictionary dictionary];
/*
枚举系统的language列表。然后设置`Accept-Language`请求头域。优先级逐级降低,最多五个。
*/
// Accept-Language HTTP Header; see http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.4
NSMutableArray *acceptLanguagesComponents = [NSMutableArray array];
[[NSLocale preferredLanguages] enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
float q = 1.0f - (idx * 0.1f);
[acceptLanguagesComponents addObject:[NSString stringWithFormat:@"%@;q=%0.1g", obj, q]];
*stop = q <= 0.5f;
}];
//数组元素使用`, `分割
[self setValue:[acceptLanguagesComponents componentsJoinedByString:@", "] forHTTPHeaderField:@"Accept-Language"];
NSString *userAgent = nil;
#if TARGET_OS_IOS
// User-Agent Header; see http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.43
userAgent = [NSString stringWithFormat:@"%@/%@ (%@; iOS %@; Scale/%0.2f)", [[NSBundle mainBundle] infoDictionary][(__bridge NSString *)kCFBundleExecutableKey] ?: [[NSBundle mainBundle] infoDictionary][(__bridge NSString *)kCFBundleIdentifierKey], [[NSBundle mainBundle] infoDictionary][@"CFBundleShortVersionString"] ?: [[NSBundle mainBundle] infoDictionary][(__bridge NSString *)kCFBundleVersionKey], [[UIDevice currentDevice] model], [[UIDevice currentDevice] systemVersion], [[UIScreen mainScreen] scale]];
#elif TARGET_OS_WATCH
// User-Agent Header; see http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.43
userAgent = [NSString stringWithFormat:@"%@/%@ (%@; watchOS %@; Scale/%0.2f)", [[NSBundle mainBundle] infoDictionary][(__bridge NSString *)kCFBundleExecutableKey] ?: [[NSBundle mainBundle] infoDictionary][(__bridge NSString *)kCFBundleIdentifierKey], [[NSBundle mainBundle] infoDictionary][@"CFBundleShortVersionString"] ?: [[NSBundle mainBundle] infoDictionary][(__bridge NSString *)kCFBundleVersionKey], [[WKInterfaceDevice currentDevice] model], [[WKInterfaceDevice currentDevice] systemVersion], [[WKInterfaceDevice currentDevice] screenScale]];
#elif defined(__MAC_OS_X_VERSION_MIN_REQUIRED)
userAgent = [NSString stringWithFormat:@"%@/%@ (Mac OS X %@)", [[NSBundle mainBundle] infoDictionary][(__bridge NSString *)kCFBundleExecutableKey] ?: [[NSBundle mainBundle] infoDictionary][(__bridge NSString *)kCFBundleIdentifierKey], [[NSBundle mainBundle] infoDictionary][@"CFBundleShortVersionString"] ?: [[NSBundle mainBundle] infoDictionary][(__bridge NSString *)kCFBundleVersionKey], [[NSProcessInfo processInfo] operatingSystemVersionString]];
#endif
if (userAgent) {
if (![userAgent canBeConvertedToEncoding:NSASCIIStringEncoding]) {
NSMutableString *mutableUserAgent = [userAgent mutableCopy];
if (CFStringTransform((__bridge CFMutableStringRef)(mutableUserAgent), NULL, (__bridge CFStringRef)@"Any-Latin; Latin-ASCII; [:^ASCII:] Remove", false)) {
userAgent = mutableUserAgent;
}
}
[self setValue:userAgent forHTTPHeaderField:@"User-Agent"];
}
//默认参数需要追加到url query后面的的方法
// HTTP Method Definitions; see http://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html
self.HTTPMethodsEncodingParametersInURI = [NSSet setWithObjects:@"GET", @"HEAD", @"DELETE", nil];
//对序列化时需要观察的属性添加观察者
self.mutableObservedChangedKeyPaths = [NSMutableSet set];
for (NSString *keyPath in AFHTTPRequestSerializerObservedKeyPaths()) {
if ([self respondsToSelector:NSSelectorFromString(keyPath)]) {
[self addObserver:self forKeyPath:keyPath options:NSKeyValueObservingOptionNew context:AFHTTPRequestSerializerObserverContext];
}
}
return self;
}
- (void)setAllowsCellularAccess:(BOOL)allowsCellularAccess {
[self willChangeValueForKey:NSStringFromSelector(@selector(allowsCellularAccess))];
_allowsCellularAccess = allowsCellularAccess;
[self didChangeValueForKey:NSStringFromSelector(@selector(allowsCellularAccess))];
}
- (void)setCachePolicy:(NSURLRequestCachePolicy)cachePolicy {
[self willChangeValueForKey:NSStringFromSelector(@selector(cachePolicy))];
_cachePolicy = cachePolicy;
[self didChangeValueForKey:NSStringFromSelector(@selector(cachePolicy))];
}
......
初始化方法,设置一些默认值,然后对需要观察的属性添加观察者,在后面是需要观察的属性的setter方法,手动触发kvo属性。
中间那么一些方法都很简单,自己看看,就不写了。
接下来就是这章的重点,构建NSMutableRequest
#pragma mark -
- (NSMutableURLRequest *)requestWithMethod:(NSString *)method
URLString:(NSString *)URLString
parameters:(id)parameters
error:(NSError *__autoreleasing *)error
{
NSParameterAssert(method);
NSParameterAssert(URLString);
NSURL *url = [NSURL URLWithString:URLString];
NSParameterAssert(url);
NSMutableURLRequest *mutableRequest = [[NSMutableURLRequest alloc] initWithURL:url];
mutableRequest.HTTPMethod = method;
/**
手动触发需要观察的属性的kvo
*/
for (NSString *keyPath in AFHTTPRequestSerializerObservedKeyPaths()) {
if ([self.mutableObservedChangedKeyPaths containsObject:keyPath]) {
[mutableRequest setValue:[self valueForKeyPath:keyPath] forKey:keyPath];
}
}
//根据创建的request和参数调用下面的方法创建一个最终的request
mutableRequest = [[self requestBySerializingRequest:mutableRequest withParameters:parameters error:error] mutableCopy];
return mutableRequest;
}
#pragma mark - AFURLRequestSerialization
- (NSURLRequest *)requestBySerializingRequest:(NSURLRequest *)request
withParameters:(id)parameters
error:(NSError *__autoreleasing *)error
{
NSParameterAssert(request);
NSMutableURLRequest *mutableRequest = [request mutableCopy];
//用枚举的方式设置请求头
[self.HTTPRequestHeaders enumerateKeysAndObjectsUsingBlock:^(id field, id value, BOOL * __unused stop) {
if (![request valueForHTTPHeaderField:field]) {
[mutableRequest setValue:value forHTTPHeaderField:field];
}
}];
NSString *query = nil;
if (parameters) {
//如果有自定义的序列化方法就调用自定义的
if (self.queryStringSerialization) {
NSError *serializationError;
query = self.queryStringSerialization(request, parameters, &serializationError);
if (serializationError) {
if (error) {
*error = serializationError;
}
return nil;
}
} else {
switch (self.queryStringSerializationStyle) {
case AFHTTPRequestQueryStringDefaultStyle:
//默认的序列化方式
query = AFQueryStringFromParameters(parameters);
break;
}
}
}
//`GET`、`HEAD`、`DELET`三种方式是需要把参数追加到query字符串后面
if ([self.HTTPMethodsEncodingParametersInURI containsObject:[[request HTTPMethod] uppercaseString]]) {
if (query && query.length > 0) {
mutableRequest.URL = [NSURL URLWithString:[[mutableRequest.URL absoluteString] stringByAppendingFormat:mutableRequest.URL.query ? @"&%@" : @"?%@", query]];
}
} else {
//一般的POST等方式就设置为HTTPBody
// #2864: an empty string is a valid x-www-form-urlencoded payload
if (!query) {
query = @"";
}
//设置Content-Type头域
if (![mutableRequest valueForHTTPHeaderField:@"Content-Type"]) {
[mutableRequest setValue:@"application/x-www-form-urlencoded" forHTTPHeaderField:@"Content-Type"];
}
[mutableRequest setHTTPBody:[query dataUsingEncoding:self.stringEncoding]];
}
return mutableRequest;
}
上面的方法就是一般情况下的请求(非multipart表单请求)走第一个方法,然后调用AFURLRequestSerialization协议进行参数序列化,拼接query或者设置HTTPBody,这就是一般情况下的request,就构建完成了。
接下来就是我们的重点内容,multipart表单请求,还是先来个例子给大家看一下
//开始
--A4CditForm07uar02yZ34WTsOi5HI0YtW7
Content-Disposition: form-data; name="data"; filename="abc.png"
Content-Type: application/octet-stream
......//数据
--A4CditForm07uar02yZ34WTsOi5HI0YtW7
Content-Disposition: form-data; name="Upload"
Submit Query
--A4CditForm07uar02yZ34WTsOi5HI0YtW7--// 表示body结束了
这就是一个multipart表单请求的格式,先有一个整体的概念,下面再详细讲怎么把参数构建为这种格式。
#pragma mark -
//第一个AFHTTPPart表单构建初始边界,即报文主体的初始边界
static NSString * AFCreateMultipartFormBoundary() {
return [NSString stringWithFormat:@"Boundary+%08X%08X", arc4random(), arc4random()];
}
static NSString * const kAFMultipartFormCRLF = @"\r\n";
//非第一个AFHTTPPart表单构建开始边界
static inline NSString * AFMultipartFormInitialBoundary(NSString *boundary) {
return [NSString stringWithFormat:@"--%@%@", boundary, kAFMultipartFormCRLF];
}
//非最后一个AFHTTPPart表单构建结束边界
static inline NSString * AFMultipartFormEncapsulationBoundary(NSString *boundary) {
return [NSString stringWithFormat:@"%@--%@%@", kAFMultipartFormCRLF, boundary, kAFMultipartFormCRLF];
}
//最后一个AFHTTPPart表单构建结束边界
static inline NSString * AFMultipartFormFinalBoundary(NSString *boundary) {
return [NSString stringWithFormat:@"%@--%@--%@", kAFMultipartFormCRLF, boundary, kAFMultipartFormCRLF];
}
//根据文件扩展获取MIME类型
static inline NSString * AFContentTypeForPathExtension(NSString *extension) {
//根据扩展名获取UTI同一类型标识符
NSString *UTI = (__bridge_transfer NSString *)UTTypeCreatePreferredIdentifierForTag(kUTTagClassFilenameExtension, (__bridge CFStringRef)extension, NULL);
//UTI同一类型标识符转换为MIME类型
NSString *contentType = (__bridge_transfer NSString *)UTTypeCopyPreferredTagWithClass((__bridge CFStringRef)UTI, kUTTagClassMIMEType);
if (!contentType) {
return @"application/octet-stream";
} else {
return contentType;
}
}
这就是表单边界的构建方法和MIME类型的获取
//序列化multipart表单参数的四个阶段
typedef enum {
//开始边界阶段
AFEncapsulationBoundaryPhase = 1,
//表单头阶段
AFHeaderPhase = 2,
//表单主体阶段
AFBodyPhase = 3,
//表单结束阶段
AFFinalBoundaryPhase = 4,
} AFHTTPBodyPartReadPhase;
@interface AFHTTPBodyPart () {
AFHTTPBodyPartReadPhase _phase;
NSInputStream *_inputStream;
unsigned long long _phaseReadOffset;
}
//进入下一阶段
- (BOOL)transitionToNextPhase;
//读取数据流
- (NSInteger)readData:(NSData *)data
intoBuffer:(uint8_t *)buffer
maxLength:(NSUInteger)length;
@end
@implementation AFHTTPBodyPart
- (instancetype)init {
self = [super init];
if (!self) {
return nil;
}
//参数序列化进入初始阶段
[self transitionToNextPhase];
return self;
}
- (void)dealloc {
if (_inputStream) {
[_inputStream close];
_inputStream = nil;
}
}
//获取主体数据流
- (NSInputStream *)inputStream {
if (!_inputStream) {
if ([self.body isKindOfClass:[NSData class]]) {
_inputStream = [NSInputStream inputStreamWithData:self.body];
} else if ([self.body isKindOfClass:[NSURL class]]) {
_inputStream = [NSInputStream inputStreamWithURL:self.body];
} else if ([self.body isKindOfClass:[NSInputStream class]]) {
_inputStream = self.body;
} else {
_inputStream = [NSInputStream inputStreamWithData:[NSData data]];
}
}
return _inputStream;
}
//multipart表单头的字符串
- (NSString *)stringForHeaders {
NSMutableString *headerString = [NSMutableString string];
for (NSString *field in [self.headers allKeys]) {
[headerString appendString:[NSString stringWithFormat:@"%@: %@%@", field, [self.headers valueForKey:field], kAFMultipartFormCRLF]];
}
[headerString appendString:kAFMultipartFormCRLF];
return [NSString stringWithString:headerString];
}
//表单大小
- (unsigned long long)contentLength {
unsigned long long length = 0;
//读取初始边界长度
NSData *encapsulationBoundaryData = [([self hasInitialBoundary] ? AFMultipartFormInitialBoundary(self.boundary) : AFMultipartFormEncapsulationBoundary(self.boundary)) dataUsingEncoding:self.stringEncoding];
length += [encapsulationBoundaryData length];
//读取表单头长度
NSData *headersData = [[self stringForHeaders] dataUsingEncoding:self.stringEncoding];
length += [headersData length];
//表单主体长度
length += _bodyContentLength;
// 表单结束长度
NSData *closingBoundaryData = ([self hasFinalBoundary] ? [AFMultipartFormFinalBoundary(self.boundary) dataUsingEncoding:self.stringEncoding] : [NSData data]);
length += [closingBoundaryData length];
return length;
}
//是否有数据可读
- (BOOL)hasBytesAvailable {
// Allows `read:maxLength:` to be called again if `AFMultipartFormFinalBoundary` doesn't fit into the available buffer
if (_phase == AFFinalBoundaryPhase) {
return YES;
}
//数据流还没打开、已经打开、正在读、正在写,都代表还有数据可以读
//数据流已经结束、已经关闭、出现错误表示没有数据可以读了
switch (self.inputStream.streamStatus) {
case NSStreamStatusNotOpen:
case NSStreamStatusOpening:
case NSStreamStatusOpen:
case NSStreamStatusReading:
case NSStreamStatusWriting:
return YES;
case NSStreamStatusAtEnd:
case NSStreamStatusClosed:
case NSStreamStatusError:
default:
return NO;
}
}
//上传时需要读区数据的时候,AFMultipartBodyStream调用此方法
- (NSInteger)read:(uint8_t *)buffer
maxLength:(NSUInteger)length
{
NSInteger totalNumberOfBytesRead = 0;
//初始阶段,构建初始边界
if (_phase == AFEncapsulationBoundaryPhase) {
NSData *encapsulationBoundaryData = [([self hasInitialBoundary] ? AFMultipartFormInitialBoundary(self.boundary) : AFMultipartFormEncapsulationBoundary(self.boundary)) dataUsingEncoding:self.stringEncoding];
totalNumberOfBytesRead += [self readData:encapsulationBoundaryData intoBuffer:&buffer[totalNumberOfBytesRead] maxLength:(length - (NSUInteger)totalNumberOfBytesRead)];
}
//表单头阶段:构建表单头
if (_phase == AFHeaderPhase) {
//设置的表单头域进行编码
NSData *headersData = [[self stringForHeaders] dataUsingEncoding:self.stringEncoding];
//编码后的表单头域写入数据流
totalNumberOfBytesRead += [self readData:headersData intoBuffer:&buffer[totalNumberOfBytesRead] maxLength:(length - (NSUInteger)totalNumberOfBytesRead)];
}
//表单主体阶段:设置表单主体
if (_phase == AFBodyPhase) {
NSInteger numberOfBytesRead = 0;
//把数据流的内容写入
numberOfBytesRead = [self.inputStream read:&buffer[totalNumberOfBytesRead] maxLength:(length - (NSUInteger)totalNumberOfBytesRead)];
if (numberOfBytesRead == -1) {
return -1;
} else {
totalNumberOfBytesRead += numberOfBytesRead;
//判断数据流是否写入完成,结束后进入下一阶段
if ([self.inputStream streamStatus] >= NSStreamStatusAtEnd) {
[self transitionToNextPhase];
}
}
}
//结束阶段:构建结束边界
if (_phase == AFFinalBoundaryPhase) {
//构建结束边界
NSData *closingBoundaryData = ([self hasFinalBoundary] ? [AFMultipartFormFinalBoundary(self.boundary) dataUsingEncoding:self.stringEncoding] : [NSData data]);
//结束边界写入数据流
totalNumberOfBytesRead += [self readData:closingBoundaryData intoBuffer:&buffer[totalNumberOfBytesRead] maxLength:(length - (NSUInteger)totalNumberOfBytesRead)];
}
return totalNumberOfBytesRead;
}
//判断数据的大小是否超过了表单允许的最大数值,选取两个较小的一个
- (NSInteger)readData:(NSData *)data
intoBuffer:(uint8_t *)buffer
maxLength:(NSUInteger)length
{
NSRange range = NSMakeRange((NSUInteger)_phaseReadOffset, MIN([data length] - ((NSUInteger)_phaseReadOffset), length));
[data getBytes:buffer range:range];
_phaseReadOffset += range.length;
//如果读入的数据已经超过了
if (((NSUInteger)_phaseReadOffset) >= [data length]) {
[self transitionToNextPhase];
}
return (NSInteger)range.length;
}
//进入下一阶段
- (BOOL)transitionToNextPhase {
if (![[NSThread currentThread] isMainThread]) {
dispatch_sync(dispatch_get_main_queue(), ^{
[self transitionToNextPhase];
});
return YES;
}
switch (_phase) {
case AFEncapsulationBoundaryPhase:
_phase = AFHeaderPhase;
break;
case AFHeaderPhase:
//要进入下一阶段(AFBodyPhase),先打开数据流,把数据流加入runloop的伪模式
[self.inputStream scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes];
[self.inputStream open];
_phase = AFBodyPhase;
break;
case AFBodyPhase:
[self.inputStream close];
_phase = AFFinalBoundaryPhase;
break;
case AFFinalBoundaryPhase:
default:
_phase = AFEncapsulationBoundaryPhase;
break;
}
_phaseReadOffset = 0;
return YES;
}
AFHTTPBodyPart类,该有的注释都有了,具体的就不在这儿多解释了,一个AFHTTPBodyPart对象就是一个表单数据,需要上传数据的时候就调用read:maxLength:
方法读取,然后调用contentLength
获取数据长度。
//继承自NSInputStream
@interface AFMultipartBodyStream ()
//编码方式
@property (readwrite, nonatomic, assign) NSStringEncoding stringEncoding;
//装在AFHTTPBodyPart对象的数组
@property (readwrite, nonatomic, strong) NSMutableArray *HTTPBodyParts;
//AFHTTPBodyPart对象的迭代器,方便迭代
@property (readwrite, nonatomic, strong) NSEnumerator *HTTPBodyPartEnumerator;
//当前的AFHTTPBodyPart对象
@property (readwrite, nonatomic, strong) AFHTTPBodyPart *currentHTTPBodyPart;
@property (readwrite, nonatomic, strong) NSOutputStream *outputStream;
@property (readwrite, nonatomic, strong) NSMutableData *buffer;
@end
@implementation AFMultipartBodyStream
#if (defined(__IPHONE_OS_VERSION_MAX_ALLOWED) && __IPHONE_OS_VERSION_MAX_ALLOWED >= 80000) || (defined(__MAC_OS_X_VERSION_MAX_ALLOWED) && __MAC_OS_X_VERSION_MAX_ALLOWED >= 1100)
@synthesize delegate;
#endif
@synthesize streamStatus;
@synthesize streamError;
- (instancetype)initWithStringEncoding:(NSStringEncoding)encoding {
self = [super init];
if (!self) {
return nil;
}
self.stringEncoding = encoding;
self.HTTPBodyParts = [NSMutableArray array];
self.numberOfBytesInPacket = NSIntegerMax;
return self;
}
//设置初始边界和结束边界
- (void)setInitialAndFinalBoundaries {
if ([self.HTTPBodyParts count] > 0) {
for (AFHTTPBodyPart *bodyPart in self.HTTPBodyParts) {
bodyPart.hasInitialBoundary = NO;
bodyPart.hasFinalBoundary = NO;
}
//第一个AFHTTPBodyPart对象设置初始边界
[[self.HTTPBodyParts firstObject] setHasInitialBoundary:YES];
//最后一个设置结束边界
[[self.HTTPBodyParts lastObject] setHasFinalBoundary:YES];
}
}
//把AFHTTPBodyPart对象加入到当前对象的HTTPBodyParts数组中
- (void)appendHTTPBodyPart:(AFHTTPBodyPart *)bodyPart {
[self.HTTPBodyParts addObject:bodyPart];
}
- (BOOL)isEmpty {
return [self.HTTPBodyParts count] == 0;
}
#pragma mark - NSInputStream
//在上传数据的时候系统调用该方法获取数据流
- (NSInteger)read:(uint8_t *)buffer
maxLength:(NSUInteger)length
{
if ([self streamStatus] == NSStreamStatusClosed) {
return 0;
}
NSInteger totalNumberOfBytesRead = 0;
//读入数据
while ((NSUInteger)totalNumberOfBytesRead < MIN(length, self.numberOfBytesInPacket)) {
if (!self.currentHTTPBodyPart || ![self.currentHTTPBodyPart hasBytesAvailable]) {
if (!(self.currentHTTPBodyPart = [self.HTTPBodyPartEnumerator nextObject])) {
break;
}
} else {
//去当前参数大小和数据包最大值中较小的的一个作为数据包大小
NSUInteger maxLength = MIN(length, self.numberOfBytesInPacket) - (NSUInteger)totalNumberOfBytesRead;
//调用AFHTTPBodyPart对象的read:maxLength:方法读取数据
NSInteger numberOfBytesRead = [self.currentHTTPBodyPart read:&buffer[totalNumberOfBytesRead] maxLength:maxLength];
//读取数据是否出错
if (numberOfBytesRead == -1) {
self.streamError = self.currentHTTPBodyPart.inputStream.streamError;
break;
} else {
totalNumberOfBytesRead += numberOfBytesRead;
//判断是否需要延时操作
if (self.delay > 0.0f) {
[NSThread sleepForTimeInterval:self.delay];
}
}
}
}
NSLog(@"buffer : %s contentLength: %zd", buffer, length);
return totalNumberOfBytesRead;
}
- (BOOL)getBuffer:(__unused uint8_t **)buffer
length:(__unused NSUInteger *)len
{
return NO;
}
//是否还有可读数据
- (BOOL)hasBytesAvailable {
return [self streamStatus] == NSStreamStatusOpen;
}
AFMultipartBodyStream对象在网络请求发送报文的时候以流的形式作为报文主体,它的内部包含多个AFHTTPBodyPart对象,系统读取AFMultipartBodyStream对象的数据时,AFMultipartBodyStream对象就会去读取AFHTTPBodyPart对象的数据。
AFMultipartBodyStream对象设置了哪些AFHTTPBodyPart是需要设置开始边界,哪些需要设置结束边界,还规定了一个表单的最大长度等数据。
- (BOOL)appendPartWithFileURL:(NSURL *)fileURL
name:(NSString *)name
error:(NSError * __autoreleasing *)error
{
NSParameterAssert(fileURL);
NSParameterAssert(name);
NSString *fileName = [fileURL lastPathComponent];
//根据文件扩展名MIME类型
NSString *mimeType = AFContentTypeForPathExtension([fileURL pathExtension]);
return [self appendPartWithFileURL:fileURL name:name fileName:fileName mimeType:mimeType error:error];
}
- (BOOL)appendPartWithFileURL:(NSURL *)fileURL
name:(NSString *)name
fileName:(NSString *)fileName
mimeType:(NSString *)mimeType
error:(NSError * __autoreleasing *)error
{
NSParameterAssert(fileURL);
NSParameterAssert(name);
NSParameterAssert(fileName);
NSParameterAssert(mimeType);
//文件路径出错
if (![fileURL isFileURL]) {
NSDictionary *userInfo = @{NSLocalizedFailureReasonErrorKey: NSLocalizedStringFromTable(@"Expected URL to be a file URL", @"AFNetworking", nil)};
if (error) {
*error = [[NSError alloc] initWithDomain:AFURLRequestSerializationErrorDomain code:NSURLErrorBadURL userInfo:userInfo];
}
return NO;
} else if ([fileURL checkResourceIsReachableAndReturnError:error] == NO) {
NSDictionary *userInfo = @{NSLocalizedFailureReasonErrorKey: NSLocalizedStringFromTable(@"File URL not reachable.", @"AFNetworking", nil)};
if (error) {
*error = [[NSError alloc] initWithDomain:AFURLRequestSerializationErrorDomain code:NSURLErrorBadURL userInfo:userInfo];
}
return NO;
}
//获取文件属性
NSDictionary *fileAttributes = [[NSFileManager defaultManager] attributesOfItemAtPath:[fileURL path] error:error];
if (!fileAttributes) {
return NO;
}
//设置表单Content-Disposition和Content-Type头域
NSMutableDictionary *mutableHeaders = [NSMutableDictionary dictionary];
[mutableHeaders setValue:[NSString stringWithFormat:@"form-data; name=\"%@\"; filename=\"%@\"", name, fileName] forKey:@"Content-Disposition"];
[mutableHeaders setValue:mimeType forKey:@"Content-Type"];
//构建AFHTTPBodyPart对象表单
AFHTTPBodyPart *bodyPart = [[AFHTTPBodyPart alloc] init];
bodyPart.stringEncoding = self.stringEncoding;
bodyPart.headers = mutableHeaders;
bodyPart.boundary = self.boundary;
bodyPart.body = fileURL;
bodyPart.bodyContentLength = [fileAttributes[NSFileSize] unsignedLongLongValue];
//把AFHTTPBodyPart对象装载到AFMultipartBodyStream对象中
[self.bodyStream appendHTTPBodyPart:bodyPart];
return YES;
}
//和上面的一样,就不写了
.....
/**
限制数据包的大小和设置一个延时读取。
是为了防止你数据包内容过大,超过了请求体正文的大小,而导致失败
*/
- (void)throttleBandwidthWithPacketSize:(NSUInteger)numberOfBytes
delay:(NSTimeInterval)delay
{
self.bodyStream.numberOfBytesInPacket = numberOfBytes;
self.bodyStream.delay = delay;
}
- (NSMutableURLRequest *)requestByFinalizingMultipartFormData {
if ([self.bodyStream isEmpty]) {
return self.request;
}
// 重制初始边界和结束边界以便获取正确的报文大小
[self.bodyStream setInitialAndFinalBoundaries];
//把装载AFTHTTPPart对象装载完成后的AFMultipartBodyStream对象设置为报文主体
[self.request setHTTPBodyStream:self.bodyStream];
//设置请求的Content-Type和Content-Length头域
[self.request setValue:[NSString stringWithFormat:@"multipart/form-data; boundary=%@", self.boundary] forHTTPHeaderField:@"Content-Type"];
[self.request setValue:[NSString stringWithFormat:@"%llu", [self.bodyStream contentLength]] forHTTPHeaderField:@"Content-Length"];
return self.request;
}
AFStreamingMultipartFormData类就是一个中间人,外部参数通过这个类把构建为AFHTTPBodyPart对象,然后把AFHTTPBodyPart写入到AFMultipartBodyStream对象数据流中,在参数写入数据流完成之后调用requestByFinalizingMultipartFormData方法来设置请求体,最后设置请求头类型和请求体大小,request创建完成。