一、为什么我们需要一个更好的网络层?

在日常开发中,我们几乎每个App都离不开网络请求。无论是加载用户头像,还是提交订单数据,网络模块都是App的“血管”。但是,很多朋友可能都遇到过这样的烦恼:网络突然断了,请求就失败了,用户只能手动重试;同样的数据反复请求,既浪费用户流量,又让界面加载变慢;或者,不同的页面里散布着几乎一样的网络请求代码,修改起来非常头疼。

如果我们把这些常见的需求——比如自动重试、数据缓存、统一管理——打包成一个独立的、高效的网络模块,那么开发起来就会轻松很多。今天,我们就来一起动手,用Swift优化我们的网络请求层,打造一个既“聪明”又“健壮”的网络模块。

二、设计蓝图:模块的核心思想

在开始写代码之前,我们先画个蓝图。我们希望这个模块能帮我们做几件核心的事情:

  1. 可重试:当网络请求因为短暂的波动(比如信号不好)而失败时,它能自动、有策略地重新尝试几次,而不是直接告诉用户“失败了”。
  2. 可缓存:对于某些不常变化的数据(比如App的配置信息、新闻列表),第一次请求后就把结果存起来。下次需要时,如果还没过期,就直接使用缓存,又快又省流量。
  3. 易使用:对外提供一个清晰、简单的接口。业务开发的同学只需要关心“请求什么”和“拿到数据后做什么”,而不需要操心重试和缓存是怎么实现的。
  4. 可配置:不同的请求可能有不同的需求。有的请求需要缓存,有的不需要;有的失败后需要重试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的启动配置、功能开关、城市列表等,可以设置较长的缓存时间。
  • 弱网环境:地铁、电梯等网络不稳定的场景,自动重试能显著提高请求成功率。
  • 节省资源:对于按流量计费或服务器压力大的场景,缓存能减少不必要的网络交互。

技术优缺点

  • 优点
    1. 提升用户体验:缓存让加载更快,重试让应用更稳定。
    2. 降低开发成本:统一封装,避免重复代码。
    3. 提高可维护性:网络相关逻辑集中管理,修改和调试方便。
    4. 灵活可配置:不同的API可以定制不同的重试和缓存策略。
  • 缺点
    1. 复杂度增加:相比直接调用URLSession,引入了更多的抽象层和状态管理。
    2. 缓存一致性:如果服务器数据更新,客户端可能仍显示旧缓存。需要设计合理的缓存过期和强制更新机制(如下拉刷新)。
    3. 内存与磁盘占用:缓存会消耗存储空间,需要设置合理的容量上限和清理策略。

注意事项

  1. 缓存键的设计:示例中的缓存键生成方法比较简单,在实际项目中,需要确保它能唯一标识一个请求,通常要综合考虑URL、方法、Header、Body等。
  2. 缓存失效:对于“读多写少”的数据缓存很有效。但对于“写后立即要读”的场景(如发表评论后刷新列表),可能需要手动清除相关缓存或使用“先更新本地再请求网络”的策略。
  3. 错误分类:不是所有错误都适合重试。例如,“404资源不存在”或“401未授权”这类客户端错误,重试是没有意义的。我们的重试逻辑应该主要针对网络超时、连接断开等临时性故障。
  4. 线程安全:我们的缓存管理器NetworkCacheManager被设计成单例,在多线程环境下读写缓存需要注意线程安全。NSCache本身是线程安全的,但我们的文件读写操作可能需要添加锁或使用串行队列来保护。
  5. 监控与日志:在生产环境中,应该为这个网络模块添加详细的日志记录和性能监控,方便追踪问题,例如记录缓存命中率、重试次数分布、平均请求耗时等。

九、总结

通过这次从设计到实现的旅程,我们构建了一个功能相对完善的Swift网络模块。它不仅仅是简单封装URLSession,而是有针对性地解决了实际开发中的痛点:通过自动重试来应对网络波动,通过智能缓存来提升性能与体验

这个模块是一个很好的起点,你可以根据自己的项目需求继续扩展它,例如:

  • 增加网络状态监听(如Reachability),在无网络时直接返回缓存或失败。
  • 集成更强大的第三方网络库(如Alamofire)作为底层引擎。
  • 添加请求优先级、请求取消、批量请求等功能。
  • 实现更复杂的缓存策略,如根据网络类型(Wi-Fi/蜂窝数据)决定是否缓存大图片。

希望这篇文章能为你优化自己的App网络层带来启发。记住,好的架构不是一蹴而就的,而是在不断解决实际问题的过程中迭代出来的。现在,就动手试试,让你应用中的网络请求变得更优雅、更强大吧!