iOS网络层设计

网络层设计

iOS项目架构

iOS的项目架构我一般设置为三层,即UI层、服务层、数据层,以及第三方依赖层。

  • UI层:主要管理项目中的所有UI界面,根据模块进行划分,如:登录模块,首页模块,搜索模块等等,还包括通用控件模块。
  • 服务层:基于各模块创建的服务于各模块的服务类,各模块的服务类之间相互独立,如果存在相互调用的情况,就将此抽象出来,设置到数据层中。UI层只负责UI的渲染和交互,数据之间的逻辑交由其对应的服务层处理。
  • 数据层:数据层提供基于整个项目最基础的一些工具,如网络基础工具,数据存取工具等等,一般在服务层中会针对各模块的实际需求进行二次封装和优化。

以上,数据层作为最基础的工具被各模块的服务层引用,各模块被对应的UI层应用,服务层之间和UI层之间相互独立,尽量解耦。

网络工具

了解了基本的项目架构后,就来详细说说数据层中的网络基础工具层的封装和服务层中网络工具使用,以及项目实际开发过程中遇到的难点及解决方法。

封装基础网络工具

iOS项目一般都会依赖AFNetworking网络库。
基础网络工具一般封装为单例模式,并根据项目需求,提供最基础的网络方法:GET、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
#import <Foundation/Foundation.h>
#import <AFNetworking/AFNetworking.h>

/** 给子类使用的回调 */
typedef void(^netSuccessBlock)(id response);
typedef void(^netFailureBlock)(NSError *error);

@interface ZORNetworking : NSObject

/** 成功回调 */
@property (nonatomic, copy) netSuccessBlock netSuccessBlock;
/** 失败回调 */
@property (nonatomic, copy) netFailureBlock netFailureBlock;

/** 创建单例对象 */
+ (instancetype)shareNetwork;

/** 销毁单例对象 */
+ (void)destoryNetwork;

/** 添加请求头 */
- (void)addHeaderWithToken:(NSString *)token;

/** 监听网络状态 */
- (void)startMonitoringNetworkStatus;

/** 通知监听网络状态 */
- (void)stopMonitoringNetworkStatus;

/** GET方法 */
- (void)GET:(NSString *)URLString parameters:(id)parameters success:(void (^)(NSURLSessionDataTask *task, id responseObject))success
failure:(void (^)(NSURLSessionDataTask *task, NSError *error))failure;

/** POST方法 */
- (void)POST:(NSString *)URLString parameters:(id)parameters success:(void (^)(NSURLSessionDataTask *task, id responseObject))success
failure:(void (^)(NSURLSessionDataTask *task, NSError *error))failure;

@end

BaseUrl设置

网络请求工具中只要切换BaseURL就能自由访问不同的服务器,AFN提供了创建BaseURL的方法,在创建单例的时候自由创建。
初始化设置的时候根据后台的设置,进行AFN请求数据格式和接受数据格式的设置,AFN提供了两个类进行设置,self.manager.requestSerializer = [AFHTTPRequestSerializer serializer];, self.manager.responseSerializer = [AFHTTPResponseSerializer serializer];
具体参考AFHTTPResponseSerializer, AFHTTPRequestSerializer这两个类,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
+ (instancetype)shareNetwork {
static ZORNetworking *network = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
network = [[ZORNetworking alloc] init];
});
return network;
}

// 初始化方法
- (instancetype)init {
if (self = [super init]) {
self.manager = [[AFHTTPSessionManager alloc] initWithBaseURL:[NSURL URLWithString:BASE_URL]];
/** 请求设置 */
// 超时时间
self.manager.requestSerializer.timeoutInterval = 45.0;

/** 接收设置 */
// 返回值格式设置
self.manager.responseSerializer = [AFJSONResponseSerializer serializer];
self.manager.responseSerializer.acceptableContentTypes = [NSSet setWithObjects:@"application/json", @"text/json", @"text/javascript", @"text/html", nil];
}
return self;
}

提供销毁单例的方法

提供销毁单例的方法是因为,想在登录的时候自由切换请求的IP地址,即BaseURL,退出登录后重新登录网络请求的单例类仍然存在,并不会重新创建,所以无法在不进行commond+R的情况下自由切换。
提供销毁单例的方法后,就可以自由的切换BaseURL,切换后再次调用网络方法,将会创建一个新的网络单例类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 将静态变量设置为全局可访问
static ZORNetworking *network = nil;
static dispatch_once_t onceToken;

@implementation ZORNetworking

+ (instancetype)shareNetwork {
dispatch_once(&onceToken, ^{
network = [[ZORNetworking alloc] init];
});
return network;
}

+ (void)destoryNetwork {
// dispatch_once_t 为 long 类型
onceToken = 0l;
network = nil;
}

@end

创建基础的请求方法

以上基础设置完成后,就可以创建基础的请求方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
- (void)GET:(NSString *)URLString parameters:(id)parameters success:(void (^)(NSURLSessionDataTask *, id))success failure:(void (^)(NSURLSessionDataTask *, NSError *))failure {
[self.manager GET:URLString parameters:parameters progress:nil success:^(NSURLSessionDataTask * _Nonnull task, id _Nullable responseObject) {
if (success) {
success(task, responseObject);
}
} failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) {
if (failure) {
failure(task, error);
}
}];
}

