iOS: 网络管理单例

网络管理单例

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

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

这里笔者根据项目要求,创建了网络管理单例 (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文件中声明方法

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

  • 实际用途
    1. 任务追踪 :当 NSURLSession 的代理方法(如 didCompleteWithError)回调时,需要通过 taskIdentifier 从字典里取出对应的 Block 来执行,并把结果返回给调用者。
    2. 避免重复请求:在发起新请求前,先查字典。如果同一个请求(比如同一个 URL)已经在跑,就直接复用或取消旧任务,防止重复网络开销。
    3. 取消操作 :当用户离开页面时,可以根据 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"}
  • **代码中的处理 **
    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编码

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 canceltask.stateNSURLSessionTaskStateRunning 变为 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 强引用了 tasktask 又强引用了这个闭包。如果不加 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];
    });
}
相关推荐
游戏开发爱好者81 小时前
TraceEagle 代理抓包详解,无感代理、去证书绑定与 HTTP/3 解密能力解析
网络协议·计算机网络·网络安全·ios·adb·https·udp
编程圈子1 小时前
电机驱动开发学习21. R3_2 双相扇区电流采样与第三相重构
驱动开发·学习·重构
aiopencode2 小时前
SwiftUI Introspect生产环境完全指南:为什么它是安全可靠的选择
后端·ios
听雨入夜2 小时前
CMake构建学习笔记-libxml库的构建
笔记·学习
不在逃避q3 小时前
一步一步学习使用LiveBindings()TListView进阶使用(),打造天气预报程序
服务器·数据库·学习
00后程序员张3 小时前
iOS加固技术路线全面解析:Bitcode模式、源码模式与汇编模式对比及爱加密优势
android·汇编·ios·小程序·uni-app·cocoa·iphone
知识分享小能手3 小时前
高等数学学习教程,从入门到精通,微分中值定理与导数的应用(5)
学习·机器学习·概率论
520拼好饭被践踏3 小时前
JAVA+Agent学习day25
java·开发语言·学习
魔术师Dix4 小时前
StartGame:Unity TDD 外部工程指南
学习·游戏·unity·c#·测试驱动开发