白水

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

引导


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 功能模块

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
/**
#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 的一些属性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 设置 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 内部进行管理。

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 通过 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 进行各种过程进度监听。

1
2
3
4
5
6
#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. 网络请求开始
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// 发送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监听的属性值发生变化:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
// 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);
}
}
}

收到请求响应:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 收到请求响应
- (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);
}
}

请求完成:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 请求完成
- (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 的一系列配置全部清理掉。

这个是我们的配置过程:

1
2
3
4
5
6
7
8
9
10
11
12
13
// 通过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];
}

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

1
2
3
4
5
6
7
8
9
10
11
// 给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)


请求序列化方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
#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
2
3
4
5
6
7
8
9
10
11
12
13
- 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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
/**
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 文件下载

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
/**
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值的改变。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 使用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 文件上传(图片、视频)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
/**
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 来拼接数据,共有三种方法如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
注解: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 相关代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
#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 框架 来检测网络状态的改变。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
#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下载地址

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
// 需包含 #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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
/**
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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
- (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 请求的封装。

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

单例方法示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**
* 获得全局网络请求实例单例方法
*
* @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,可根据情况进行配置。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
// 重写 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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
#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 网络请求」

作者:白水

链接:http://githubidea.github.io/SourceAnnotations/AFNLibraryUse.html

版权声明: 署名-非商业性使用-禁止演绎 4.0 国际 本博客文章除特别声明外均为原创,如需转载请务必保留原链接(可点击进入的链接)和 作者出处。