网络管理单例
在仿写天气预报项目的时候,需要多次创建网络请求,例如首页展示收藏城市的天气状况, 在搜索的时候通过输入的字符进行搜索,获取城市名称, 经纬度以及所在省份等信息, 还有在搜索后点击搜索结果查看具体城市的天气状况
那么在这么多地方都去创建网络请求的话, 就会显得很繁琐,代码的结构都是一样的,于是就可以想到用一个类将网络请求封装起来, 在需要的时候直接调用就可以创建
这里笔者根据项目要求,创建了网络管理单例 (NetworkManager), 在单例中实现了创建网络请求以及取消网洛请求的方法
网络管理单例实现了以下功能:
- 统一封装网络请求
- 提供了
GET:parameters:completion:方法,内部自动处理 URL 参数拼接、URL 编码、创建NSURLRequest、启动NSURLSessionDataTask。 - 统一设置超时时间, 会话配置, 避免在每个控制器中重复编写网络请求代码
请求取消以及生命周期管理
- 通过 runningTasks 保存所有正在进行的请求任务
- 提供了
cancelRequestForURL:和cancelAllRequest方法, 允许在发起新的请求的时候取消旧的请求, 防止- 过时数据回显 (搜索时输入变化, 旧的请求结果返回后覆盖新的返回结果)
- 网络资源的浪费和内存泄漏
- 界面刷新错乱 (在首页刷新的时候, 取消正在进行的旧请求, 确保只处理最新相应)
线程安全
- 使用串行队列 syneQueue 保证runningTasks 的读写操作, 确保在多线程环境下 (如多个网络回调时修改字典) ,不会出现崩溃和数据竞争
集中配置与管理
- 可在单例中统一修改超时时间, 缓存策略, 请求头等, 方便全局调优, 无需逐个控制器修改
在.h文件中声明方法
@interfaceNetworkManager: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创建对外的方法
/// 全局复用的 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 在执行。当主线程的“存入”任务正在排队时,后台线程的“移除”任务必须乖乖等在门外,等“存入”彻底完成后才能进来执行。这就把“多线程并行操作”强制降级为“单线程串行操作”,从根本上杜绝了崩溃风险。
首先实现单例的创建方法
#pragmamark-单例创建+(instancetype)sharedManager{staticdispatch_once_t onceToken;staticNetworkManager*instance=nil;dispatch_once(&onceToken,^{instance=[[superallocWithZone:nil]init];});returninstance;}+(instancetype)allocWithZone:(struct_NSZone*)zone{return[selfsharedManager];}-(instancetype)init{self=[superinit];if(self){// 保证代码只执行一次staticdispatch_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);});}returnself;}-(id)copyWithZone:(NSZone*)zone{returnself;}-(id)mutableCopyWithZone:(NSZone*)zone{returnself;}+ (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(串行队列)。它的核心作用是将所有的“读写字典”操作,强制按顺序排队执行。队列中的任务一个接一个执行,前一个没执行完,后一个绝不开始。这天然保证了互斥
创建网络请求
-(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编码
NSMutableString*fullUrl=[NSMutableString stringWithString:urlString];if(parameters&¶meters.count>0){[fullUrl appendString:@"?"];NSMutableArray*pairs=[NSMutableArray array];for(NSString*keyinparameters.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这样的百分号编码,保证传输无歧义。
// 取消相同的 URL 的旧请求[selfcancelRequestForURL:fullUrl];// 取消正在进行的请求-(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。最后从任务字典中移除旧的任务
// 创建URLRequestNSURL*url=[NSURL URLWithString:fullUrl];NSMutableURLRequest*request=[NSMutableURLRequest requestWithURL:url];// HTTP方法request.HTTPMethod=@"GET";// 时间阈值request.timeoutInterval=15;将完整的URL字符串转换为 NSURL对象, 这是系统底层网络库的标准输入格式,
NSMutableURLRequest 指定这是一恶搞可变的网络请求, 可以设置它的各种属性 , 这里设置了它的 HTTP 方法和 超时时间, 如果在15 秒内TCP没有建立成功, 或者服务器没有返回完整的数据, 系统会自动取消这个任务
// 创建DataTask// __weak 弱引用// typeof(self) 在编译时自动推导self的具体类名// 将self的弱引用赋值给变量 weakSelf__weaktypeof(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内部逻辑
// 无论成功, 任务结束后都从 runningTasks 中删除dispatch_sync(weakSelf.syncQueue,^{[weakSelf.runningTasks removeObjectForKey:fullUrl];});当任务执行完成之后,就从字典将这个任务移除,无论请求是否成功
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];这里就是请求成功并获取到了数据, 将返回的JSON数据 通过 NSJSONSerialization 解析为 NSDictionary 对象并回调, 同时把任务添加到任务字典
最后的 [task resume] 是启动任务的步骤,NSURLSessionDataTask在创建时处于挂起 (Suspended) 状态, 如果不调用 resume, 任务永远不会开始建立 TCP 连接, 也不会传输任何数据
启动后的流程: 系统会开始DNS解析, 三次握手, 然后发送 HTTP 请求头和数据, 等待服务器响应, 最终触发之前写的 competionHandler 闭包
GET方法完整代码
-(void)GET:(NSString*)urlString parameters:(NSDictionary*)parameters completion:(void(^)(NSDictionary*_Nullable,NSError*_Nullable))completion{NSMutableString*fullUrl=[NSMutableString stringWithString:urlString];if(parameters&¶meters.count>0){[fullUrl appendString:@"?"];NSMutableArray*pairs=[NSMutableArray array];for(NSString*keyinparameters.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 的旧请求[selfcancelRequestForURL:fullUrl];// NSLog(@"开始请求:%@", fullUrl);// 创建URLRequestNSURL*url=[NSURL URLWithString:fullUrl];NSMutableURLRequest*request=[NSMutableURLRequest requestWithURL:url];// HTTP方法request.HTTPMethod=@"GET";// 时间阈值request.timeoutInterval=15;// 创建DataTask// __weak 弱引用// typeof(self) 在编译时自动推导self的具体类名// 将self的弱引用赋值给变量 weakSelf__weaktypeof(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*taskinself.runningTasks.allValues){if(task.state==NSURLSessionTaskStateRunning){[task cancel];}}[self.runningTasks removeAllObjects];});}