- (void)POST:(NSString *)URLString parameters:(id)parameters success:(void (^)(NSURLSessionDataTask *, id))success failure:(void (^)(NSURLSessionDataTask *, NSError *))failure {
[self.manager POST:URLString parameters:parameters progress:nil success:^(NSURLSessionDataTask * _Nonnull task, id _Nullable responseObject) {
if (success) {
success(task, responseObject);
}
} failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) {
if (failure) {
failure(task, error);
}
}];
}

网络监控方法的创建和调用

AFN提供了监控网络状态的方法,可以直接调用使用,为了实时获取网络的状态,一般在APP的启动方法里调用,但是如果网络状态的枚举类型定义在了网络工具单例类中,并定义相关的枚举属性,虽然可以实时监测到,但是通过状态属性并不能实时获取到。需要创建新的网络状态的单例类来获取,在需要检测网络状态的地方调用即可。
检测网络状态方法如下:

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
- (void)startMonitoringNetworkStatus {
[[self.manager reachabilityManager] setReachabilityStatusChangeBlock:^(AFNetworkReachabilityStatus status) {
switch (status) {
case AFNetworkReachabilityStatusReachableViaWiFi: {
[ZORNetworkStatus shareNetStatus].networkStatus = NetworkStatusViaWiFi;
DLog(@"wifi");
break;
}
case AFNetworkReachabilityStatusReachableViaWWAN: {
[ZORNetworkStatus shareNetStatus].networkStatus = NetworkStatusViaWWAN;
DLog(@"移动网络");
break;
}
case AFNetworkReachabilityStatusNotReachable: {
[ZORNetworkStatus shareNetStatus].networkStatus = NetworkStatusNotReachable;
DLog(@"无网络");
break;
}
case AFNetworkReachabilityStatusUnknown: {
[ZORNetworkStatus shareNetStatus].networkStatus = NetworkStatusUnKnown;
DLog(@"未知网络");
break;
}
default:
break;
}
}];
[[self.manager reachabilityManager] startMonitoring];
}

网络服务层

通过继承或分类创建进一步解析后的网络服务层,在这层中根据返回的结果设置自定义的提示内容,如加载动画,加载完成提示,网络不佳提示,无数据提示,错误提示等等。

基础服务

创建基础网络服务,通过继承或分类进行创建,根据产品的设计,进行不同情况的一系列提示。
其中NSError为自定义的error

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
- (void)POST:(NSString *)URL params:(id)params success:(void (^)(id))success failure:(void (^)(NSError *))failure {
// 加载动画设置
// code

[self POST:URL parameters:params success:^(NSURLSessionDataTask *task, id responseObject) {
// 隐藏加载动画
// code

// 根据后台的通用数据结构,解析返回结果,
NSDictionary *response = responseObject;

// 结果中可能带有code和message,解析后进行不同的UI设置

// code解析
NSInteger ok = [response[@"code"] integerValue];
// 获取后台返回的消息
NSString *msg = response[@"message"];

if (成功) { // 有数据
// 解析出数据后返回,一般为json字符串
// code
if (success) {
success(data);
}
} else { // 失败
// 不同失败结果的UI设置
// 无数据设置等等
// code

if (failure) {
// 自定义error对象并回调
NSError *error = [NSError errorWithDomain:NSURLErrorDomain code:NetErrorTypeBackData userInfo:@{NSLocalizedDescriptionKey : msg}];
failure(error);
}
}
} failure:^(NSURLSessionDataTask *task, NSError *error) {
// 隐藏加载动画
// code

// 解析error
// 根据error的code,和后台的说明进行设置
// 1、请求失败设置
// code

// 2、请求超时设置
// code

...
// 返回的error,可以直接返回,也可以进行自定义后返回
// code
if (error) {
if (failure) {
failure(error);
}
}
}];
}

各模块的网络服务层

针对不同的模块,创建不同模块的网络服务层,通过分类创建。
给模块中所有需要用到网络请求的action都创建一个对应的方法,方便区分和维护。
在这一层中,尽量将网络请求的URL、参数包装、解析后的数据等操作都进行处理,UI层只管调用方法,调用成功后直接拿到需要的数据进行UI的渲染。
通过这种设计最大程度的对ViewController进行瘦身。

AFN单独传递字符串参数的设置

项目中用到DES加密,后台要求将加密后的字符串密文直接传递过去,在以往的经验中,所有的参数都是通过NSDictionary进行传递的。直接改变参数为字符串传递,返回错误的结果。
结果调试,后台一直报的错误是格式错误。
经过查找资料,和对HTTP请求的理解有关,AFN默认的请求是键值形式的字符串,即application/x-www-form-urlencoded。
AFN内部会将接收到的参数自动按照键值的形式进行组织后传递,所有会看到错误为:input string '=%' ,类似这种错误。
遇到这种问题可以尝试下面两种方法进行解决。

  • 解决方法1
    传递的参数为纯文本形式的字符串,设置请求头为[self.manager.requestSerializer setValue:@"text/plain" forHTTPHeaderField:@"Content-Type"];
  • 解决方法2
    AFN提供了requestSerializer的扩展方法,直接创建一个自定义的requestSerializer,就不需要通过init创建requestSerializer了,AFN就不会将参数按照键值的形式传递了。
    也有可能还需要设置请求头,具体根据后台进行设置。
    1
    2
    3
    4
    // 自定义requestSerializer
    [self.manager.requestSerializer setQueryStringSerializationWithBlock:^NSString * _Nonnull(NSURLRequest * _Nonnull request, id _Nonnull parameters, NSError * _Nullable __autoreleasing * _Nullable error) {
    return parameters;
    }];