iOS: 网络管理单例
2026/8/1 12:14:00 网站建设 项目流程

网络管理单例

在仿写天气预报项目的时候,需要多次创建网络请求,例如首页展示收藏城市的天气状况, 在搜索的时候通过输入的字符进行搜索,获取城市名称, 经纬度以及所在省份等信息, 还有在搜索后点击搜索结果查看具体城市的天气状况

那么在这么多地方都去创建网络请求的话, 就会显得很繁琐,代码的结构都是一样的,于是就可以想到用一个类将网络请求封装起来, 在需要的时候直接调用就可以创建

这里笔者根据项目要求,创建了网络管理单例 (NetworkManager), 在单例中实现了创建网络请求以及取消网洛请求的方法

网络管理单例实现了以下功能:

  1. 统一封装网络请求
  • 提供了GET:parameters:completion:方法,内部自动处理 URL 参数拼接、URL 编码、创建NSURLRequest、启动NSURLSessionDataTask
  • 统一设置超时时间, 会话配置, 避免在每个控制器中重复编写网络请求代码
  1. 请求取消以及生命周期管理

    1. 通过 runningTasks 保存所有正在进行的请求任务
    2. 提供了cancelRequestForURL:cancelAllRequest方法, 允许在发起新的请求的时候取消旧的请求, 防止
      1. 过时数据回显 (搜索时输入变化, 旧的请求结果返回后覆盖新的返回结果)
      2. 网络资源的浪费和内存泄漏
      3. 界面刷新错乱 (在首页刷新的时候, 取消正在进行的旧请求, 确保只处理最新相应)
  2. 线程安全

    1. 使用串行队列 syneQueue 保证runningTasks 的读写操作, 确保在多线程环境下 (如多个网络回调时修改字典) ,不会出现崩溃和数据竞争
  3. 集中配置与管理

    1. 可在单例中统一修改超时时间, 缓存策略, 请求头等, 方便全局调优, 无需逐个控制器修改

在.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 或NSURLSessionTasktaskIdentifier),Value 是请求的回调闭包(Block)或自定义的上下文模型。这里存储的是Task (NSURLSessionDataTask*)

  • 实际用途
    1. 任务追踪:当NSURLSession的代理方法(如didCompleteWithError)回调时,需要通过taskIdentifier从字典里取出对应的 Block 来执行,并把结果返回给调用者。
    2. 避免重复请求:在发起新请求前,先查字典。如果同一个请求(比如同一个 URL)已经在跑,就直接复用或取消旧任务,防止重复网络开销。
    3. 取消操作:当用户离开页面时,可以根据 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"}
  • **代码中的处理 **
    1. 判断字典是否存在且不为空。
    2. 遍历所有键值对,将每个value转为字符串。
    3. value进行URL 编码(处理中文、空格等特殊字符)。
    4. 拼装成key=value形式的数组,再用&连接成最终参数字符串。
    5. 拼接到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。通常成功时errornil,失败时jsonnil(两者互斥)。
  • 代码中的处理
    1. 在请求成功的逻辑里,解析出json,通过completion(json, nil)回传。
    2. 在请求失败(网络错误、状态码异常、解析失败)的逻辑里,通过completion(nil, error)回传。
    3. 通过dispatch_async(dispatch_get_main_queue())强制回到主线程执行该 Block,方便调用方直接更新 UI。

参数拼接URL编码

NSMutableString*fullUrl=[NSMutableString stringWithString:urlString];if(parameters&&parameters.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 canceltask.stateNSURLSessionTaskStateRunning变为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强引用了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.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&&parameters.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];});}

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询