AFN 3.0学习总结(三)
参考:AFNetworking 3.0 源码解读(三)之 AFURLRequestSerialization
说明:很多内容都是摘抄原文,只是根据自己的需要进行摘抄或者总结,如有不妥请及时指出,谢谢。
一、URL编码知识
1、为什么需要url编码
全部采用ASCII编码,目的就是为了统一标准,不出现歧义
2、us-ascii字符集中没有对应的可打印的字符
所以说并非是所有的ascii字符都是合法的
3、rangeOfComposedCharacterSequencesForRange
根据range对字符串截取,但是该函数的好处就是不对阶段emoji表情或者中文,它会截取完整的字符
二、httpbody
1、AFMultipartFormData
大家在使用AFN上传图片或者文件的时候,都会用到AFMultipartFormData这个协议,然后调用协议内容的方法,拼接数据,但是内部到底是如何实现的呢?我们来分析一下
先看一个完整的post的请求
某app的一个登录POST请求:
POST / HTTP/1.1 Host: log.nuomi.com Content-Type: multipart/form-data; boundary=Boundary+6D3E56AA6EAA83B7 Cookie: access_log=7bde65268e2260bb0a85c7de2c67c468; BAIDUID=428D86FDBA6028DE2A5496BE3E7FC308:FG=1; BAINUOCUID=4368e1b7499c455dcd437da336ca1ca9feb8f57d; BDUSS=Ecwa3NvN1NjNWhsVGxWZktFfkc2bzJxQjZ3RFJpTFBiUzZqZUJZU0ZTSmZsN0ZXQVFBQUFBJCQAAAAAAAAAAAEAAABxbLRYWXV1dXV3dXV1AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAF8KilZfCopWR; bn_na_copid=60139b4b2ba75706fc384d987c2e4007; bn_na_ctag=W3siayI6Imljb25fMSIsInMiOiJ0dWFuIiwidiI6IjMyNiIsInQiOiIxNDUxODg2OTE0In1d;
channel=user_center%7C%7C;
channel_content=;
channel_webapp=webapp;
condition=6.0.3;
domainUrl=sh;
na_qab=6be39bfce918bb7b51887412e009faa6;
UID=1488219249
Connection: keep-alive Accept: */*
User-Agent: Bainuo/6.1.0 (iPhone; iOS 9.0; Scale/2.00)
Accept-Language: zh-Hans-CN;q=1, en-CN;q=0.9
Content-Length: 22207
Accept-Encoding: gzip, deflate
--Boundary+6D3E56AA6EAA83B7 /// 开始
Content-Disposition: form-data; name="app_version"
6.1.0
--Boundary+6D3E56AA6EAA83B7
先来看这个body的内容
--Boundary+6D3E56AA6EAA83B7 /// 开始--------------------初始边界
Content-Disposition: form-data; name="app_version"----------body头
6.1.0------------------------------------body
--Boundary+6D3E56AA6EAA83B7-----------------------------结束边界
组成分为4个部分:1、初始边界 2、body头 3、body 4、结束边界
以下的AFMultipartFormData协议提供的所有的相关方法
方法 body定义 httpbody扩展类对AFHTTPBodyPart的扩展部分,增加了三个属性
1、phase:使用枚举包装body的4大组成部分
2、输入流
3、每个组成部分的位置
增加了两个方法:
1、- (BOOL)transitionToNextPhase 转移到下一个部分
2、- (NSInteger)readData:(NSData *)data intoBuffer:(uint8_t *)buffer maxLength(NSUInteger)length 读取数据
看一看transitionToNextPhase这个函数实现(吐糟一下这个代码引用的排版可真够烂的...)
这个就是把每个阶段依次往后传递(暂时还没理解,先接着往下看),值得注意的就是在header结束时候数据流打开,在body结束时候,数据流关闭。
往下看看inputStream干了些什么
inputstream上图是,根据不同的body类型,inputStream选择不同的创建方式
上面这个函数是把header字典拼接成body的头,看下面的例子
Content-Disposition: form-data; name="record"; filename="record.jpg"
Content-Type: application/json
对于NSInputStream的使用来说,我们要手动实现如下方法
- (NSInteger)read:(uint8_t *)buffer maxLength:(NSUInteger)length;
这样当我们open打开数据流的时候,就会调用这个方法,我们需要在这个方法冲处理我们的逻辑,这也是上面能够读到完成body的数据的解释,还有这个maxLength和buffer有关系,uint8_t在mac64上大小为32768
2、AFMultipartBodyStream
该类继承与NSInputStream,AFHttpBodyPart就像是一个个具体的数据,而AFMultipartBodyStream更像是一个管道,和body相连,数据从body沿着管道流入到request中。
核心方法:(具体实现请看代码,因为排版实在是太不好用,暂时不粘贴里面实现了)
- (NSInteger)read:(uint8_t *)buffer maxLength:(NSUInteger)length
举个列子看看具体是怎么工作的:
1、假如上传一个图片大小为80000,也即是差不多大小80k吧
2、数据并不是一次性读取,而是分批次读取的,一次性读取的大小大概是32k,也就是32*1024=32768的大小(上面分析过,为什么是这个大小)
3、第一次调用后self.currentHTTPBodyPart,指向我们的图片通过如下方法在body中读取了32768大小的数据保存到了buffer中
NSInteger numberOfBytesRead = [self.currentHTTPBodyPart read:&buffer[totalNumberOfBytesRead] maxLength:maxLength];
4、由于整个图片大小为80k,一次只读取了32k,还有一部分数据没有读完,所以这个方法之后还会被再次调用
5、第二次调用这个方法,因为[self.currentHTTPBodyPart hasBytesAvailable]还有可用数据,所以还是会走到else中读取剩下的内容,因此继续执行3的方法
6、至于为什么能够接着上次的数据继续读取余下的内容,这个是body内部封装实现的,具体可看看原文关于body部分的解释
7、重复3、4、5步骤,直到数据全部读取完毕,stream最后就会关闭。到此我们的图片数据就以流的形式传到服务器上了。
3、AFStreamMutipartFormData
在写一个功能的时候,我们往往并不能把业务功能分隔的很完美,这个就跟经验相关了,通过封装AFHTTPBodyPart和AFMultipartBodyStream这两个小工具,我们已经能够拿到数据了。还记得之前的AFMultipartFormData 协议吗?在使用时,我们调用协议的方法,来把数据上传的。理所当然,我们只要让AFMultipartBodyStream实现这个协议不就可以做到我们的目的了吗?
但这显然是不够好的,因此AFNetworking 又 再次对 AFHTTPBodyPart和AFMultipartBodyStream 进行了封装,得到了AFStreamMutipartFormData
我们再看一下post方法,如下
我们发现,post中参数的正好就是这个新的类AFStreamMutipartFormData。
之所以说AFStreamMutipartFormData起到了request和数据的连接作用,就是因为他的这俩个属性。
- (BOOL)appendPartWithFileURL:(NSURL *)fileURL name:(NSString *)name fileName:(NSString *)fileName mimeType:(NSString *)mimeType error:(NSError * __autoreleasing *)error
上看函数很好理解,就是先验证文件有效性,然后读取文件的部分信息,赋值给AFHTTPBodyPart模型,最终把这个模型拼接到管道的模型数组中。
1)NSParameterAssert() 判断参数是否为空,为空就抛出异常
2)使用isFileURL 判断一个URL是否为fileURL
3)使用checkResourceIsReachableAndReturnError判断路径能够到达
4)使用 [[NSFileManager defaultManager] attributesOfItemAtPath:[fileURL path] error:error] 获取本地文件属性。
下面的方法是一个核心的方法
是把数据和请求建立联系的核心方法,通过调用[self.request setHTTPBodyStream:self.bodyStream];这个方法建立联系,最后返回一个最后返回一个NSMutableURLRequest
以上都是为了解决问题的一些辅助类,下面是正题
三、AFHTTPRequestSerializer
1、缓存粗略是一个枚举类型,定义如下:
一一解释一下
1、NSURLRequestUseProtocolCachePolicy
这个是默认的缓存策略,缓存不存在,就请求服务器,缓存存在,会根据response中的Cache-Control字段判断下一步操作,如: Cache-Control字段为must-revalidata, 则询问服务端该数据是否有更新,无更新的话直接返回给用户缓存数据,若已更新,则请求服务端
2、NSURLRequestReloadIgnoringLocalCacheData
这个策略是不管有没有本地缓存,都请求服务器
3、NSURLRequestReloadIgnoringLocalAndRemoteCacheData
这个策略会忽略本地缓存和中间代理 直接访问源server
4、NSURLRequestReturnCacheDataElseLoad
这个策略指,有缓存就是用,不管其有效性,即Cache-Control字段 ,没有就访问源server
5、NSURLRequestReturnCacheDataDontLoad
这个策略只加载本地数据,不做其他操作,适用于没有网路的情况
6、NSURLRequestReloadRevalidatingCacheData
这个策略标示缓存数据必须得到服务器确认才能使用,未实现。
2、HTTPShouldUsePipelining
管线化属性,默认关闭。
在HTTP连接中,一般都是一个请求对应一个连接,每次建立tcp连接是需要一定时间的。管线化,允许一次发送一组请求而不必等到响应。但由于目前并不是所有的服务器都支持这项功能,因此这个属性默认是不开启的。管线化使用同一tcp连接完成任务,因此能够大大提升请求的时间。但是响应要和请求的顺序 保持一致才行。使用场景也有,比如说首页要发送很多请求,可以考虑这种技术。但前提是建立连接成功后才可以使用。
3、NSURLRequestNetworkServiceType
网络服务类型枚举,可以通过这个值来指定当前的网络类型,系统会跟据制定的网络类型对很多方面进行优化,这个就设计到很细微的编程技巧了,可作为一个优化的点备用。
不是太清楚具体是干什么的,以后知道了,再详细的解释。
4、权限验证
这两个方法Authorization 这个词有关,上边的那个方法是根据用户名和密码 生成一个 Authorization 和值,拼接到请求头中规则是这样的
Authorization: Basic YWRtaW46YWRtaW4= 其中Basic表示基础认证,当然还有其他认证。后边的YWRtaW46YWRtaW4= 是根据username:password 拼接后然后在经过Base64编码后的结果。
如果header中有 Authorization这个字段,那么服务器会验证用户名和密码,如果不正确的话会返回401错误。
5、关于block的联想
通过这个自定义序列化的接口,我们可以联想到,在我们自己编码过程总,如果有需要自定义的地方,也可以采用block的方式,比如自定义一个view,里面有title,subtitle可以定制,那我们可以从block中返回这个view,让用户自己设置
6、AFHTTPRequestSerializerObservedKeyPaths
上面的方法就是生成一个字符串数组,而这些字符串就是用来被监听的属性。再继续看下面的方法
在这里对属性的setter进行了重写(屏幕太小,只能对部分属性进行截图,其它的也一样),而且手动触发了kvo,根据第二行注释可以看出,这是为了专门解决一个问题不得不采用手动触发的方式。
那么问题来了,手动触发kvo,岂不是会导致收到2次改变的消息?接着往下看
这个函数的作用是,如果监听对象的集合中,存在某个key,则关闭自动kvo通知,这样以来,上看的手动触发,就不会出现2次的消息通知了。
7、核心
- (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;
}
}
}
//如果请求的method为GET/HEAD/DELETE 直接把查询拼接到URL中,不需要进行处理
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 {
//其它的需要设置下面的字段信息,然后赋值给httpBody
// #2864: an empty string is a valid x-www-form-urlencoded payload
if (!query) {
query = @"";
}
if (![mutableRequest valueForHTTPHeaderField:@"Content-Type"]) {
[mutableRequest setValue:@"application/x-www-form-urlencoded" forHTTPHeaderField:@"Content-Type"];
}
[mutableRequest setHTTPBody:[query dataUsingEncoding:self.stringEncoding]];
}
return mutableRequest;
}
这个方法也不是很复杂,主要的作用就是根据参数对NSUrlRequest进行初始化,包活:
1、请求头
2、query字段,如果是GET/HEAD/DELETE直接拼接到URL中,其它情况拼接到HTTPBody中
注意:这个方法不处理数据流,只处理参数类型的数据
- (NSMutableURLRequest *)requestWithMultipartFormRequest:(NSURLRequest *)request
writingStreamContentsToFile:(NSURL *)fileURL
completionHandler:(void (^)(NSError *error))handler
{
NSParameterAssert(request.HTTPBodyStream);
NSParameterAssert([fileURL isFileURL]);
NSInputStream *inputStream = request.HTTPBodyStream;
NSOutputStream *outputStream = [[NSOutputStream alloc] initWithURL:fileURL append:NO];
__block NSError *error = nil;
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[inputStream scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
[outputStream scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
[inputStream open];
[outputStream open];
while ([inputStream hasBytesAvailable] && [outputStream hasSpaceAvailable]) {
uint8_t buffer[1024];
NSInteger bytesRead = [inputStream read:buffer maxLength:1024];
if (inputStream.streamError || bytesRead < 0) {
error = inputStream.streamError;
break;
}
NSInteger bytesWritten = [outputStream write:buffer maxLength:(NSUInteger)bytesRead];
if (outputStream.streamError || bytesWritten < 0) {
error = outputStream.streamError;
break;
}
if (bytesRead == 0 && bytesWritten == 0) {
break;
}
}
[outputStream close];
[inputStream close];
if (handler) {
dispatch_async(dispatch_get_main_queue(), ^{
handler(error);
});
}
});
NSMutableURLRequest *mutableRequest = [request mutableCopy];
mutableRequest.HTTPBodyStream = nil;
return mutableRequest;
}
上面这个方式是NSInputStream和NSOutputStream用法的一个典型案例,以上函数的意思就是把HTTPBodyStream的数据写入到指定的文件中。
AFJSONRequestSerializer这个类可以把参数转换为json进行上传,如果服务器要求我们上传的必须是json格式,就用上了
可能有很多地方还是没有重点写出来,待我多看几遍深入掌握之后继续写(摘抄)。