网络管理单例
在仿写天气预报项目的时候,需要多次创建网络请求,例如首页展示收藏城市的天气状况, 在搜索的时候通过输入的字符进行搜索,获取城市名称, 经纬度以及所在省份等信息, 还有在搜索后点击搜索结果查看具体城市的天气状况
那么在这么多地方都去创建网络请求的话, 就会显得很繁琐,代码的结构都是一样的,于是就可以想到用一个类将网络请求封装起来, 在需要的时候直接调用就可以创建
这里笔者根据项目要求,创建了网络管理单例 (NetworkManager), 在单例中实现了创建网络请求以及取消网洛请求的方法
网络管理单例实现了以下功能:
- 统一封装网络请求
- 提供了
GET:parameters:completion:方法,内部自动处理 URL 参数拼接、URL 编码、创建NSURLRequest、启动NSURLSessionDataTask。 - 统一设置超时时间, 会话配置, 避免在每个控制器中重复编写网络请求代码
-
请求取消以及生命周期管理
- 通过 runningTasks 保存所有正在进行的请求任务
- 提供了
cancelRequestForURL:和cancelAllRequest方法, 允许在发起新的请求的时候取消旧的请求, 防止- 过时数据回显 (搜索时输入变化, 旧的请求结果返回后覆盖新的返回结果)
- 网络资源的浪费和内存泄漏
- 界面刷新错乱 (在首页刷新的时候, 取消正在进行的旧请求, 确保只处理最新相应)
-
线程安全
- 使用串行队列 syneQueue 保证runningTasks 的读写操作, 确保在多线程环境下 (如多个网络回调时修改字典) ,不会出现崩溃和数据竞争
-
集中配置与管理
- 可在单例中统一修改超时时间, 缓存策略, 请求头等, 方便全局调优, 无需逐个控制器修改
在.h文件中声明方法
objc
@interface NetworkManager : NSObject
+ (instancetype) sharedManager;
// 参数1: 请求地址
// 参数2: 查询参数, 键值对字典, 会自动拼接到url后
// 参数3: 长Block回调
// 没有返回值
// 本身可以传 nil
// 参数1: 返回的 JSON 回调 请求失败 json = nil
// 参数2: 请求错误对象 请求成功 error = nil
//
- (void) GET: (NSString*) urlString
parameters: (NSDictionary* _Nullable) parameters
completion: (void (^ _Nullable)(NSDictionary* _Nullable json, NSError* _Nullable error)) completion;
// 取消所有请求
- (void) cancelAllRequests;
// 取消特定 URL 请求
- (void) cancelRequestForURL: (NSString*) urlString;
@end
创建对外的方法
objc
/// 全局复用的 URLSession(配置统一超时时间)
@property (nonatomic, strong) NSURLSession *session;
/// 存储所有正在运行的数据任务,Key 为完整 URL 字符串,Value 为 NSURLSessionDataTask
/// 该字典可能被多线程同时访问,必须通过串行队列保护。
@property (nonatomic, strong) NSMutableDictionary<NSString *, NSURLSessionDataTask *> *runningTasks;
/// 保护 runningTasks 的串行队列(保证字典读写线程安全)
@property (nonatomic, strong) dispatch_queue_t syncQueue;
@end
声明全局复用的URLSession 会话对象, 一个任务字典和一个行串队列
这里的session是一个全局复用的会话对象, 负责管理所有网络任务的生命周期, 包括建立 TCP 连接、发送 HTTP 请求头、接收服务器返回的二进制数据流、处理 HTTP/2 多路复用等。
如果不复用, 每发起一个新的请求就会创建一个新的NSURLSession, 会消耗大量的系统资源
复用时, 系统底层会共用同一个连接池 (同一台服务器的多个请求可以复用同一个TCP连接), 显著提高网络速度和降低延迟
runningTasks 是声明的任务字典, 用来存储当前正在进行但是尚未结束的所有网络任务 (NSURLSessionDataTask 对象)
存储: key = 完整的 URL字符串, Value = 任务对象 (NSURLSessionDataTask*)
@property (nonatomic, strong) dispatch_queue_t syncQueue; ------ 线程保护锁
这是一个 GCD(Grand Central Dispatch)串行队列 , 专门用于保护 runningTasks 字典的读写安全。
NSMutableDictionary 不是线程安全的 。 session 的回调闭包(completionHandler)通常运行在后台子线程 ,而外部调用 GET 方法可能来自主线程。如果主线程在写字典(存入 task),同时后台线程也在写字典(移除 task),就会导致内存错误(野指针访问)或数据错乱,App 直接崩溃。
在代码中,所有访问 runningTasks 的地方(存入、取出、移除)都被强制包裹在 dispatch_sync(self.syncQueue, ^{ ... }) 里面。因为 syncQueue 是串行的,GCD 会确保任何时候只有一个 Block 在执行。当主线程的"存入"任务正在排队时,后台线程的"移除"任务必须乖乖等在门外,等"存入"彻底完成后才能进来执行。这就把"多线程并行操作"强制降级为"单线程串行操作",从根本上杜绝了崩溃风险。
首先实现单例的创建方法
objc
#pragma mark - 单例创建
+ (instancetype) sharedManager {
static dispatch_once_t onceToken;
static NetworkManager* instance = nil;
dispatch_once(&onceToken, ^{
instance = [[super allocWithZone: nil] init];
});
return instance;
}
+ (instancetype) allocWithZone: (struct _NSZone*) zone {
return [self sharedManager];
}
- (instancetype)init {
self = [super init];
if (self) {
// 保证代码只执行一次
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
NSURLSessionConfiguration *config = [NSURLSessionConfiguration defaultSessionConfiguration];
config.timeoutIntervalForRequest = 15.0;
self.session = [NSURLSession sessionWithConfiguration:config];
self.runningTasks = [NSMutableDictionary dictionary];
// 创建串行任务队列
// 参数1: 队列的名字
// 参数2: 串行属性
self.syncQueue =dispatch_queue_create("com.weather.sync",DISPATCH_QUEUE_SERIAL);
});
}
return self;
}
- (id) copyWithZone: (NSZone*) zone {
return self;
}
- (id) mutableCopyWithZone: (NSZone*) zone {
return self;
}
+ (instancetype)sharedManager 方法是单例的全局访问入口, 在不同的文件也可以通过这个方法获取全局单例,
在初始化方法init 中, 配置了网络会话, 任务字典,以及一个串行队列,
任务字典是一个 NSMutableDictionary, 其核心作用是 缓存和管理的当前正在进行中的网络请求任务
通常 Key 是请求的唯一标识(如 URL、请求 ID 或
NSURLSessionTask的taskIdentifier),Value 是请求的回调闭包(Block)或自定义的上下文模型。这里存储的是Task (NSURLSessionDataTask*)
- 实际用途 :
- 任务追踪 :当
NSURLSession的代理方法(如didCompleteWithError)回调时,需要通过taskIdentifier从字典里取出对应的 Block 来执行,并把结果返回给调用者。- 避免重复请求:在发起新请求前,先查字典。如果同一个请求(比如同一个 URL)已经在跑,就直接复用或取消旧任务,防止重复网络开销。
- 取消操作 :当用户离开页面时,可以根据 ID 从字典里取出正在运行的任务,调用
[task cancel]。
串行队列是一个由 GCD 创建的DISPATCH_QUEUE_SERIAL(串行队列)。它的核心作用是 将所有的"读写字典"操作,强制按顺序排队执行。队列中的任务一个接一个执行,前一个没执行完,后一个绝不开始。这天然保证了互斥
创建网络请求
objc
- (void) GET:(NSString *)urlString parameters:(NSDictionary *)parameters completion:(void (^)(NSDictionary * _Nullable, NSError * _Nullable))completion
参数说明
(NSString *)urlString
- 作用 :服务器的基础地址(不含参数)。
- 调用方传入什么 :传入接口的固定路径部分,例如
@"https://api.weather.com/v1/beijing"。- 代码中的处理 :作为拼接
fullUrl的起点。代码默认它不带?和查询参数 ,直接在后面追加?和拼接后的parameters。
(NSDictionary *)parameters
- 作用 :GET 请求的查询参数(键值对)。
- 调用方传入什么 :传入一个字典,例如
@{@"key": @"123", @"lang": @"zh"}。- **代码中的处理 **
- 判断字典是否存在且不为空。
- 遍历所有键值对,将每个
value转为字符串。- 对
value进行 URL 编码(处理中文、空格等特殊字符)。- 拼装成
key=value形式的数组,再用&连接成最终参数字符串。- 拼接到
urlString后面,生成完整的fullUrl(形如https://api.weather.com/v1/beijing?key=123&lang=zh)。- 特殊值 :如果传入
nil或空字典,则直接跳过拼接,fullUrl就是urlString本身。
(void (^)(NSDictionary * _Nullable, NSError * _Nullable))completion_NULLable : 参数可以传 nil
- 类型 :一个无返回值(
void)、接收两个参数的 Block。- 作用 :请求结束后的"回调"。调用方通过这个 Block,告诉你的网络层:"等数据回来了,或者出错了,请执行我这段代码来处理结果"。
- 两个内部参数解读 :
NSDictionary * _Nullable:成功时返回的 JSON 数据(已解析成字典)。NSError * _Nullable:失败时返回的错误信息。_Nullable的含义 :告诉调用方,这两个参数可以传nil。通常成功时error为nil,失败时json为nil(两者互斥)。- 代码中的处理 :
- 在请求成功的逻辑里,解析出
json,通过completion(json, nil)回传。- 在请求失败(网络错误、状态码异常、解析失败)的逻辑里,通过
completion(nil, error)回传。- 通过
dispatch_async(dispatch_get_main_queue())强制回到主线程执行该 Block,方便调用方直接更新 UI。
参数拼接URL编码
objc
NSMutableString* fullUrl = [NSMutableString stringWithString: urlString];
if (parameters && parameters.count > 0) {
[fullUrl appendString: @"?"];
NSMutableArray* pairs = [NSMutableArray array];
for (NSString* key in parameters.allKeys) {
id value = parameters[key];
NSString* valueStr = [NSString stringWithFormat: @"%@", value];
// URL编码
NSString* encodeValue = [valueStr stringByAddingPercentEncodingWithAllowedCharacters: [NSCharacterSet URLQueryAllowedCharacterSet]];
[pairs addObject: [NSString stringWithFormat: @"%@=%@", key, encodeValue]];
}
// 把所有的元素用指定分隔符粘成一个完整的字符串
[fullUrl appendString: [pairs componentsJoinedByString: @"&"]];
}
GET请求的参数是直接跟在 URL问号后面的, 并且参数之间用&连接, 如果参数数值中包含中文或者特殊文字, 就会破坏 URL 的结构
stringByAddingPerrcentEncodingWithAllowedChatacters 会将这些字符转换为%E5%BC%A0 这样的百分号编码,保证传输无歧义。
objc
// 取消相同的 URL 的旧请求
[self cancelRequestForURL: fullUrl];
objc
// 取消正在进行的请求
- (void)cancelRequestForURL:(nonnull NSString *)urlString {
// dispatch_sync: GCD 同步派发函数
// 将后面的任务(Block) 提交到指定的对列, 并阻塞当前的线程, 直到 Block 执行完毕才继续往下走
//参数1: 指定任务要在哪一个队列上进行
// 所有对可变字典 self.runningTasks 的访问(增、删、改、查)都必须排着队在这个队列上执行,从而避免多线程同时操作 NSMutableDictionary 导致的崩溃或数据错乱
dispatch_sync(self.syncQueue, ^{
// 取出对应的任务对象
NSURLSessionDataTask* task = self.runningTasks[urlString];
// 判断任务是否正在传输
if (task && task.state == NSURLSessionTaskStateRunning) {
// 修改对象的内部状态, 标记为取消状态
[task cancel];
}
// 无论任务是否存在,都从字典移除 (不会取消底层的网络任务)
[self.runningTasks removeObjectForKey: urlString];
});
}
如果一个请求返回, 在任务字典中查找, 如果已经存在相同任务, 就取消旧的任务,如果这个任务正在运行, 就调用task cancel 将 task.state 从 NSURLSessionTaskStateRunning 变为 NSURLSessionTaskStateCanceling,最终变为 NSURLSessionTaskStateCompleted。最后从任务字典中移除旧的任务
objc
// 创建URLRequest
NSURL* url = [NSURL URLWithString: fullUrl];
NSMutableURLRequest* request = [NSMutableURLRequest requestWithURL: url];
// HTTP方法
request.HTTPMethod = @"GET";
// 时间阈值
request.timeoutInterval = 15;
将完整的URL字符串转换为 NSURL对象, 这是系统底层网络库的标准输入格式,
NSMutableURLRequest 指定这是一恶搞可变的网络请求, 可以设置它的各种属性 , 这里设置了它的 HTTP 方法和 超时时间, 如果在15 秒内TCP没有建立成功, 或者服务器没有返回完整的数据, 系统会自动取消这个任务
objc
// 创建DataTask
// __weak 弱引用
// typeof(self) 在编译时自动推导self的具体类名
// 将self的弱引用赋值给变量 weakSelf
__weak typeof(self) weakSelf = self;
NSURLSessionDataTask* task = [self.session dataTaskWithRequest: request completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
}
-
__weak:这是一个编译器修饰符,表示 弱引用 。它不会增加对象的引用计数。当被引用的对象(self)被释放时,这个指针会自动被置为nil,不会产生野指针。 -
typeof(self):是编译器提供的一个 类型推断运算符 。它在编译时自动推导出self的具体类型(在这里是NetworkManager *)。用typeof而不是直接写NetworkManager *的好处是:如果以后改了类名,这行代码不需要修改,且支持子类化 -
__weak typeof(self) weakSelf = self:防循环引用的标准操作。因为闭包会强引用它捕获的外部变量,而self(NetworkManager)又通过runningTasks强引用了task,task又强引用了这个闭包。如果不加weak,就会形成一个self -> runningTasks -> task -> 闭包 -> self的循环引用,导致内存泄漏。 -
*
dataTaskWithRequest:completionHandler::这是NSURLSession提供的最便捷的 API。它将"网络传输"和"数据处理"捆绑 在一起。你传入的闭包会被系统内部强引用,并存储在task对象中。
completionHandler内部逻辑
objc
// 无论成功, 任务结束后都从 runningTasks 中删除
dispatch_sync(weakSelf.syncQueue, ^{
[weakSelf.runningTasks removeObjectForKey: fullUrl];
});
当任务执行完成之后,就从字典将这个任务移除,无论请求是否成功
objc
if (error) {
dispatch_sync(dispatch_get_main_queue(), ^{
if (completion) {
completion(nil, error);
}
});
return;
}
// 检查 HTTP 状态码
if (![response isKindOfClass:[NSHTTPURLResponse class]]) {
// 极少发生,兜底处理:当做成功但无数据返回
dispatch_async(dispatch_get_main_queue(), ^{
if (completion) completion(nil, nil);
});
return;
}
NSHTTPURLResponse* httpResponse = (NSHTTPURLResponse*) response;
if (httpResponse.statusCode < 200 || httpResponse.statusCode >= 300) {
// NSError 的错误域
NSError* statusError = [NSError errorWithDomain: @"com.netword.status"
code: httpResponse.statusCode
userInfo: @{NSLocalizedDescriptionKey: [NSString stringWithFormat: @"HTTP Error %ld", (long)httpResponse.statusCode]}];
// 网络请求默认在后台的子线程中进行
dispatch_async(dispatch_get_main_queue(), ^{
if (completion) {
completion(nil, statusError);
}
});
return;
}
这里进行的是错误的处理, 如果么请求失败, 那就打印错误信息, 并直接返回
objc
// jSON 解析
NSError* jsonError = nil;
// 将服务器返回的原始二进制数据转换为可操作的字典对象
// data: 服务器通过网路传回来的原始字节
// options: 解析选项 KNilOptions 值为 0 不开启任何特殊模式
NSDictionary* json = [NSJSONSerialization JSONObjectWithData: data
options: kNilOptions
error: &jsonError];
// 回到主线程执行业务回调
dispatch_async(dispatch_get_main_queue(), ^{
if (completion) {
completion(json, jsonError);
}
});
}];
dispatch_sync(self.syncQueue, ^{
weakSelf.runningTasks[fullUrl] = task;
});
// 启动请求
[task resume];
这里就是请求成功并获取到了数据, 将返回的JSON数据 通过 NSJSONSerialization 解析为 NSDictionary 对象并回调, 同时把任务添加到任务字典
最后的 task resume 是启动任务的步骤, NSURLSessionDataTask 在创建时处于挂起 (Suspended) 状态, 如果不调用 resume, 任务永远不会开始建立 TCP 连接, 也不会传输任何数据
启动后的流程: 系统会开始DNS解析, 三次握手, 然后发送 HTTP 请求头和数据, 等待服务器响应, 最终触发之前写的 competionHandler 闭包
GET方法完整代码
objc
- (void) GET:(NSString *)urlString parameters:(NSDictionary *)parameters completion:(void (^)(NSDictionary * _Nullable, NSError * _Nullable))completion {
NSMutableString* fullUrl = [NSMutableString stringWithString: urlString];
if (parameters && parameters.count > 0) {
[fullUrl appendString: @"?"];
NSMutableArray* pairs = [NSMutableArray array];
for (NSString* key in parameters.allKeys) {
id value = parameters[key];
NSString* valueStr = [NSString stringWithFormat: @"%@", value];
// URL编码
NSString* encodeValue = [valueStr stringByAddingPercentEncodingWithAllowedCharacters: [NSCharacterSet URLQueryAllowedCharacterSet]];
[pairs addObject: [NSString stringWithFormat: @"%@=%@", key, encodeValue]];
}
// 把所有的元素用指定分隔符粘成一个完整的字符串
[fullUrl appendString: [pairs componentsJoinedByString: @"&"]];
}
// 取消相同的 URL 的旧请求
[self cancelRequestForURL: fullUrl];
// NSLog(@"开始请求:%@", fullUrl);
// 创建URLRequest
NSURL* url = [NSURL URLWithString: fullUrl];
NSMutableURLRequest* request = [NSMutableURLRequest requestWithURL: url];
// HTTP方法
request.HTTPMethod = @"GET";
// 时间阈值
request.timeoutInterval = 15;
// 创建DataTask
// __weak 弱引用
// typeof(self) 在编译时自动推导self的具体类名
// 将self的弱引用赋值给变量 weakSelf
__weak typeof(self) weakSelf = self;
NSURLSessionDataTask* task = [self.session dataTaskWithRequest: request completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
// NSLog(@"收到服务器响应");
// 无论成功, 任务结束后都从 runningTasks 中删除
dispatch_sync(weakSelf.syncQueue, ^{
[weakSelf.runningTasks removeObjectForKey: fullUrl];
});
// NSLog(@"网络错误:%@", error);
if (error) {
dispatch_sync(dispatch_get_main_queue(), ^{
if (completion) {
completion(nil, error);
}
});
return;
}
// 检查 HTTP 状态码
if (![response isKindOfClass:[NSHTTPURLResponse class]]) {
// 极少发生,兜底处理:当做成功但无数据返回
dispatch_async(dispatch_get_main_queue(), ^{
if (completion) completion(nil, nil);
});
return;
}
NSHTTPURLResponse* httpResponse = (NSHTTPURLResponse*) response;
if (httpResponse.statusCode < 200 || httpResponse.statusCode >= 300) {
// NSError 的错误域
NSError* statusError = [NSError errorWithDomain: @"com.netword.status"
code: httpResponse.statusCode
userInfo: @{NSLocalizedDescriptionKey: [NSString stringWithFormat: @"HTTP Error %ld", (long)httpResponse.statusCode]}];
// 网络请求默认在后台的子线程中进行
dispatch_async(dispatch_get_main_queue(), ^{
if (completion) {
completion(nil, statusError);
}
});
return;
}
// jSON 解析
NSError* jsonError = nil;
// 将服务器返回的原始二进制数据转换为可操作的字典对象
// data: 服务器通过网路传回来的原始字节
// options: 解析选项 KNilOptions 值为 0 不开启任何特殊模式
NSDictionary* json = [NSJSONSerialization JSONObjectWithData: data
options: kNilOptions
error: &jsonError];
// 回到主线程执行业务回调
dispatch_async(dispatch_get_main_queue(), ^{
if (completion) {
completion(json, jsonError);
}
});
}];
dispatch_sync(self.syncQueue, ^{
weakSelf.runningTasks[fullUrl] = task;
});
// 启动请求
[task resume];
}
// 取消正在进行的请求
- (void)cancelRequestForURL:(nonnull NSString *)urlString {
// dispatch_sync: GCD 同步派发函数
// 将后面的任务(Block) 提交到指定的对列, 并阻塞当前的线程, 直到 Block 执行完毕才继续往下走
//参数1: 指定任务要在哪一个队列上进行
// 所有对可变字典 self.runningTasks 的访问(增、删、改、查)都必须排着队在这个队列上执行,从而避免多线程同时操作 NSMutableDictionary 导致的崩溃或数据错乱
dispatch_sync(self.syncQueue, ^{
// 取出对应的任务对象
NSURLSessionDataTask* task = self.runningTasks[urlString];
// 判断任务是否正在传输
if (task && task.state == NSURLSessionTaskStateRunning) {
// 修改对象的内部状态, 标记为取消状态
[task cancel];
}
// 无论任务是否存在,都从字典移除 (不会取消底层的网络任务)
[self.runningTasks removeObjectForKey: urlString];
});
}
- (void) cancelAllRequests {
dispatch_sync(self.syncQueue, ^{
for (NSURLSessionDataTask* task in self.runningTasks.allValues) {
if (task.state == NSURLSessionTaskStateRunning) {
[task cancel];
}
}
[self.runningTasks removeAllObjects];
});
}