一、为什么我们需要一个更好的网络层?
在日常开发中,我们几乎每个App都离不开网络请求。无论是加载用户头像,还是提交订单数据,网络模块都是App的“血管”。但是,很多朋友可能都遇到过这样的烦恼:网络突然断了,请求就失败了,用户只能手动重试;同样的数据反复请求,既浪费用户流量,又让界面加载变慢;或者,不同的页面里散布着几乎一样的网络请求代码,修改起来非常头疼。
如果我们把这些常见的需求——比如自动重试、数据缓存、统一管理——打包成一个独立的、高效的网络模块,那么开发起来就会轻松很多。今天,我们就来一起动手,用Swift优化我们的网络请求层,打造一个既“聪明”又“健壮”的网络模块。
二、设计蓝图:模块的核心思想
在开始写代码之前,我们先画个蓝图。我们希望这个模块能帮我们做几件核心的事情:
- 可重试:当网络请求因为短暂的波动(比如信号不好)而失败时,它能自动、有策略地重新尝试几次,而不是直接告诉用户“失败了”。
- 可缓存:对于某些不常变化的数据(比如App的配置信息、新闻列表),第一次请求后就把结果存起来。下次需要时,如果还没过期,就直接使用缓存,又快又省流量。
- 易使用:对外提供一个清晰、简单的接口。业务开发的同学只需要关心“请求什么”和“拿到数据后做什么”,而不需要操心重试和缓存是怎么实现的。
- 可配置:不同的请求可能有不同的需求。有的请求需要缓存,有的不需要;有的失败后需要重试3次,有的1次就够了。我们的模块应该能灵活地支持这些配置。
听起来是不是很实用?接下来,我们就用代码把它实现出来。
三、从基础开始:封装网络请求
万丈高楼平地起,我们先封装一个最基础的网络请求器。这里我们使用Swift原生的URLSession,因为它足够强大和现代。
技术栈:Swift + URLSession
import Foundation
/// 网络请求方法枚举
enum HTTPMethod: String {
case get = "GET"
case post = "POST"
// 可以按需添加 put, delete 等
}
/// 网络请求配置
struct NetworkRequestConfig {
let baseURL: String
let path: String
let method: HTTPMethod
let parameters: [String: Any]?
let headers: [String: String]?
// 可以扩展更多配置,如超时时间、缓存策略等
var timeoutInterval: TimeInterval = 30.0
}
/// 核心网络请求器
class BasicNetworkFetcher {
/// 发起网络请求
/// - Parameters:
/// - config: 请求配置
/// - completion: 完成回调,返回Result类型(成功或失败)
func request<T: Decodable>(with config: NetworkRequestConfig,
completion: @escaping (Result<T, Error>) -> Void) {
// 1. 构建URL
guard let url = URL(string: config.baseURL + config.path) else {
completion(.failure(NetworkError.invalidURL))
return
}
var urlRequest = URLRequest(url: url)
urlRequest.httpMethod = config.method.rawValue
urlRequest.timeoutInterval = config.timeoutInterval
// 2. 设置请求头
config.headers?.forEach { key, value in
urlRequest.setValue(value, forHTTPHeaderField: key)
}
// 3. 处理请求体(例如POST请求的JSON参数)
if let parameters = config.parameters, config.method == .post {
do {
let jsonData = try JSONSerialization.data(withJSONObject: parameters, options: [])
urlRequest.httpBody = jsonData
urlRequest.setValue("application/json", forHTTPHeaderField: "Content-Type")
} catch {
completion(.failure(error))
return
}
}
// 4. 创建并启动数据任务
let task = URLSession.shared.dataTask(with: urlRequest) { data, response, error in
// 切换到主线程回调,方便更新UI
DispatchQueue.main.async {
// 5. 处理错误
if let error = error {
completion(.failure(error))
return
}
// 6. 检查HTTP状态码
guard let httpResponse = response as? HTTPURLResponse,
(200...299).contains(httpResponse.statusCode) else {
completion(.failure(NetworkError.badResponse))
return
}
// 7. 解析数据
guard let data = data else {
completion(.failure(NetworkError.noData))
return
}
do {
let decodedObject = try JSONDecoder().decode(T.self, from: data)
completion(.success(decodedObject))
} catch {
completion(.failure(error))
}
}
}
task.resume()
}
}
/// 自定义网络错误
enum NetworkError: Error {
case invalidURL
case badResponse
case noData
}
这个BasicNetworkFetcher类已经是一个不错的起点。它用NetworkRequestConfig来统一管理请求参数,并通过泛型<T: Decodable>支持将JSON数据自动解码成我们想要的模型(Model)。但是,它还没有重试和缓存功能。别急,我们一步步来。
四、让网络请求更“坚韧”:实现自动重试机制
网络世界并不稳定。用户可能走进电梯,或者瞬间切换到飞行模式。对于这些短暂的失败,我们应该给请求一个“再来一次”的机会。
重试策略有很多种,比如:
- 简单重试:失败后立刻重试,最多N次。
- 指数退避:每次重试前等待一段时间,且等待时间按指数增长(例如等1秒、2秒、4秒),避免对服务器造成连续冲击。
我们来实现一个带指数退避的智能重试器。
/// 重试策略
enum RetryPolicy {
case none // 不重试
case fixed(count: UInt) // 固定次数重试
case exponentialBackoff(maxCount: UInt, initialDelay: TimeInterval) // 指数退避重试
}
/// 可重试的网络请求器
class RetriableNetworkFetcher: BasicNetworkFetcher {
private let maxRetryCount: UInt
private let initialDelay: TimeInterval
init(maxRetryCount: UInt = 3, initialDelay: TimeInterval = 1.0) {
self.maxRetryCount = maxRetryCount
self.initialDelay = initialDelay
super.init()
}
/// 发起可重试的请求
func retriableRequest<T: Decodable>(with config: NetworkRequestConfig,
retryPolicy: RetryPolicy = .exponentialBackoff(maxCount: 3, initialDelay: 1.0),
completion: @escaping (Result<T, Error>) -> Void) {
var currentAttempt: UInt = 0
// 内部递归函数,用于实现重试逻辑
func attemptRequest() {
// 调用父类的原始请求方法
super.request(with: config) { [weak self] (result: Result<T, Error>) in
guard let self = self else { return }
switch result {
case .success(let value):
// 成功,直接回调
completion(.success(value))
case .failure(let error):
currentAttempt += 1
// 判断是否需要以及是否能够重试
let shouldRetry: Bool
let delayBeforeRetry: TimeInterval
switch retryPolicy {
case .none:
shouldRetry = false
delayBeforeRetry = 0
case .fixed(let count):
shouldRetry = currentAttempt <= count
delayBeforeRetry = 0 // 固定重试可立即进行,也可加固定延迟
case .exponentialBackoff(let maxCount, let initialDelay):
shouldRetry = currentAttempt <= maxCount
// 计算指数退避延迟:初始延迟 * (2的 (当前尝试次数-1) 次方)
delayBeforeRetry = initialDelay * pow(2.0, Double(currentAttempt - 1))
}
if shouldRetry {
print("请求失败,第\(currentAttempt)次重试,延迟\(delayBeforeRetry)秒。错误: \(error.localizedDescription)")
// 延迟一段时间后再次尝试
DispatchQueue.global().asyncAfter(deadline: .now() + delayBeforeRetry) {
attemptRequest()
}
} else {
// 不再重试,将最终失败结果回调出去
print("已达最大重试次数(\(currentAttempt)),最终失败。")
completion(.failure(error))
}
}
}
}
// 开始第一次尝试
attemptRequest()
}
}
现在,我们的网络请求有了“韧性”。当遇到可恢复的错误时,它会按照我们设定的策略默默重试,只有在用尽所有机会后,才会将失败结果告知用户。这大大提升了用户在弱网环境下的体验。
五、给数据安个“家”:实现智能缓存
缓存是提升应用响应速度和节省资源的利器。我们的目标是:对于GET请求,如果配置了缓存,就先将结果存起来。下次遇到相同请求时,先检查缓存是否有效(比如是否过期),如果有效就直接返回缓存数据。
Swift中简单的缓存可以用NSCache或者写入文件系统。这里我们设计一个基于内存和磁盘的双层缓存。
import Foundation
/// 缓存配置
struct CacheConfig {
let memoryCapacity: Int // 内存缓存容量,单位字节
let diskCapacity: Int // 磁盘缓存容量,单位字节
let expiryTime: TimeInterval // 缓存过期时间,单位秒
}
/// 缓存条目,存储数据及其元信息
struct CacheEntry {
let data: Data
let timestamp: Date // 缓存创建时间
let expiry: TimeInterval
var isValid: Bool {
return Date().timeIntervalSince(timestamp) < expiry
}
}
/// 智能缓存管理器
class NetworkCacheManager {
static let shared = NetworkCacheManager()
private init() {}
private let memoryCache = NSCache<NSString, NSData>()
private let fileManager = FileManager.default
private let cacheDirectory: URL
init() {
// 指定缓存目录
let directories = fileManager.urls(for: .cachesDirectory, in: .userDomainMask)
cacheDirectory = directories[0].appendingPathComponent("NetworkCache", isDirectory: true)
// 创建缓存目录
try? fileManager.createDirectory(at: cacheDirectory, withIntermediateDirectories: true)
// 配置内存缓存
memoryCache.totalCostLimit = 10 * 1024 * 1024 // 10MB内存限制
}
/// 生成缓存键(通常使用请求的完整URL+参数等唯一标识)
private func cacheKey(for request: NetworkRequestConfig) -> String {
// 这里简单演示,生产环境需要更复杂的键生成逻辑以确保唯一性
let key = "\(request.method.rawValue)_\(request.baseURL)\(request.path)_\(request.parameters?.description ?? "")"
return key
}
/// 保存数据到缓存
func save(_ data: Data, for request: NetworkRequestConfig, expiry: TimeInterval) {
let key = cacheKey(for: request)
let entry = CacheEntry(data: data, timestamp: Date(), expiry: expiry)
// 1. 存入内存缓存
memoryCache.setObject(data as NSData, forKey: key as NSString)
// 2. 存入磁盘缓存
let fileURL = cacheDirectory.appendingPathComponent(key.md5) // 使用MD5作为文件名
do {
let archivedData = try JSONEncoder().encode(entry)
try archivedData.write(to: fileURL)
} catch {
print("磁盘缓存写入失败: \(error)")
}
}
/// 从缓存加载数据
func load(for request: NetworkRequestConfig) -> Data? {
let key = cacheKey(for: request)
// 1. 检查内存缓存
if let memoryData = memoryCache.object(forKey: key as NSString) as Data? {
print("从内存缓存命中")
return memoryData
}
// 2. 检查磁盘缓存
let fileURL = cacheDirectory.appendingPathComponent(key.md5)
guard let diskData = try? Data(contentsOf: fileURL),
let entry = try? JSONDecoder().decode(CacheEntry.self, from: diskData) else {
return nil
}
// 3. 检查缓存是否有效
if entry.isValid {
print("从磁盘缓存命中并加载到内存")
// 加载到内存缓存,方便下次快速读取
memoryCache.setObject(entry.data as NSData, forKey: key as NSString)
return entry.data
} else {
// 缓存已过期,删除之
try? fileManager.removeItem(at: fileURL)
return nil
}
}
/// 清除所有缓存
func clearAllCache() {
memoryCache.removeAllObjects()
try? fileManager.removeItem(at: cacheDirectory)
try? fileManager.createDirectory(at: cacheDirectory, withIntermediateDirectories: true)
}
}
// 扩展String,用于生成MD5(这里使用简化版,实际项目建议使用更可靠的加密库)
extension String {
var md5: String {
// 此处为示意,省略具体MD5实现。实际可使用CommonCrypto库。
// 返回一个固定值用于示例编译。
return "md5_placeholder_for_\(self)"
}
}
有了缓存管理器,我们的网络模块就具备了“记忆”能力。对于频繁访问且变化不大的数据,速度会有肉眼可见的提升。
六、最终组装:打造集大成者的网络模块
现在,让我们把重试和缓存功能整合到一起,创建一个终极版的网络模块。同时,我们设计一个更友好的API。
/// 统一网络请求配置,包含基础配置、重试策略和缓存策略
struct AdvancedNetworkConfig {
let baseRequestConfig: NetworkRequestConfig
let retryPolicy: RetryPolicy
let shouldCache: Bool
let cacheExpiry: TimeInterval
}
/// 终极网络模块
class UltimateNetworkModule {
private let retriableFetcher = RetriableNetworkFetcher()
private let cacheManager = NetworkCacheManager.shared
/// 统一的请求入口
func sendRequest<T: Decodable>(with config: AdvancedNetworkConfig,
completion: @escaping (Result<T, Error>) -> Void) {
let requestConfig = config.baseRequestConfig
// 第1步:检查缓存(仅对GET请求且配置了缓存的情况)
if requestConfig.method == .get && config.shouldCache {
if let cachedData = cacheManager.load(for: requestConfig) {
do {
let decodedObject = try JSONDecoder().decode(T.self, from: cachedData)
print("【网络模块】使用缓存数据")
completion(.success(decodedObject))
return // 缓存命中,直接返回,不再发起网络请求
} catch {
print("【网络模块】缓存数据解析失败,继续请求网络")
// 缓存数据损坏,继续请求网络
}
}
}
// 第2步:发起带有重试机制的网络请求
print("【网络模块】发起网络请求")
retriableFetcher.retriableRequest(with: requestConfig,
retryPolicy: config.retryPolicy) { [weak self] (result: Result<T, Error>) in
guard let self = self else { return }
switch result {
case .success(let value):
// 第3步:请求成功,处理缓存
if requestConfig.method == .get && config.shouldCache {
// 将成功的数据序列化回Data以便存储
if let dataToCache = try? JSONEncoder().encode(value) {
self.cacheManager.save(dataToCache, for: requestConfig, expiry: config.cacheExpiry)
print("【网络模块】网络请求成功,数据已缓存")
}
}
completion(.success(value))
case .failure(let error):
// 请求最终失败
completion(.failure(error))
}
}
}
}
七、实战演练:看看它如何工作
让我们用一个实际的例子,来感受一下这个网络模块的便利性。假设我们要从某个API获取用户信息。
// 定义数据模型
struct UserProfile: Codable {
let id: Int
let name: String
let email: String
}
// 准备请求配置
let requestConfig = NetworkRequestConfig(
baseURL: "https://api.example.com",
path: "/user/profile",
method: .get,
parameters: ["user_id": "12345"],
headers: ["Authorization": "Bearer some_token"]
)
// 准备高级配置:重试3次(指数退避),缓存300秒
let advancedConfig = AdvancedNetworkConfig(
baseRequestConfig: requestConfig,
retryPolicy: .exponentialBackoff(maxCount: 3, initialDelay: 1.0),
shouldCache: true,
cacheExpiry: 300
)
// 使用终极网络模块
let networkModule = UltimateNetworkModule()
networkModule.sendRequest(with: advancedConfig) { (result: Result<UserProfile, Error>) in
switch result {
case .success(let userProfile):
print("成功获取用户信息: \(userProfile.name)")
// 更新UI...
case .failure(let error):
print("获取用户信息失败: \(error.localizedDescription)")
// 显示错误提示...
}
}
看,对于业务开发者来说,接口非常简洁。只需要配置好请求和缓存策略,然后处理成功或失败的回调即可。所有复杂的重试逻辑、缓存读写都被封装在模块内部。
八、深入探讨:场景、优缺点与注意事项
应用场景
- 列表页/详情页:新闻、商品、文章等不常变化的数据,非常适合缓存,提升二次打开速度。
- 配置信息:App的启动配置、功能开关、城市列表等,可以设置较长的缓存时间。
- 弱网环境:地铁、电梯等网络不稳定的场景,自动重试能显著提高请求成功率。
- 节省资源:对于按流量计费或服务器压力大的场景,缓存能减少不必要的网络交互。
技术优缺点
- 优点:
- 提升用户体验:缓存让加载更快,重试让应用更稳定。
- 降低开发成本:统一封装,避免重复代码。
- 提高可维护性:网络相关逻辑集中管理,修改和调试方便。
- 灵活可配置:不同的API可以定制不同的重试和缓存策略。
- 缺点:
- 复杂度增加:相比直接调用URLSession,引入了更多的抽象层和状态管理。
- 缓存一致性:如果服务器数据更新,客户端可能仍显示旧缓存。需要设计合理的缓存过期和强制更新机制(如下拉刷新)。
- 内存与磁盘占用:缓存会消耗存储空间,需要设置合理的容量上限和清理策略。
注意事项
- 缓存键的设计:示例中的缓存键生成方法比较简单,在实际项目中,需要确保它能唯一标识一个请求,通常要综合考虑URL、方法、Header、Body等。
- 缓存失效:对于“读多写少”的数据缓存很有效。但对于“写后立即要读”的场景(如发表评论后刷新列表),可能需要手动清除相关缓存或使用“先更新本地再请求网络”的策略。
- 错误分类:不是所有错误都适合重试。例如,“404资源不存在”或“401未授权”这类客户端错误,重试是没有意义的。我们的重试逻辑应该主要针对网络超时、连接断开等临时性故障。
- 线程安全:我们的缓存管理器
NetworkCacheManager被设计成单例,在多线程环境下读写缓存需要注意线程安全。NSCache本身是线程安全的,但我们的文件读写操作可能需要添加锁或使用串行队列来保护。 - 监控与日志:在生产环境中,应该为这个网络模块添加详细的日志记录和性能监控,方便追踪问题,例如记录缓存命中率、重试次数分布、平均请求耗时等。
九、总结
通过这次从设计到实现的旅程,我们构建了一个功能相对完善的Swift网络模块。它不仅仅是简单封装URLSession,而是有针对性地解决了实际开发中的痛点:通过自动重试来应对网络波动,通过智能缓存来提升性能与体验。
这个模块是一个很好的起点,你可以根据自己的项目需求继续扩展它,例如:
- 增加网络状态监听(如Reachability),在无网络时直接返回缓存或失败。
- 集成更强大的第三方网络库(如Alamofire)作为底层引擎。
- 添加请求优先级、请求取消、批量请求等功能。
- 实现更复杂的缓存策略,如根据网络类型(Wi-Fi/蜂窝数据)决定是否缓存大图片。
希望这篇文章能为你优化自己的App网络层带来启发。记住,好的架构不是一蹴而就的,而是在不断解决实际问题的过程中迭代出来的。现在,就动手试试,让你应用中的网络请求变得更优雅、更强大吧!
评论