网络AFNetworking 3.0

AFN 3.0学习总结(三)

2018-01-12  本文已影响356人  油麦菜洋葱头

参考: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格式,就用上了

可能有很多地方还是没有重点写出来,待我多看几遍深入掌握之后继续写(摘抄)。

上一篇 下一篇

猜你喜欢

热点阅读