美文网首页iOS开发专题iOS猛码计划XMPP 环信 融云
iOS 框架注解—「AFNetworking 网络请求」

iOS 框架注解—「AFNetworking 网络请求」

作者: 简晰333 | 来源:发表于2017-03-11 04:12 被阅读1142次

    引导


    AFNetWorking 基本是 iOS 开发中使用网络通信框架的标配,这个框架本身比较庞大,也很复杂,但是使用起来非常非常简单。

    本篇文章主要从 [AFN / 功能逻辑 / 基本使用 / 封装优化] 整理,该模块将系统化学习,后续替换、补充文章内容 ~
    在「时间 & 知识 」有限内,总结的文章难免有「未全、不足 」的地方,还望各位好友指出槽点,以提高文章质量@CoderLN著;

    目录:

    1. 需求 | 版本区别
    2. AFN 功能模块
    3. AFN 工程目录
    4. AFN 内部逻辑处理(get)
    5. AFN 内部逻辑处理(post)
    6. AFN GET | POST 请求
    7. AFN 文件下载 | 上传
    8. AFN 文件上传(图片、视频)
    9. AFN 序列化处理
    10. AFN 检测网络状态
    11. AFN https请求
    12. AFN 离线断点下载
    13. AFN 封装优化
    14. SourceCodeToolsClassPublic-Codeidea

    需求 | 版本区别


    AFN --> 需求 | 版本区别
    • 1.x 版本,内部底层是基于 NSURLConnection 的,是对 NSURLConnection 一次封装。

    • 在13年,苹果推出 NSURLSession 也就是会话管理者,后来 2.x AFN框架又对 NSURLSession 进行一次封装,其实在 2.0-2.6.3 AFN内部是使用两套系统,一套是基于 NSURLConnection的,一套是基于 NSURLSession的。

    • 版本升级到3.0之后,AFN 就不在支持 NSURLConnection 了,把有关 URLConnection 的代码已全部移除。

    AFN 功能模块

    /**
     #warning - 以下为功能模块相关的方法示例, 具体方法作用、使用、注解请移步 -> github.com/CoderLN/Apple-GitHub-Codeidea
     
     AFN 功能模块
     
     1.NSURLSession 网络通信模块管理者
        AFURLSessionManager
        AFHTTPSessionManager -> AFNURL 继承
     
     2.Serialization 网络通信信息序列化/反序列化模块
        <AFURLRequestSerialization> 请求序列化
            AFHTTPRequestSerializer
            AFJSONRequestSerializer
            AFPropertyListRequestSerializer
     
        <AFURLResponseSerialization> 响应者序列化
            AFHTTPResponseSerializer 返回原始类型,服务器返回什么类型就按什么类型解析(Data二进制、html网页)
            AFJSONResponseSerializer 返回JSON类型,默认解析方案
            AFXMLParserResponseSerializer 返回XML类型,XML解析方案
     
     Additional Functionality 额外的功能
        3.AFSecurityPolicy 网络通信安全策略模块
        4.AFNetworkReachabilityManager 网络状态监听管理者
        `HTTPS`(HTTP+SSL加密协议)
     
     5.UIKit+AFNetworking UIKit类库的扩展与工具类
     
     */
    

    AFN 工程目录


    目前版本是 3.1.0,我通过 CocoaPods 导入的 AFNetworking,导入后目录如下

    CocoaPods-->AFN 3.1.0工程目录

    使用 CocoaPods 导入后可以看到目录很清晰主要是在五个文件夹下, 除去 Support Files,可以看到AF分为如下5个功能模块:

    • NSURLSession(网络通信模块)

    • ReachAbility(网络状态监听模块)

    • Security(网络通信安全策略模块)

    • Serialization(网络通信信息序列化/反序列化模块)

    • UIKit(UIKit库的扩展)

    其核心当然是网络通信模块,其余的四个模块,均是为了配合网络通信或对已有 UIKit 的一个扩展及工具包。
    这五个模块所对应的类的结构关系图如下所示:


    AFN 功能模块-->关系

    可以看到,AFN 的核心是 AFURLSessionManager 类,AFHTTPSessionManager 继承于 AFURLSessionManager, 针对HTTP协议传输做了封装。而 AFURLResponseSerializationAFSecurityPolicyAFNetworkReachabilityManager则被AFURLSessionManager所用。
    其次,还可以看到一个单独的UIKit 包提供了对 iOS UIKit 类库的扩展与工具类。

    建议:
    可以学习下AFN对 UIKit 做了一些分类,对自己能力提升是非常有帮助的。

    手动导入的时候,显示两个文件夹,如下


    手动导入-->AFN 3.1.0工程目录

    很明显第一个文件夹里边是跟网络请求相关的,第二个是跟UI相关的。

    AFN 内部逻辑处理(get)


    这是 AFNetworking 发起一个 Get 请求的流程图,大概可以分为这几个步骤,下面会逐个解读这个流程。


    AFN-->GET业务逻辑处理.png
    1. AFHTTPSessionManager 发起GET请求
    manager-->GET请求

    这个方法是 AFN 的 Get请求 的起点,其他 Get 请求的方法也都是直接或者间接调用这个方法来发起 Get 请求。这个方法的代码量很少也很直观,就是调用其他方法生成 NSURLSessionDataTask对象的实例,然后调用 NSURLSessionDataTaskresume 方法发起请求。

    2. 创建 NSURLSessionDataTask
    创建-->NSURLSessionDataTask

    这个方法是创建 NSURLSessionDataTask 对象实例并返回这个实例。首先创建一个 NSMutableURLRequest 对象的实例,然后配置。之后是使用 NSMutableURLRequest 对象的实例创建NSURLSessionDataTask 对象实例,然后配置,可以选择性地传入各类Block回调,用于监听网络请求的进度比如上传进度,下载进度,请求成功,请求失败。

    3. 配置 NSMutableURLRequest对象
    配置-->NSMutableURLRequest对象

    在这个方法中先使用了 url 创建了一个 NSMutableURLRequest 对象的实例,并且设置了 HTTPMethodGet 方法(如果是Post方法,那么这里就是设置Post方法)然后使用KVC的方法设置了 NSMutableURLRequest 的一些属性。

    // 设置 NSMutableURLRequest 的属性
    static NSArray * AFHTTPRequestSerializerObservedKeyPaths() {
        static NSArray *_AFHTTPRequestSerializerObservedKeyPaths = nil;
        static dispatch_once_t onceToken;
        dispatch_once(&onceToken, ^{
            //allowsCellularAccess 允许使用数据流量
            //cachePolicy 缓存策略
            //HTTPShouldHandleCookies 处理Cookie
            //HTTPShouldUsePipelining 批量请求
            //networkServiceType 网络状态
            //timeoutInterval 超时
            _AFHTTPRequestSerializerObservedKeyPaths = @[NSStringFromSelector(@selector(allowsCellularAccess)), NSStringFromSelector(@selector(cachePolicy)), NSStringFromSelector(@selector(HTTPShouldHandleCookies)), NSStringFromSelector(@selector(HTTPShouldUsePipelining)), NSStringFromSelector(@selector(networkServiceType)), NSStringFromSelector(@selector(timeoutInterval))];
        });
    
        return _AFHTTPRequestSerializerObservedKeyPaths;
    }
    
    配置-->NSMutableURLRequest对象

    先设置 HTTP header,之后格式化请求参数,设置参数的编码类型。这个是这个方法的基本操作流程。对于Get操作来说,参数是直接拼接在请求地址后面。

    4. 配置 NSURLSessionDataTask对象
    配置-->NSURLSessionDataTask对象

    之后配置 NSMutableURLRequest 对象就需要配置 NSURLSessionDataTask 对象了。主要分为2个步骤,第一个步骤是创建 NSURLSessionDataTask 对象实例,第二个步骤是给NSURLSessionDataTask 对象实例设置 Delegate。用于实时了解网络请求的过程。

    给NSURLSessionDataTask对象实例设置Delegate.png

    AFN 的代理统一使用 AFURLSessionManagerTaskDelegate 对象来管理,使用 AFURLSessionManagerTaskDelegate 对象来接管NSURLSessionTask 网络请求过程中的回调,然后再传入 AFN 内部进行管理。

    @interface AFURLSessionManagerTaskDelegate : NSObject <NSURLSessionTaskDelegate, 
    NSURLSessionDataDelegate, NSURLSessionDownloadDelegate>
    

    如代码所示 AFURLSessionManagerTaskDelegate 接管了NSURLSessionTaskDelegateNSURLSessionDataDelegateNSURLSessionDownloadDelegate 的各种回调,然后做内部处理。这也是第三方网络请求框架的重点,让网络请求更加易用,好用。

    // 通过 task 的标识符管理代理
    - (void)setDelegate:(AFURLSessionManagerTaskDelegate *)delegate
                forTask:(NSURLSessionTask *)task
    {
        NSParameterAssert(task);
        NSParameterAssert(delegate);
    
        [self.lock lock];
        // 将task和代理类绑定,task的taskIdentifier作为字典的key,delegate作为字典的value
        self.mutableTaskDelegatesKeyedByTaskIdentifier[@(task.taskIdentifier)] = delegate;
        // 给该task添加两个KVO事件(Resume 和 Suspend)
        [self addNotificationObserverForTask:task];
        [self.lock unlock];
    }
    

    通过NSURLSessionTasktaskIdentifier标识符对delegate进行管理,只是用于识别该NSURLSessionTask的代理。

    NSURLSessionTask 设置进度回调

    设置各类回调 Block,给 NSURLSessionTask 使用 KVO 进行各种过程进度监听。

    #pragma mark -
    // 给task添加暂停和恢复的通知
    - (void)addNotificationObserverForTask:(NSURLSessionTask *)task {
        [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(taskDidResume:) name:AFNSURLSessionTaskDidResumeNotification object:task];
        [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(taskDidSuspend:) name:AFNSURLSessionTaskDidSuspendNotification object:task];
    }
    

    监听 NSURLSessionTask 被挂起 和 恢复的通知。

    5. 网络请求开始
    // 发送GET请求
    /**
     GET: 请求路径(不包含参数),url
     parameters: 字典(发送给服务器的数据~参数)
     progress: 进度回调
     success: 成功回调(task:请求任务、responseObject:响应体信息JSON->OC对象)
     failure: 失败回调(error:错误信息)
     task.response: 响应头
     */
    - (NSURLSessionDataTask *)GET:(NSString *)URLString
                       parameters:(id)parameters
                         progress:(void (^)(NSProgress * _Nonnull))downloadProgress
                          success:(void (^)(NSURLSessionDataTask * _Nonnull, id _Nullable))success
                          failure:(void (^)(NSURLSessionDataTask * _Nullable, NSError * _Nonnull))failure
    {
    
        NSURLSessionDataTask *dataTask = [self dataTaskWithHTTPMethod:@"GET"
                                                            URLString:URLString
                                                           parameters:parameters
                                                       uploadProgress:nil
                                                     downloadProgress:downloadProgress
                                                              success:success
                                                              failure:failure];
    
        [dataTask resume];
    
        return dataTask;
    }
    

    NSURLSessionTask 创建和配置完毕之后,它并不会主动执行,而是需要我们主动调用 resume 方法,NSURLSessionTask 才会开始执行。

    6. 网络请求回调
    NSURLSessionDelegate 方法 NSURLSessionTaskDelegate 方法

    AFN 里面有关 NSURLSessionDelegate 的回调方法非常的多,这里我们只说和 NSURLSessionTask 相关的部分方法和 KVO 处理来进行说明,其他的大家可以参考源码细看。


    KVO监听请求过程.png 收到响应数据.png 请求完成.png

    对于我们的 Get请求 来说,我们最关注的莫过于关注请求过程进度,收到响应数据和请求完成这2个回调。

    KVO监听的属性值发生变化:

    // KVO监听的属性值发生变化
    - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSString *,id> *)change context:(void *)context {
        if ([object isKindOfClass:[NSURLSessionTask class]] || [object isKindOfClass:[NSURLSessionDownloadTask class]]) {
            if ([keyPath isEqualToString:NSStringFromSelector(@selector(countOfBytesReceived))]) {
                NSLog(@"countOfBytesReceived");
                // 这个是在Get请求下,网络响应过程中已经收到的数据量
                // 已经收到
                self.downloadProgress.completedUnitCount = [change[NSKeyValueChangeNewKey] longLongValue];
            } else if ([keyPath isEqualToString:NSStringFromSelector(@selector(countOfBytesExpectedToReceive))]) {
                NSLog(@"countOfBytesExpectedToReceive");
                // 这个是在Get请求下,网络响应过程中期待收到的数据量
                // 期待收到
                self.downloadProgress.totalUnitCount = [change[NSKeyValueChangeNewKey] longLongValue];
            } else if ([keyPath isEqualToString:NSStringFromSelector(@selector(countOfBytesSent))]) {
                NSLog(@"countOfBytesSent");
                // 已经发送
                self.uploadProgress.completedUnitCount = [change[NSKeyValueChangeNewKey] longLongValue];
            } else if ([keyPath isEqualToString:NSStringFromSelector(@selector(countOfBytesExpectedToSend))]) {
                NSLog(@"countOfBytesExpectedToSend");
                // 期待发送
                self.uploadProgress.totalUnitCount = [change[NSKeyValueChangeNewKey] longLongValue];
            }
        }
        else if ([object isEqual:self.downloadProgress]) {
            // 下载进度变化
            if (self.downloadProgressBlock) {
                self.downloadProgressBlock(object);
            }
        }
        else if ([object isEqual:self.uploadProgress]) {
            // 上传进度变化
            if (self.uploadProgressBlock) {
                self.uploadProgressBlock(object);
            }
        }
    }
    

    收到请求响应:

    // 收到请求响应
    - (void)URLSession:(NSURLSession *)session
              dataTask:(NSURLSessionDataTask *)dataTask
              didReceiveResponse:(NSURLResponse *)response
              completionHandler:(void (^)(NSURLSessionResponseDisposition disposition))completionHandler {
        NSLog(@"收到请求响应");
        // 允许处理服务器的响应,才会继续接收服务器返回的数据
        NSURLSessionResponseDisposition disposition = NSURLSessionResponseAllow;
        
        // 是否有收到请求响应的回调Block
        if (self.dataTaskDidReceiveResponse) {
            // 若有调用该Block
            disposition = self.dataTaskDidReceiveResponse(session, dataTask, response);
        }
        // 是否有请求响应完成的回调Block
        if (completionHandler) {
            // 若有调用该Block
            completionHandler(disposition);
        }
    }
    

    请求完成:

    // 请求完成
    - (void)URLSession:(NSURLSession *)session
                  task:(NSURLSessionTask *)task
                  didCompleteWithError:(NSError *)error {
        NSLog(@"请求完成");
        // 取出该NSURLSessionTask的代理对象
        AFURLSessionManagerTaskDelegate *delegate = [self delegateForTask:task];
        
        // delegate may be nil when completing a task in the background
        if (delegate) {
            // 若是该代理对象存在,那么将对应数据转给该代理对象处理
            [delegate URLSession:session task:task didCompleteWithError:error];
            // NSURLSessionTask任务完成之后,移除该NSURLSessionTask的代理对象
            [self removeDelegateForTask:task];
        }
        // 是否有请求完成的回调Block
        if (self.taskDidComplete) {
            // 若有调用改Block
            self.taskDidComplete(session, task, error);
        }
    }
    

    因为在配置 NSURLSessionDataTask 对象的时候我们有给 NSURLSessionTask 做了一系列配置,那么当 NSURLSessionDataTask 任务完成之后,我们需要将该 NSURLSessionDataTask 的一系列配置全部清理掉。

    这个是我们的配置过程:

    // 通过task的标识符管理代理
    - (void)setDelegate:(AFURLSessionManagerTaskDelegate *)delegate
                forTask:(NSURLSessionTask *)task
    {
        NSParameterAssert(task);
        NSParameterAssert(delegate);
    
        [self.lock lock];
        self.mutableTaskDelegatesKeyedByTaskIdentifier[@(task.taskIdentifier)] = delegate;
        [delegate setupProgressForTask:task];
        [self addNotificationObserverForTask:task];
        [self.lock unlock];
    }
    

    那么对应的清理过程是这样的,就是设置过程中做了什么,在清理过程中就需要去掉什么。

    // 给task移除delegate
    - (void)removeDelegateForTask:(NSURLSessionTask *)task {
        NSParameterAssert(task);
    
        AFURLSessionManagerTaskDelegate *delegate = [self delegateForTask:task];
        [self.lock lock];
        [delegate cleanUpProgressForTask:task];
        [self removeNotificationObserverForTask:task];
        [self.mutableTaskDelegatesKeyedByTaskIdentifier removeObjectForKey:@(task.taskIdentifier)];
        [self.lock unlock];
    }
    
    cleanUpProgressForTask.png removeNotificationObserverForTask.png

    AFN 内部逻辑处理(post)


    请求序列化方法
    #pragma mark - AFURLRequestSerialization
    // 设置Header和请求参数
    - (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) {
            // 判断header的field是否存在,如果不存在则设置,存在则跳过
            if (![request valueForHTTPHeaderField:field]) {
                // 设置 header
                [mutableRequest setValue:value forHTTPHeaderField:field];
            }
        }];
        
        NSString *query = nil;
        if (parameters) {
            // 用传进来的自定义block格式化请求参数
            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/DELETE方法, 对于GET/HEAD/DELETE方法,把参数加到URL后面
        if ([self.HTTPMethodsEncodingParametersInURI containsObject:[[request HTTPMethod] uppercaseString]]) {
            // 判断是否有参数
            if (query && query.length > 0) {
                // 拼接请求参数
                NSLog(@"query-->%@",query);
                mutableRequest.URL = [NSURL URLWithString:[[mutableRequest.URL absoluteString] stringByAppendingFormat:mutableRequest.URL.query ? @"&%@" : @"?%@", query]];
            }
        } else {
            // #2864: an empty string is a valid x-www-form-urlencoded payload
            if (!query) {
                query = @"";
            }
            // 参数带在body上,大多是POST PUT
            if (![mutableRequest valueForHTTPHeaderField:@"Content-Type"]) {
                // 设置Content-Type HTTP头,告诉服务端body的参数编码类型
                [mutableRequest setValue:@"application/x-www-form-urlencoded" forHTTPHeaderField:@"Content-Type"];
            }
            [mutableRequest setHTTPBody:[query  dataUsingEncoding:self.stringEncoding]];
        }
        
        return mutableRequest;
    }
    

    如果是 Post 请求,那么请求参数是没有拼接在 URL 上面,而是放在 body 上,这是 Post 和 Get 请求的最大区别了,其他过程和Get 请求并没有太多区别。

    总结

    AFN发起Get请求主要分为以下步骤:

    - 1.创建`NSURLSessionDataTask`
    
    - 2.配置`NSURLSessionDataTask`
    
    - 3.设置`NSURLSessionDataTask的Delegate`
    
    - 4.调用`NSURLSessionDataTask`的`resume`方法开始请求
    
    - 5.在`Delegate`的方法里面处理网络请求的各个过程
    
    - 6.清理`NSURLSessionDataTask`的配置
    
    其实也就是使用 `NSURLSessionDataTask` 的步骤,AFN在这几个步骤加了一些封装,让我们的使用更简单。
    

    AFN GET | POST 请求

    下面就直接来代码了【代码具有详细注释】。

    AFN 发送 get 和 post 请求方法,其实内部都会调用这一个方法 - dataTaskWithHTTPMethod:URLString:parameters:uploadProgress:downloadProgress:success:failure:

    /**
     AFN GET/POST请求步骤:
        1.创建会话管理者
        2.设置响应序列化解析方案 (返回数据类型 默认JSON = AFJSON、XML = AFXML、Data text/html = AFHTTP)
        3.拼接参数 (1.可以拼接在基础url后面❓隔开,多个参数(username=CoderLN&pwd=Codeidea)之间以 & 链接; 2.以字典方式拼接参数(parameters:dict))
        4.发送GET、POST请求
            1.responseObject: 响应体信息(内部已编码处理JSON->OC对象)
            2.task.response: 响应头信息
     */
    
    #pragma mark - get请求(返回JSON类型)
    
    - (void)get {
        // 1.创建会话管理者
        AFHTTPSessionManager *manager = [AFHTTPSessionManager manager];
        // 设置解析方案 (默认JSON)
        //manager.responseSerializer = [AFJSONResponseSerializer serializer];
        
        // 2.以字典方式拼接参数
        NSString * urlStr = @"http://120.25.226.186:32812/login";
        NSDictionary *parameters = @{
                                     @"username":@"username",
                                     @"pwd":@"pwd",
                                     @"type":@"JSON"
                                    };
        
        /**
         3.发送GET请求
             @param GET:    NSString类型的请求路径,AFN内部会自动将该路径包装为一个url并创建请求对象
             @param parameters:     请求参数,以字典的方式传递,AFN内部会判断当前是POST请求还是GET请求,
                                    以选择直接拼接还是转换为NSData放到请求体中传递.
             @param progress:   进度回调,GET请求不需要进度此处为nil.
             @param success:    请求成功之后回调Block
                       task:    请求任务、
             responseObject:    响应体信息(内部已编码处理JSON->OC对象,通常是数组或字典)
             @param failure:    失败回调(error:错误信息)
              task.response:    响应头信息
         */
        [manager POST:urlStr parameters:nil progress:^(NSProgress * _Nonnull uploadProgress) {
            
            NSLog(@"%f",1.0 * uploadProgress.completedUnitCount / uploadProgress.totalUnitCount);
        } success:^(NSURLSessionDataTask * _Nonnull task, id  _Nullable responseObject) {
            
            NSLog(@"响应头 %@ , 响应体 %@",task.response,responseObject);
        } failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) {
            
            NSLog(@"%@",error);
        }];
    }
    

    总结:
    这里使用方法其实非常简单和我们之前的使用没有太大的区别,只是这里 AFN 把请求路径给拆分了,把参数单独拿出来,并且用一个字典的方式装载。相信这里大家应该都可以明白为什么作者 把参数单独给拿出来,这样更有利于代码的封装,我们使用起来更加的方便。
    这里关于 AFN(GET | POST 请求)内部业务逻辑是如何处理的,和之前使用 NSURLSession 大致是一样的。

    AFN 文件下载

    /**
     AFN 文件下载步骤:
        1.创建会话管理者
        2.创建下载url和请求对象NSURLRequest
        3.manager创建下载任务DownloadTask
        1.return URL = [destination:指定存储路径(targetPath临时路径, fullPath存储路径) NSSearchPathForDirectoriesInDomains];
        2.下载进度 (1.0 *downloadProgress.completedUnitCount已经完成数据大小 / downloadProgress.totalUnitCount数据总大小)
        4.执行下载resume
     */
    
    - (void)fileDownload {
        // 1.创建会话管理者
        AFHTTPSessionManager *manager = [AFHTTPSessionManager manager];
        
        // 2.创建下载路径和请求对象
        NSURL *url = [NSURL URLWithString:@"http://120.25.226.186:32812/resources/videos/minion_01.mp4"];
        NSURLRequest *request = [NSURLRequest requestWithURL:url];
        
        // 3.创建下载任务(downloadTask)
        /**
         @param Request:            请求对象
         @param progress:           进度回调(监听下载进度)
                completedUnitCount: 已经下载的数据大小
                totalUnitCount:     文件数据的总大小
         @param destination:        回调,该block需要返回值(NSURL类型),告诉系统应该把文件剪切到什么地方.
                targetPath:         文件的临时保存路径(tmp目录)
                response:           响应头信息
         @param completionHandler:  请求完成后回调
                response:           响应头信息
                filePath:           最终文件的保存路径,即destination回调的返回值(fullPath)
                error:              错误信息
         */
        NSURLSessionDownloadTask *downloadTask = [manager downloadTaskWithRequest:request progress:^(NSProgress * _Nonnull downloadProgress) {
            
            NSLog(@"下载进度 %f",1.0 *downloadProgress.completedUnitCount / downloadProgress.totalUnitCount);
        } destination:^NSURL * _Nonnull(NSURL * _Nonnull targetPath, NSURLResponse * _Nonnull response) {
            
            // 指定下载路径 (targetPath临时路径, fullPath存储路径)
            NSString *fullPath = [[NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) lastObject] stringByAppendingPathComponent:response.suggestedFilename];
            NSLog(@"%@\n%@",targetPath,fullPath);
            
            return [NSURL fileURLWithPath:fullPath];
            
        } completionHandler:^(NSURLResponse * _Nonnull response, NSURL * _Nullable filePath, NSError * _Nullable error) {
            
            NSLog(@"%@\n%@",filePath,error);
        }];
        
        // 4.执行下载
        [downloadTask resume];
    }
    
    

    注解:
    如何监听下载进度,AFN 3.0之后的版本监听下载进度是上面的做法。而AFN 在2.6.3 之前并没有提供 progress 回调给我们,此时要想监听下载进度需要使用KVO,给它添加一位观察者监听内部 progress值的改变。

    // 使用KVO监听下载进度
    [progress addObserver:self forKeyPath:@"completedUnitCount" options:NSKeyValueObservingOptionNew context:nil];
    
    // 获取并计算当前文件的下载进度
    -(void)observeValueForKeyPath:(NSString *)keyPath 
                      ofObject:(NSProgress *)progress 
                      change:(NSDictionary<NSString *,id> *)change 
                      context:(void *)context {
     
         NSLog(@"已经下载的数据大小 %zd -- 文件数据的总大小%zd",progress.completedUnitCount,progress.totalUnitCount);
         NSLog(@"下载进度 %f",1.0 * progress.completedUnitCount/progress.totalUnitCount)
    }
    
    - (void)dealloc{
        // 移除(监听)
        [self.person removeObserver:self forKeyPath:@"completedUnitCount"];
    }
    

    AFN 文件上传(图片、视频)

    /**
     AFN 文件上传步骤:
        1.创建会话管理者
        2.发送post请求
            1.使用formData(请求体)来拼接数据:(appendPartWithFileURL:、appendPartWithFileData:);
            2.上传进度 (1.0 * uploadProgress.completedUnitCount已经完成数据大小 / uploadProgress.totalUnitCount数据总大小)
     */
     
    - (void)fileUpload {
        // 1.创建会话管理者
        AFHTTPSessionManager *manager = [AFHTTPSessionManager manager];
    
        /*
         2.发送post请求
             @param POST:                       请求路径(NSString类型)
             @param parameters:                 非文件参数,以字典的方式传递
             @param constructingBodyWithBlock:  处理要上传的文件数据(在该回调中拼接文件参数)
                    formData:                   请求体 (拼接数据)
             @param progress:                   进度回调
                    uploadProgress.completedUnitCount:  已经上传的数据大小
                    uploadProgress.totalUnitCount:      数据的总大小
             @param success:                            成功回调
                    task:                               上传任务
                    responseObject:                     服务器返回的响应体信息(已经以JSON的方式转换为OC对象)
             @param failure :                           失败回调
         */
        [manager POST:@"http://120.25.226.186:32812/upload" parameters:nil constructingBodyWithBlock:^(id<AFMultipartFormData>  _Nonnull formData) {
            
            // 使用formData来拼接数据
            [formData appendPartWithFileURL:[NSURL fileURLWithPath:@"urlPath"] name:@"file" error:nil];
            
        } progress:^(NSProgress * _Nonnull uploadProgress) {
            NSLog(@"上传进度---%f",1.0 * uploadProgress.completedUnitCount / uploadProgress.totalUnitCount);
            
        } success:^(NSURLSessionDataTask * _Nonnull task, id  _Nullable responseObject) {
            NSLog(@"上传成功---%@",responseObject);
            
        } failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) {
            NSLog(@"上传失败---%@",error);
        }];
    }
    
     - - -
     - - -
    
    - (void)videoUpload
    {
        // 在下面
    }
    

    注解:
    这里使用 formData 来拼接数据,共有三种方法如下

    /**
     注解:formData(请求体)来拼接数据 appendPartWithFile
     
        FileData: 二进制数据 要上传的文件参数
        name:     服务器规定的 @"file"
        fileName: 该文件上传到服务器保存名称
        mimeType: 文件的类型 image/png(MIMEType:大类型/小类型)
    
        // 第一种方式
        UIImage *image = [UIImage imageNamed:@"Codeidea.png"];
        NSData *imageData = UIImagePNGRepresentation(image);
        [formData appendPartWithFileData:imageData name:@"file" fileName:@"xxxx.png" mimeType:@"image/png"];
    
        // 第二种方式
        [formData appendPartWithFileURL:[NSURL fileURLWithPath:@" "] name:@"file" fileName:@"123.png" mimeType:@"image/png" error:nil];
    
        // 第三种方式
        [formData appendPartWithFileURL:[NSURL fileURLWithPath:@" "] name:@"file" error:nil];
     */
    

    AFN 序列化处理


    1、AFN 它内部默认把服务器响应的数据当做 JSON来进行解析,所以如果服务器返回给我的不是JSON数据那么请求报错,这个时候需要设置 AFN 对响应信息的解析方式。AFN提供了三种解析响应信息的方式,分别是:

    • AFHTTPResponseSerializer(默认二进制响应数据,解析方案)

    • AFJSONResponseSerializer(返回JSON类型,JSON解析方案.默认)

    • AFXMLParserResponseSerializer(返回XML类型,XML解析)

    2、还有一种情况就是服务器返回给我们的数据格式不太一致(查看:开发者工具Content-Type:text/xml),那么这种情况也有可能请求不成功。
    解决方法:

      1. 强制更换AFN数据解析类型,只支持一下添加的数据类型这样AFN自带的或者后期新增了数据解析类型这里就没有了(不建议使用)。
    • 在原有可解析数据类型基础上,获取AFN原由数据解析类型基础上添加一些响应解析器能够接受的数据类型。

    返回JSON、XML、二进制、text/xml 相关代码

    #pragma mark - get请求(返回XML类型)
    - (void)xml {
        // 1.创建会话管理者
        AFHTTPSessionManager *manager = [AFHTTPSessionManager manager];
        
        // 2.设置解析方式
        
        // 返回数据类型 = 解析方式; JSON = AFJSON、XML = AFXML、Data  = AFHTTP
        manager.responseSerializer = [AFXMLParserResponseSerializer serializer];
        manager.responseSerializer = [AFHTTPResponseSerializer serializer];
        manager.responseSerializer = [AFJSONResponseSerializer serializer];
        
        // 返回数据类型 = 解析方式; text/html (设置支持接收类型 @"text/html") = AFHTTP
        manager.responseSerializer.acceptableContentTypes = [NSSet setWithObject:@"text/html"];
        
        
        // 设置拼接参数
        NSDictionary *dict = @{
                               @"type": @"XML"
                               };
        
        // 3.发送GET请求
        //[manager GET:@"http://120.25.226.186:32812/video?type=XML" parameters: ]
        [manager GET:@"http://120.25.226.186:32812/video" parameters:dict progress:^(NSProgress * _Nonnull downloadProgress) {
            
        } success:^(NSURLSessionDataTask * _Nonnull task, id  _Nullable responseObject) {
        //} success:^(NSURLSessionDataTask * _Nonnull task, NSXMLParser *parser) {
            
            NSLog(@"请求成功-\n%@",responseObject);
           
            // 解析服务器返回的XML数据
            NSXMLParser *parser = (NSXMLParser *)responseObject;
            parser.delegate = self;// 设置代理 遵守<NSXMLParserDelegate>
            [parser parse];// 开始解析
            
        } failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) {
            
            NSLog(@"请求失败-\n%@",error);
        }];
    }
    
    
    #pragma mark - NSXMLParserDelegate
    
    - (void)parser:(NSXMLParser *)parser didStartElement:(NSString *)elementName namespaceURI:(NSString *)namespaceURI qualifiedName:(NSString *)qName attributes:(NSDictionary<NSString *,NSString *> *)attributeDict {
        
        if ([elementName isEqualToString:@"videos"]) {
            return;
        }
        NSLog(@"开始解析某个元素%@--%@",elementName,attributeDict);
    }
    

    AFN 检测网络状态


    方案一:使用 AFN 框架 来检测网络状态的改变。

    #pragma mark - AFN实时检测网络状态
    /**
     AFN实时检测网络状态步骤:
        1.创建检测网络状态管理者 AFNetworkReachabilityManager
        2.检测网络状态改变 setReachabilityStatusChangeBlock:
        3.开始检测 startMonitoring
     */
    - (void)afnReachability
    {
        // 1.创建检测网络状态管理者 2.检测网络状态改变
        [[AFNetworkReachabilityManager sharedManager] setReachabilityStatusChangeBlock:^(AFNetworkReachabilityStatus status) {
            switch (status) {
                case AFNetworkReachabilityStatusReachableViaWiFi:
                    NSLog(@"WiFi");
                    break;
                case AFNetworkReachabilityStatusReachableViaWWAN:
                    NSLog(@"蜂窝网络");
                    break;
                case AFNetworkReachabilityStatusNotReachable:
                    NSLog(@"没有网络");
                    break;
                case AFNetworkReachabilityStatusUnknown:
                    NSLog(@"未知");
                    break;
                    
                default:
                    break;
            }
        }];
        
        // 3.开始检测
        [[AFNetworkReachabilityManager sharedManager] startMonitoring];
    }
    

    方案二:Reachability(系统) 实时检测网络。
    Reachability下载地址

    // 需包含 #import "Reachability.h"
    
    - (void)reachability
    {
        // 1.添加通知(观察者)
        [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(networkStatusChange) name:kReachabilityChangedNotification object:nil];
        
        // 2.检测网络状态 (设置为全局对象)
        Reachability *rb = [Reachability reachabilityForLocalWiFi];
        
        // 3.开始监控网络(一旦网络状态发生改变, 就会发出通知kReachabilityChangedNotification)
        [rb startNotifier];
        self.rb = rb;
        
        // 程序第一次运行就调用检测
        [self networkStatusChange];
    }
    
    
    -(void)networkStatusChange
    {
        // 4.检测手机是否能上网络; 该方法得到一个Reachability类型的蜂窝网络对象
        Reachability * connect = [Reachability reachabilityForInternetConnection];
        // 5.判断网络状态
        switch (connect.currentReachabilityStatus) {
            case ReachableViaWWAN:
                NSLog(@"蜂窝网络");
                break;
            case ReachableViaWiFi:
                NSLog(@"WIFI");
                break;
            case NotReachable:
                NSLog(@"没有网络");
                break;
                
            default:
                break;
        }
    }
    
    - (void)dealloc
    {
        [[NSNotificationCenter defaultCenter] removeObserver:self];
    }
    

    AFN https请求


    在写项目中,数据的安全性至关重要,而仅仅用 POST 请求提交用户的隐私数据,还是不能完全解决安全问题。

    要想非常安全的传输数据,建议使用https。抓包不可以,但是中间人攻击则有可能。建议双向验证防止中间人攻击。

    方案一:使用AFN https

    /**
     AFN https步骤:
        1.创建会话管理者
        2.设置解析方案
            1.设置对证书的处理方式(允许自签名证书YES) securityPolicy.allowInvalidCertificates
            2.是否验证域名的CN字段(NO) securityPolicy.validatesDomainName
        3.发送GET请求
     */
    #pragma mark - 方案二:使用AFN https
    
    - (void)https {
        AFHTTPSessionManager *manager = [AFHTTPSessionManager manager];
        
        // 更改解析方式
        manager.responseSerializer = [AFHTTPResponseSerializer serializer];
        
        // 设置对证书的处理方式
        // 允许自签名证书,必须的
        manager.securityPolicy.allowInvalidCertificates = YES;
        // 是否验证域名的CN字段(不是必须的,但是如果写YES,则必须导入证书)
        manager.securityPolicy.validatesDomainName = NO;
        
        [manager GET:@"https://kyfw.12306.cn/otn" parameters:nil progress:nil success:^(NSURLSessionDataTask * _Nonnull task, id  _Nullable responseObject) {
            NSLog(@"success---%@",[[NSString alloc]initWithData:responseObject encoding:NSUTF8StringEncoding]);
        } failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) {
            NSLog(@"error---%@",error);
        }];
    }
    

    方案二:使用NSURLSession https

    - (void)urlSession {
        // 1.创建session
        NSURLSession *session = [NSURLSession sessionWithConfiguration:[NSURLSessionConfiguration defaultSessionConfiguration] delegate:self delegateQueue:[NSOperationQueue mainQueue]];
        
        // 2.session创建Task
        NSURLSessionDataTask *dataTask = [session dataTaskWithRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:@"https://kyfw.12306.cn/otn"]] completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
            // 3.解析数据
            NSLog(@"%@---%@",[[NSString alloc]initWithData:data encoding:NSUTF8StringEncoding],error);
        }];
        // 4.执行task
        [dataTask resume];
    }
    
    
    
    
    #pragma mark - 遵守<NSURLSessionDelegate>
    
    // 5.如果发送的请求是https的,那么才会调用该方法
    // 如果实现了这个方法,就一定要记得回调 completionHandler(NSURLSessionAuthChallengeUseCredential,credential); 不然会请求造成阻塞
    -(void)URLSession:(NSURLSession *)session didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition, NSURLCredential * _Nullable))completionHandler {
        /**
         判断服务器传给我们的信任的类型,只有是【服务器信任的时候,才安装证书】
         NSURLSessionAuthChallengeDisposition 如何处理证书
         NSURLAuthenticationMethodServerTrust 服务器信任
         */
        if(![challenge.protectionSpace.authenticationMethod isEqualToString:@"NSURLAuthenticationMethodServerTrust"]) {
            return;
        }
        NSLog(@"%@",challenge.protectionSpace);
       
        /*
         NSURLCredential 授权信息
         NSURLSessionAuthChallengeUseCredential = 0, 使用该证书 安装该证书
         NSURLSessionAuthChallengePerformDefaultHandling = 1, 默认采用的方式,该证书被忽略
         NSURLSwillCacheResponseessionAuthChallengeCancelAuthenticationChallenge = 2, 取消请求,证书忽略
         NSURLSessionAuthChallengeRejectProtectionSpace = 3,          拒绝
         
         注解:
         并不是所有的https的请求都需要安装证书(授权)的,请求一些大型的网站有的是强制安装的,如:苹果官网https://www.apple.com
         */
        NSURLCredential *credential = [[NSURLCredential alloc] initWithTrust:challenge.protectionSpace.serverTrust];
        completionHandler(NSURLSessionAuthChallengeUseCredential,credential);
    }
    

    AFN 离线断点下载


    AFN 是基于 NSURLSession 的。所以实现原理和NSURLSession 差不多。 这里使用了 NSURLSessionDataTask,以便实现「离线断点下载」。在这里仅供参考(不必拿走直接用)。

    详情请移步看代码实现。



    AFN 封装优化

    封装思维

    • 在开发的时候可以创建一个工具类,继承自我们的 AFN 中的请求管理者,再控制器中真正发请求的代码使用自己封装的工具类。

    • 这样做的优点是以后如果修改了底层依赖的框架,那么我们修改这个工具类就可以了,而不用再一个一个的去修改。

    • 该工具类一般提供一个单例方法,在该方法中会设置一个基本的请求路径。

    • 该方法通常还会提供对 GET或POST 请求的封装。

    • 在外面的时候通过该工具类来发送请求

    单例方法示例:

    /**
     * 获得全局网络请求实例单例方法
     *
     * @return 网络请求类的实例对象
     */
    + (instancetype)sharedManager
    {
        static NetWorkManager * instance = nil;
        static dispatch_once_t onceToken;
        dispatch_once(&onceToken, ^{
            
            // 设置BaseURL
            // 注意:BaseURL中一定要以/结尾
            instance = [[self alloc] initWithBaseURL:[NSURL URLWithString:@"http://120.25.226.186:32812/"]];
        });
        
        return instance;
    }
    

    接下来可以重写 initWithBaseURL,可根据情况进行配置。

    // 重写 initWithBaseURL
    - (instancetype)initWithBaseURL:(NSURL *)url
    {
        if (self = [super initWithBaseURL:url]) {
            
    #warning 可根据情况进行配置
          
            // 设置响应序列化
            self.responseSerializer = [AFJSONResponseSerializer serializer];
            
            // 设置请求序列化
            AFHTTPRequestSerializer * requestSerializer = [AFHTTPRequestSerializer serializer];
            self.requestSerializer = requestSerializer;
            
            // 设置超时时间
            requestSerializer.timeoutInterval = 5;
            
            // 设置请求头
            [requestSerializer setValue:@"application/json" forHTTPHeaderField:@"Content-Type"];
            
            // 我们项目是把access_token(后台验证用户省份标识)放在了请求头里,有的项目是放在了请求体里,视实际情况而定
            [requestSerializer setValue:ACCESS_TOKEN forHTTPHeaderField:@"access_token"];
            
            // 设置缓存策略
            requestSerializer.cachePolicy = NSURLRequestReloadIgnoringLocalCacheData;
            
            // 是否信任带有一个无效或者过期的SSL证书的服务器,默认不信任。
            self.securityPolicy.allowInvalidCertificates = YES;
            // 是否验证域名的CN字段(不是必须的,但是如果写YES,则必须导入证书)
            self.securityPolicy.validatesDomainName = NO;
        
            
            // 1.强制更换AFN数据解析类型,只支持一下添加的数据类型这样AFN自带的就没有了,如果AFN新增了数据解析类型这里也没有变化,所以用下面2方法,在原有可解析数据类型基础上添加。
            //instance.responseSerializer.acceptableContentTypes = [NSSet setWithObjects:@"application/json", @"text/json", @"text/javascript", @"text/html", @"text/plain", nil];
            
            // 2.获取AFN原由数据解析类型基础上添加一些响应解析器能够接受的数据类型
            NSMutableSet * acceptableContentTypes = [NSMutableSet setWithSet:self.responseSerializer.acceptableContentTypes];
            [acceptableContentTypes addObjectsFromArray:@[@"application/json", @"text/json", @"text/javascript", @"text/html", @"text/plain"]];
            self.responseSerializer.acceptableContentTypes = acceptableContentTypes;
    
        }
        
        return self;
    }
    

    由于代码量,AFN工具类已经放到我的 GitHub 上面,且会替换、补充内容 ~
    在这里先贴出 .h 文件 NetWorkManager.h

    #warning - 以下为功能模块相关的方法示例, 具体方法作用、使用、注解请移步 -> github.com/CoderLN/Apple-GitHub-Codeidea
    
    #import "AFHTTPSessionManager.h"
    
    // NS_ENUM 枚举
    typedef NS_ENUM(NSUInteger, HttpRequestType) {
        HttpRequestTypeGET,
        HttpRequestTypePOST,
    };
    
    
    /**定义请求成功的block*/
    typedef void (^requestSuccess)(id  _Nullable responseObject);
    
    /**定义请求失败的block*/
    typedef void (^requestFailure)(NSError * _Nonnull error);
    
    /**定义 上传/下载 进度block*/
    typedef void (^progress)(float progress);
    
    /**定义 下载完成回调 进度block*/
    typedef void (^completionHandler)(NSURL *fullPath, NSError *error);
    
    
    @interface NetWorkManager : AFHTTPSessionManager
    
    
    /**
     * 获得全局网络请求实例单例方法
     *
     * @return 网络请求类的实例对象
     */
    + (instancetype)sharedManager;
    
    
    #pragma mark - AFN实时检测网络状态
    
    /**
     * AFN实时检测网络状态
     */
    + (void)afnReachability;
    
    
    /**
     * 网络请求
     *
     * @param requestType   GET / POST
     * @param urlString     请求的地址
     * @param parameters    请求的参数
     * @param successBlock       请求成功的回调
     * @param failureBlock       请求失败的回调
     */
    + (void)requestWithType:(HttpRequestType)requestType url:(NSString *)urlString parameters:(id)parameters success:(requestSuccess)successBlock failure:(requestFailure)failureBlock;
    
    
    
    
    /**
     * 文件下载
     *
     * @param urlString             请求的地址
     * @param parameters            文件下载预留参数 (可为nil)
     * @param downloadProgressBlock 下载进度回调
     * @param completionHandler     请求完成回调
     *        fullPath              文件存储路径
     */
    + (void)downloadFileWithURL:(NSString *)urlString parameters:(id)parameters progress:(progress)downloadProgressBlock completionHandler:(completionHandler)completionHandler;
    
    
    
    
    /**
     * 文件上传 (多张图片上传)
     *
     * @param urlString         上传的地址
     * @param parameters        文件上传预留参数 (可为nil)
     * @param imageAry          上传的图片数组
     * @param width             图片要被压缩到的宽度
     * @param uploadProgressBlock    上传进度
     * @param successBlock      上传成功的回调
     * @param failureBlock      上传失败的回调
     */
    + (void)uploadFileWithURL:(NSString *)urlString parameters:(id)parameters imageAry:(NSArray *)imageAry targetWidth:(CGFloat)width progress:(progress)uploadProgressBlock success:(requestSuccess)successBlock failure:(requestFailure)failureBlock;
    
    
    
    
    
    /**
     *  视频上传
     *
     *  @param operations   上传视频预留参数---视具体情况而定 可移除
     *  @param videoPath    上传视频的本地沙河路径
     *  @param urlString     上传的url
     *  @param successBlock 成功的回调
     *  @param failureBlock 失败的回调
     *  @param progress     上传的进度
     
     整体思路已经清楚,拿到视频资源,先转为mp4,写进沙盒,然后上传,上传成功后删除沙盒中的文件。
     本地拍摄的视频,上传到服务器:
     https://www.cnblogs.com/HJQ2016/p/5962813.html
     */
    + (void)uploadVideoWithOperaitons:(NSDictionary *)operations withVideoPath:(NSString *)videoPath withUrlString:(NSString *)urlString withSuccessBlock:(requestSuccess)successBlock withFailureBlock:(requestFailure)failureBlock withUploadProgress:(progress)progress;
    
    
    
    
    
    /**
     * 取消所有的网络请求
     */
    + (void)cancelAllRequest;
    
    
    
    /**
     * 取消指定的网络请求
     *
     * @param requestMethod     请求方式(GET、POST)
     * @param urlString  请求URL
     */
    + (void)cancelWithRequestMethod:(NSString *)requestMethod parameters:(id)parameters requestUrlString:(NSString *)urlString;
    
     
    @end
    

    参阅和推荐


    Reading


    • 如果在阅读过程中遇到 Error || New ideas,希望你能 issue 我,我会及时补充谢谢。
    • 熬夜写者不易,喜欢可 赞赏 or Star 一波;点击左上角关注 或 『Public:Codeidea』,在 Demo | 文章 | Rss 更新时收到提醒通知,便捷阅读。

    相关文章

      网友评论

        本文标题:iOS 框架注解—「AFNetworking 网络请求」

        本文链接:https://www.haomeiwen.com/subject/njsugttx.html