在实际工作中,有机会写网络相关的部分代码。经过讨论,大的方向是将iOS传输简化为数据业务(只实现POST),上传业务,下载业务三部分。编程语言是Object-C,最低支持版本为iOS7,第三方库选择AFNetworking3.0。(如果是新项目,编程语言是Swift,最低支持版本为iOS8,第三方库选择Alamofire)
AFNetworking学习
AFNetworking3.0相比以前的版本精简了许多,强烈建议升级。
gitHub上的源代码
官方文档
整体架构
AFURLSessionManager
整个AFNetworking3.0中最核心的一个类。如果想用NSURLSession函数族自己实现网络通讯,可以参考这个类的一些做法。
直接从NSObject继承而来,并没有直接继承NSURLSession。session成为一个属性,用组合代替继承,更容易理解。
多线程采用了NSOperationQueue,这是对GCD的一种对象化封装,使用起来更方便,而且能够很方便地实现顺序依赖,取消等功能。
/**
The managed session.
*/
@property (readonly, nonatomic, strong) NSURLSession *session;
/**
The operation queue on which delegate callbacks are run.
*/
@property (readonly, nonatomic, strong) NSOperationQueue *operationQueue;
- 虽然名字是XXXManager,但是没有采用单例模式。
- (instancetype)initWithSessionConfiguration:(NSURLSessionConfiguration *)configuration {
self = [super init];
if (!self) {
return nil;
}
if (!configuration) {
configuration = [NSURLSessionConfiguration defaultSessionConfiguration];
}
self.sessionConfiguration = configuration;
self.operationQueue = [[NSOperationQueue alloc] init];
self.operationQueue.maxConcurrentOperationCount = 1;
self.session = [NSURLSession sessionWithConfiguration:self.sessionConfiguration delegate:self delegateQueue:self.operationQueue];
self.responseSerializer = [AFJSONResponseSerializer serializer];
self.securityPolicy = [AFSecurityPolicy defaultPolicy];
#if !TARGET_OS_WATCH
self.reachabilityManager = [AFNetworkReachabilityManager sharedManager];
#endif
self.mutableTaskDelegatesKeyedByTaskIdentifier = [[NSMutableDictionary alloc] init];
self.lock = [[NSLock alloc] init];
self.lock.name = AFURLSessionManagerLockName;
[self.session getTasksWithCompletionHandler:^(NSArray *dataTasks, NSArray *uploadTasks, NSArray *downloadTasks) {
for (NSURLSessionDataTask *task in dataTasks) {
[self addDelegateForDataTask:task uploadProgress:nil downloadProgress:nil completionHandler:nil];
}
for (NSURLSessionUploadTask *uploadTask in uploadTasks) {
[self addDelegateForUploadTask:uploadTask progress:nil completionHandler:nil];
}
for (NSURLSessionDownloadTask *downloadTask in downloadTasks) {
[self addDelegateForDownloadTask:downloadTask progress:nil destination:nil completionHandler:nil];
}
}];
return self;
}
- 分为数据、上传、下载三种不同的业务。
/**
Creates an `NSURLSessionDataTask` with the specified request.
@param request The HTTP request for the request.
@param uploadProgressBlock A block object to be executed when the upload progress is updated. Note this block is called on the session queue, not the main queue.
@param downloadProgressBlock A block object to be executed when the download progress is updated. Note this block is called on the session queue, not the main queue.
@param completionHandler A block object to be executed when the task finishes. This block has no return value and takes three arguments: the server response, the response object created by that serializer, and the error that occurred, if any.
*/
- (NSURLSessionDataTask *)dataTaskWithRequest:(NSURLRequest *)request
uploadProgress:(nullable void (^)(NSProgress *uploadProgress))uploadProgressBlock
downloadProgress:(nullable void (^)(NSProgress *downloadProgress))downloadProgressBlock
completionHandler:(nullable void (^)(NSURLResponse *response, id _Nullable responseObject, NSError * _Nullable error))completionHandler;
/**
Creates an `NSURLSessionUploadTask` with the specified request for an HTTP body.
@param request The HTTP request for the request.
@param bodyData A data object containing the HTTP body to be uploaded.
@param uploadProgressBlock A block object to be executed when the upload progress is updated. Note this block is called on the session queue, not the main queue.
@param completionHandler A block object to be executed when the task finishes. This block has no return value and takes three arguments: the server response, the response object created by that serializer, and the error that occurred, if any.
*/
- (NSURLSessionUploadTask *)uploadTaskWithRequest:(NSURLRequest *)request
fromData:(nullable NSData *)bodyData
progress:(nullable void (^)(NSProgress *uploadProgress))uploadProgressBlock
completionHandler:(nullable void (^)(NSURLResponse *response, id _Nullable responseObject, NSError * _Nullable error))completionHandler;
/**
Creates an `NSURLSessionDownloadTask` with the specified request.
@param request The HTTP request for the request.
@param downloadProgressBlock A block object to be executed when the download progress is updated. Note this block is called on the session queue, not the main queue.
@param destination A block object to be executed in order to determine the destination of the downloaded file. This block takes two arguments, the target path & the server response, and returns the desired file URL of the resulting download. The temporary file used during the download will be automatically deleted after being moved to the returned URL.
@param completionHandler A block to be executed when a task finishes. This block has no return value and takes three arguments: the server response, the path of the downloaded file, and the error describing the network or parsing error that occurred, if any.
@warning If using a background `NSURLSessionConfiguration` on iOS, these blocks will be lost when the app is terminated. Background sessions may prefer to use `-setDownloadTaskDidFinishDownloadingBlock:` to specify the URL for saving the downloaded file, rather than the destination block of this method.
*/
- (NSURLSessionDownloadTask *)downloadTaskWithRequest:(NSURLRequest *)request
progress:(nullable void (^)(NSProgress *downloadProgress))downloadProgressBlock
destination:(nullable NSURL * (^)(NSURL *targetPath, NSURLResponse *response))destination
completionHandler:(nullable void (^)(NSURLResponse *response, NSURL * _Nullable filePath, NSError * _Nullable error))completionHandler;
AFHTTPSessionManager
继承自AFURLSessionManager,专门用来实现HTTPS协议,提供了POST、GET、HEAD、DELETE、PUT、PATCH等方便方法。
具体的实现都直接或者间接调用了父类AFURLSessionManager数据业务的方法,下载和上传业务没有涉及
/**
Creates an `NSURLSessionDataTask` with the specified request.
@param request The HTTP request for the request.
@param uploadProgressBlock A block object to be executed when the upload progress is updated. Note this block is called on the session queue, not the main queue.
@param downloadProgressBlock A block object to be executed when the download progress is updated. Note this block is called on the session queue, not the main queue.
@param completionHandler A block object to be executed when the task finishes. This block has no return value and takes three arguments: the server response, the response object created by that serializer, and the error that occurred, if any.
*/
- (NSURLSessionDataTask *)dataTaskWithRequest:(NSURLRequest *)request
uploadProgress:(nullable void (^)(NSProgress *uploadProgress))uploadProgressBlock
downloadProgress:(nullable void (^)(NSProgress *downloadProgress))downloadProgressBlock
completionHandler:(nullable void (^)(NSURLResponse *response, id _Nullable responseObject, NSError * _Nullable error))completionHandler;
- 虽然名字是XXXManager,也提供了工厂方法
+ (instancetype)manager;
,但是并没有使用单例模式。
+ (instancetype)manager {
return [[[self class] alloc] initWithBaseURL:nil];
}
- 通过包装,在父类AFURLSessionManager的基础上隐藏了NSURLRequest的概念,简化为urlString,并且是相对于baseURL的相对路径,会在内部进行拼接,形成一个完整的urlString。
- (NSURLSessionDataTask *)dataTaskWithHTTPMethod:(NSString *)method
URLString:(NSString *)URLString
parameters:(id)parameters
uploadProgress:(nullable void (^)(NSProgress *uploadProgress)) uploadProgress
downloadProgress:(nullable void (^)(NSProgress *downloadProgress)) downloadProgress
success:(void (^)(NSURLSessionDataTask *, id))success
failure:(void (^)(NSURLSessionDataTask *, NSError *))failure
{
NSError *serializationError = nil;
NSMutableURLRequest *request = [self.requestSerializer requestWithMethod:method URLString:[[NSURL URLWithString:URLString relativeToURL:self.baseURL] absoluteString] parameters:parameters error:&serializationError];
if (serializationError) {
if (failure) {
dispatch_async(self.completionQueue ?: dispatch_get_main_queue(), ^{
failure(nil, serializationError);
});
}
return nil;
}
__block NSURLSessionDataTask *dataTask = nil;
dataTask = [self dataTaskWithRequest:request
uploadProgress:uploadProgress
downloadProgress:downloadProgress
completionHandler:^(NSURLResponse * __unused response, id responseObject, NSError *error) {
if (error) {
if (failure) {
failure(dataTask, error);
}
} else {
if (success) {
success(dataTask, responseObject);
}
}
}];
return dataTask;
}
Request和Response
包括AFURLRequestSerialization和AFURLResponseSerialization两个文件。但是这两个是protocol,而不是类。真正的主体是AFHTTPRequestSerializer和AFHTTPResponseSerializer。这里的命名没有采用主体的名字,感觉挺别扭的。
这两个类用在了AFHTTPSessionManager中,在AFURLSession中只用到AFURLResponseSerialization,但是这两个类的命名都是AFURLXXX,不是很好。
根据不同的编码方式,提供了AFJSONXXX,AFXMLXXX等子类。在默认的实现中,response采用了JSON的方式,而request不是。这也不是很好。
- (instancetype)initWithBaseURL:(NSURL *)url
sessionConfiguration:(NSURLSessionConfiguration *)configuration
{
self = [super initWithSessionConfiguration:configuration];
if (!self) {
return nil;
}
// Ensure terminal slash for baseURL path, so that NSURL +URLWithString:relativeToURL: works as expected
if ([[url path] length] > 0 && ![[url absoluteString] hasSuffix:@"/"]) {
url = [url URLByAppendingPathComponent:@""];
}
self.baseURL = url;
self.requestSerializer = [AFHTTPRequestSerializer serializer];
self.responseSerializer = [AFJSONResponseSerializer serializer];
return self;
}
- 设置http头部信息
/**
Sets the value for the HTTP headers set in request objects made by the HTTP client. If `nil`, removes the existing value for that header.
@param field The HTTP header to set a default value for
@param value The value set as default for the specified header, or `nil`
*/
- (void)setValue:(nullable NSString *)value
forHTTPHeaderField:(NSString *)field;
AFSecurityPolicy
- 分为三个安全等级
typedef NS_ENUM(NSUInteger, AFSSLPinningMode) {
AFSSLPinningModeNone,
AFSSLPinningModePublicKey,
AFSSLPinningModeCertificate,
};
默认是AFSSLPinningModeNone,一般要防止中间人攻击,可以选用第三种AFSSLPinningModeCertificate。不过客户端也要放证书文件(.cer),这个要跟后台统一考虑。
不是单例模式
+ (instancetype)defaultPolicy {
AFSecurityPolicy *securityPolicy = [[self alloc] init];
securityPolicy.SSLPinningMode = AFSSLPinningModeNone;
return securityPolicy;
}
AFNetworkReachabilityManager
- 通过属性可以知道当前的网络状态。
/**
Whether or not the network is currently reachable.
*/
@property (readonly, nonatomic, assign, getter = isReachable) BOOL reachable;
/**
Whether or not the network is currently reachable via WWAN.
*/
@property (readonly, nonatomic, assign, getter = isReachableViaWWAN) BOOL reachableViaWWAN;
/**
Whether or not the network is currently reachable via WiFi.
*/
@property (readonly, nonatomic, assign, getter = isReachableViaWiFi) BOOL reachableViaWiFi;
- 通过block来监控网络状况。可以打开或者关闭监控功能
/**
Starts monitoring for changes in network reachability status.
*/
- (void)startMonitoring;
/**
Stops monitoring for changes in network reachability status.
*/
- (void)stopMonitoring;
///---------------------------------------------------
/// @name Setting Network Reachability Change Callback
///---------------------------------------------------
/**
Sets a callback to be executed when the network availability of the `baseURL` host changes.
@param block A block object to be executed when the network availability of the `baseURL` host changes.. This block has no return value and takes a single argument which represents the various reachability states from the device to the `baseURL`.
*/
- (void)setReachabilityStatusChangeBlock:(nullable void (^)(AFNetworkReachabilityStatus status))block;
- 默认监控本地状况,已经兼容IPv6。
+ (instancetype)manager
{
#if (defined(__IPHONE_OS_VERSION_MIN_REQUIRED) && __IPHONE_OS_VERSION_MIN_REQUIRED >= 90000) || (defined(__MAC_OS_X_VERSION_MIN_REQUIRED) && __MAC_OS_X_VERSION_MIN_REQUIRED >= 101100)
struct sockaddr_in6 address;
bzero(&address, sizeof(address));
address.sin6_len = sizeof(address);
address.sin6_family = AF_INET6;
#else
struct sockaddr_in address;
bzero(&address, sizeof(address));
address.sin_len = sizeof(address);
address.sin_family = AF_INET;
#endif
return [self managerForAddress:&address];
}
- 采用单例模式
+ (instancetype)sharedManager {
static AFNetworkReachabilityManager *_sharedManager = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
_sharedManager = [self manager];
});
return _sharedManager;
}
UIKit+AFNetworking
通过类别给UIKit添加网络功能,这在当时是比较推荐的做法
随着APP规模的增大,现在强调界面、逻辑、数据分层,降低耦合度。所以给UIKit组件添加网络功能的做法就不推荐了。
图片下载和缓存可以用,不过更有名的第三方库是SDWebImage
其他参考
AFNetworking 概述(一)
AFNetworking 的核心 AFURLSessionManager(二)
处理请求和响应 AFURLSerialization(三)
AFNetworkReachabilityManager 监控网络状态(四)
验证 HTTPS 请求的证书(五)
架构设计
ZADataTask
数据业务
直接调用AFNetworking的AFHTTPSessionManager实现
参数只有url,parameter;只用POST方式,放弃GET方式。
request部分也用JSON解析
self.sessionManager.requestSerializer = [AFJSONRequestSerializer serializer];
- 安全性再包一层,隐藏AFNetworking的相关内容
typedef NS_ENUM(NSInteger , ZASecurityLevel) {
ZASecurityLevelNormal, // AFSSLPinningModeNone
ZASecurityLevelMiddle, // AFSSLPinningModePublicKey
ZASecurityLevelHigh, // AFSSLPinningModeCertificate
};
ZAUploadTask
上传业务
直接调用AFNetworking的AFURLSessionManager实现
放弃用POST实现上传的方式,采用更通用的NSURLSessionUploadTask。
ZADownloadTask
上传业务
直接调用AFNetworking的AFURLSessionManager实现
ZANetworkConfig
单例
一些可供配置的信息,比如数据业务的baseUrl
数据,上传,下载业务提供不同的baseUrl,这三者可以为不同的服务器
// server base url
@property (nonatomic, copy) NSString *dataTaskBaseUrl;
@property (nonatomic, copy) NSString *uploadTaskBaseUrl;
@property (nonatomic, copy) NSString *downloadTaskBaseUrl;
ZANetworkManager
单例
管理所有的task
// task array
@property (nonatomic, strong) NSMutableArray *dataTaskArray;
@property (nonatomic, strong) NSMutableArray *uploadTaskArray;
@property (nonatomic, strong) NSMutableArray *downloadTaskArray;
ZANetworkReachbility
- 监控网络状况,创建本地,数据服务器,上传服务器,下载服务器4个不同的监控对象。
// reachability
self.localReachability = [AFNetworkReachabilityManager sharedManager];
self.dataTaskServerReachability = [AFNetworkReachabilityManager managerForDomain:self.config.dataTaskBaseUrl];
self.uploadTaskServerReachability = [AFNetworkReachabilityManager managerForDomain:self.config.uploadTaskBaseUrl];
self.downloadTaskServerReachability = [AFNetworkReachabilityManager managerForDomain:self.config.downloadTaskBaseUrl];
[self.localReachability startMonitoring];
[self.dataTaskServerReachability startMonitoring];
[self.uploadTaskServerReachability startMonitoring];
[self.downloadTaskServerReachability startMonitoring];
- 虽然AFURLSessionManager有属性reachabilityManager,大致也可以用,不过不是非常好。数据、上传、下载三种业务都不是单例,在没有这些实例存在的情况下,属性是无效的。所以这里考虑将监控网络状况和具体的网络业务分开,用一个单例来完成网络监控功能,同时也不用当心实例的生存期问题,同时还可以将本地,数据服务器,上传服务器,下载服务器同时进行监控。
过程笔记
用属性还是成员变量?
用属性,不要用成员变量,带_的一般是内部变量,不要轻易用。
虽然有时候改错时XCode提的意见是带_的成员变量,不过还是建议用属性。
在init函数中也照样用属性,不要用带_的成员变量。AFNetworking就是这么做的。
对头文件中暴露的属性,readonly的怎么办(用属性会报错)?AFNetworking的对策是在内部类扩展中提供一个同名的readwrite属性。
头文件AFHTTPSessionManager.h中的只读声明:
@property (readonly, nonatomic, strong, nullable) NSURL *baseURL;
源文件AFHTTPSessionManager.m中的同名私有属性:
@interface AFHTTPSessionManager ()
@property (readwrite, nonatomic, strong) NSURL *baseURL;
@end
getter和setter一般情况都不要写,用系统默认的
在getter或者setter函数中做额外的事情,这种做法虽然可以,但是并不是一个好的习惯。这个时候,应该思考,把这项功能当做一个属性是不是合适?在大多数情况下,用一个方法来替代这个对外暴露的属性是更好的选择。
怎么取消宏定义?
Object-C、C语言开发,#define是很常见的一种做法。不过Swift都不提供宏定义功能,所以考虑不用宏定义
一般来说,都是推荐用const定义的变量来取代宏定义;但是由于作用域问题,远不如宏定义来得方便。这种方法基本不可取。
一般来说,需要限制全局变量的使用,其副作用甚至比宏定义更大。取代全局变量的一个方法是单例。
同理,用单例来取代宏定义,应该是可行的。ZANetworkConstant就是这样的一个单例,用只读属性来保证不被修改。
@interface ZANetworkConstant : NSObject
+ (instancetype)sharedInstance;
// server base url
@property (nonatomic, readonly, copy) NSString *defaulDataTaskBaseUrl;
@property (nonatomic, readonly, copy) NSString *defaulUploadTaskBaseUrl;
@property (nonatomic, readonly, copy) NSString *defaulDownloadTaskBaseUrl;
@end
错误信息处理
Object-C编程,一般网络错误用NSError来表示。一般按照下面的简单方法使用就好了
- code: 用来放自定义的error code
- domain:自定义的error domain,比如“com.company.app”
- uerInfo:NSLocalizedDescriptionKey对应的error message
统一头文件
将调用者需要包含的头文件归总在一个文件中是一个不错的方法
AFNetworking, UIKit等都是这么做的
#ifndef ZANetwork_h
#define ZANetwork_h
/**
* 自定义的类型
*/
#import "ZANetworkType.h"
/**
* 公共配置单例
*/
#import "ZANetworkConfig.h"
/**
* 数据业务
*/
#import "ZADataTask.h"
#endif /* ZANetwork_h */
相对url还是绝对url?
在实际使用中,数据业务占80%以上的使用场景。一般分为生存,验收,测试三个不同服务器,使正式版,试用版,开发版之间不会相互影响。所以,数据业务的url一般分为baseUrl和相对url两部分,方便切换服务器地址。
上传和下载的url一般都是通过数据业务,从数据服务器返回的,很多情况下是绝对url。所以对于上传和下载,并没有将url分为baseUrl和相对url的需求。
在很多时候,数据、上传、下载三种业务共用一个服务器。切换时,服务器在拼接上传、下载业务的绝对url时,直接使用数据业务的baseUrl就可以了。对服务端编码来说,也没有任何变化。
如果上传、下载业务跟数据业务的服务器不是同一个,并且也要根据生产、验证、测试等场景切换服务器,那么现在大多数的url都要改为baseUrl+相对url的形式。现实中,这个问题只是被掩藏了而已。
AFHTTPSessionManager提供了baseUrl属性,而AFURLSessionManager没有提供baseUrl属性。
AFHTTPSessionManager和AFURLSessionManager初始化函数都是调用了NSURLSession的初始化函数。而NSURLSession初始化是不需要url参数的。虽然AFHTTPSessionManager的baseUrl参数在初始化的时候传入,但是并没有在初始化的时候使用,只是保存在一个私有属性中。
AFHTTPSessionManager用到baseUrl的时候是在形成NSURLRequest的时候,将baseUrl和数据业务函数传入的url进行拼接,形成一个总的url,构造NSURLRequest,然后使用。各种Method的函数都是调用以下函数实现,这个函数中有拼接url的代码
- (NSURLSessionDataTask *)dataTaskWithHTTPMethod:(NSString *)method
URLString:(NSString *)URLString
parameters:(id)parameters
success:(void (^)(NSURLSessionDataTask *, id))success
failure:(void (^)(NSURLSessionDataTask *, NSError *))failure
{
NSError *serializationError = nil;
NSMutableURLRequest *request = [self.requestSerializer requestWithMethod:method URLString:[[NSURL URLWithString:URLString relativeToURL:self.baseURL] absoluteString] parameters:parameters error:&serializationError];
if (serializationError) {
if (failure) {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wgnu"
dispatch_async(self.completionQueue ?: dispatch_get_main_queue(), ^{
failure(nil, serializationError);
});
#pragma clang diagnostic pop
}
return nil;
}
__block NSURLSessionDataTask *dataTask = nil;
dataTask = [self dataTaskWithRequest:request completionHandler:^(NSURLResponse * __unused response, id responseObject, NSError *error) {
if (error) {
if (failure) {
failure(dataTask, error);
}
} else {
if (success) {
success(dataTask, responseObject);
}
}
}];
return dataTask;
}
AFURLSessionManager的数据、上传、下载业务相关的函数,都是直接使用NSURLRequest作为参数的,要求的是绝对url。
为了切换服务器的灵活性和整体一致性,上传、下载业务也采用baseUrl和相对url组合的方式,进行拼接,形成最终的url,生成NSURLRequest,调用AFURLSessionManager的相关函数实现。
至于上传、下载服务器和数据业务共用一个的情况,只要将他们的baseUrl设成一样就可以了。
当然,这就要求服务在返回上传、下载服务的url时,不要拼接,只要返回相对的url就可以了。
如果服务端一定要返回完整的绝对url,怎么办呢?一种方法是将上传、下载的baseUrl设为空字符串@“”;另一种方法是通过判断前缀@“http”(兼容HTTPS)识别绝对url,不拼接,直接用就好了。这里采用第二种方法。