
网络管理单例在仿写天气预报项目的时候,需要多次创建网络请求,例如首页展示收藏城市的天气状况, 在搜索的时候通过输入的字符进行搜索,获取城市名称, 经纬度以及所在省份等信息, 还有在搜索后点击搜索结果查看具体城市的天气状况那么在这么多地方都去创建网络请求的话, 就会显得很繁琐,代码的结构都是一样的,于是就可以想到用一个类将网络请求封装起来, 在需要的时候直接调用就可以创建这里笔者根据项目要求,创建了网络管理单例 (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)NSMutableDictionaryNSString*,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;—— 线程保护锁这是一个GCDGrand 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*instancenil;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.timeoutIntervalForRequest15.0;self.session[NSURLSession sessionWithConfiguration:config];self.runningTasks[NSMutableDictionary dictionary];// 创建串行任务队列// 参数1: 队列的名字// 参数2: 串行属性self.syncQueuedispatch_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的taskIdentifierValue 是请求的回调闭包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 编码处理中文、空格等特殊字符。拼装成keyvalue形式的数组再用连接成最终参数字符串。拼接到urlString后面生成完整的fullUrl形如https://api.weather.com/v1/beijing?key123langzh。特殊值如果传入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(parametersparameters.count0){[fullUrl appendString:?];NSMutableArray*pairs[NSMutableArray array];for(NSString*keyinparameters.allKeys){id valueparameters[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*taskself.runningTasks[urlString];// 判断任务是否正在传输if(tasktask.stateNSURLSessionTaskStateRunning){// 修改对象的内部状态, 标记为取消状态[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.HTTPMethodGET;// 时间阈值request.timeoutInterval15;将完整的URL字符串转换为 NSURL对象, 这是系统底层网络库的标准输入格式,NSMutableURLRequest 指定这是一恶搞可变的网络请求, 可以设置它的各种属性 , 这里设置了它的 HTTP 方法和 超时时间, 如果在15 秒内TCP没有建立成功, 或者服务器没有返回完整的数据, 系统会自动取消这个任务// 创建DataTask// __weak 弱引用// typeof(self) 在编译时自动推导self的具体类名// 将self的弱引用赋值给变量 weakSelf__weaktypeof(self)weakSelfself;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防循环引用的标准操作。因为闭包会强引用它捕获的外部变量而selfNetworkManager又通过runningTasks强引用了tasktask又强引用了这个闭包。如果不加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.statusCode200||httpResponse.statusCode300){// NSError 的错误域NSError*statusError[NSError errorWithDomain:com.netword.statuscode: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*jsonErrornil;// 将服务器返回的原始二进制数据转换为可操作的字典对象// 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(parametersparameters.count0){[fullUrl appendString:?];NSMutableArray*pairs[NSMutableArray array];for(NSString*keyinparameters.allKeys){id valueparameters[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.HTTPMethodGET;// 时间阈值request.timeoutInterval15;// 创建DataTask// __weak 弱引用// typeof(self) 在编译时自动推导self的具体类名// 将self的弱引用赋值给变量 weakSelf__weaktypeof(self)weakSelfself;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.statusCode200||httpResponse.statusCode300){// NSError 的错误域NSError*statusError[NSError errorWithDomain:com.netword.statuscode: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*jsonErrornil;// 将服务器返回的原始二进制数据转换为可操作的字典对象// 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*taskself.runningTasks[urlString];// 判断任务是否正在传输if(tasktask.stateNSURLSessionTaskStateRunning){// 修改对象的内部状态, 标记为取消状态[task cancel];}// 无论任务是否存在,都从字典移除 (不会取消底层的网络任务)[self.runningTasks removeObjectForKey:urlString];});}-(void)cancelAllRequests{dispatch_sync(self.syncQueue,^{for(NSURLSessionDataTask*taskinself.runningTasks.allValues){if(task.stateNSURLSessionTaskStateRunning){[task cancel];}}[self.runningTasks removeAllObjects];});}