×

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

96
白水ln
2017.03.11 04:12* 字数 3042

引导


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 更新时收到提醒通知,便捷阅读。
GitHub
Web note ad 1