避免重复创建URLSession-dataTask:首次请求返回时共享数据
这个场景我太熟悉了!之前做电商项目的时候,用户反复点击商品详情的刷新按钮,重复创建dataTask不仅占网络带宽,还可能导致界面多次刷新、回调顺序混乱,确实得找个更优雅的解决办法。下面几个方案我亲测好用,你可以根据业务场景选择:
优化重复网络请求的几种实用方案
1. 缓存正在进行的任务,避免重复发起
核心思路是维护一个字典,把当前正在执行的任务和对应的请求标识(比如请求URL+参数的哈希值)绑定。当用户再次发起相同请求时:
- 如果已有未完成的任务,要么忽略重复请求,要么把新的回调合并到已有任务中(看业务需求)
- 如果没有,创建新任务并存入字典,任务完成(成功/失败)后从字典移除
举个简单的代码示例:
class NetworkManager { static let shared = NetworkManager() private var ongoingTasks: [String: URLSessionDataTask] = [:] private let session = URLSession.shared func fetchData(with url: URL, completion: @escaping (Data?, Error?) -> Void) { // 如果请求带参数,建议把参数转成字符串一起生成key,避免不同参数的请求被误判为相同 let taskKey = url.absoluteString // 检查是否有正在进行的相同请求 if ongoingTasks[taskKey] != nil { print("已有相同请求在处理,忽略本次重复请求") return } let task = session.dataTask(with: url) { [weak self] data, _, error in // 任务完成后从缓存移除 self?.ongoingTasks.removeValue(forKey: taskKey) completion(data, error) } ongoingTasks[taskKey] = task task.resume() } }
如果需要让多个相同请求都拿到结果,可以把ongoingTasks的value改成包含任务和回调数组的结构体,任务完成时遍历所有回调执行。
2. 取消旧任务,只保留最新请求
如果是像搜索框输入这类场景——用户快速输入时,只需要最后一次输入的搜索结果,那可以在发起新请求时直接取消之前未完成的同类任务:
class SearchNetworkManager { static let shared = SearchNetworkManager() private var currentSearchTask: URLSessionDataTask? private let session = URLSession.shared func search(query: String, completion: @escaping (Data?, Error?) -> Void) { // 先取消之前的搜索任务 currentSearchTask?.cancel() guard let url = URL(string: "https://your-api.com/search?q=\(query.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed)!)") else { completion(nil, NSError(domain: "Invalid URL", code: -1, userInfo: nil)) return } let task = session.dataTask(with: url) { [weak self] data, _, error in // 取消的任务会返回URLError.cancelled,这里可以跳过错误处理 if let error = error as? URLError, error.code == .cancelled { return } self?.currentSearchTask = nil completion(data, error) } currentSearchTask = task task.resume() } }
这种方案能确保用户只拿到最新的请求结果,避免旧结果覆盖新结果的尴尬。
3. 结合URLSession内置缓存减少重复请求
如果你的请求是GET类型、数据不会频繁更新,可以利用URLSession的缓存策略,让系统自动处理重复请求:
// 配置请求缓存策略:优先用缓存,没有缓存再发起网络请求 let request = URLRequest(url: url, cachePolicy: .returnCacheDataElseLoad, timeoutInterval: 30) let task = session.dataTask(with: request) { data, response, error in // 处理结果 } task.resume()
不过这种方式依赖服务器返回的缓存头(比如Cache-Control),适合数据更新不频繁的场景,不能完全解决“正在进行的重复请求”问题,但能减少后续相同请求的网络开销。
4. 封装自定义任务,实现回调合并(进阶方案)
如果需要更灵活的任务管理,可以封装一个自定义的RequestTask类,管理每个请求的状态和回调数组,支持多个请求共享同一个网络任务:
class RequestTask { let task: URLSessionDataTask var completions: [(Data?, Error?) -> Void] = [] init(task: URLSessionDataTask, completion: @escaping (Data?, Error?) -> Void) { self.task = task self.completions.append(completion) } } class AdvancedNetworkManager { static let shared = AdvancedNetworkManager() private var ongoingTasks: [String: RequestTask] = [:] private let session = URLSession.shared func fetchData(with url: URL, completion: @escaping (Data?, Error?) -> Void) { let taskKey = url.absoluteString if let existingTask = ongoingTasks[taskKey] { // 把新的回调添加到已有任务中 existingTask.completions.append(completion) return } let task = session.dataTask(with: url) { [weak self] data, _, error in guard let self = self, let requestTask = self.ongoingTasks.removeValue(forKey: taskKey) else { return } // 遍历所有回调执行,让所有发起请求的地方都拿到结果 requestTask.completions.forEach { $0(data, error) } } let requestTask = RequestTask(task: task, completion: completion) ongoingTasks[taskKey] = requestTask task.resume() } }
这种方案能最大化节省网络资源,同时保证所有发起请求的页面或组件都能获取到数据。
总结一下:
- 要避免重复请求,优先选缓存正在进行的任务方案,根据业务决定忽略重复请求还是合并回调
- 实时性要求高的场景(比如搜索),用取消旧任务的方案更合适
- 数据更新不频繁的场景,可以配合URLSession缓存策略做辅助优化
内容的提问来源于stack exchange,提问作者iKK
相关产品推荐
相关产品推荐

