一、网络层架构设计原则
一个健壮的iOS网络层需要解决以下核心问题:统一错误处理、统一超时机制、证书校验、重试策略、Mock测试、流量统计。网络层的架构设计决定了整个App的数据通信质量。
1.1 分层架构设计
// 三层网络架构
//
// Layer 1: URLSession 封装层
// 职责: URLSession 配置、SessionDelegate、基础请求发送
//
// Layer 2: 协议层(Protocol-Oriented Network)
// 职责: 请求/响应模型定义、参数编码、结果解析
//
// Layer 3: 业务层
// 职责: 具体API调用、错误映射、业务逻辑处理
// Layer 1: 核心网络引擎
class NetworkEngine {
static let shared = NetworkEngine()
private let session: URLSession
private init() {
let config = URLSessionConfiguration.default
config.timeoutIntervalForRequest = 30
config.timeoutIntervalForResource = 60
config.waitsForConnectivity = true // 网络不可用时等待
config.requestCachePolicy = .reloadIgnoringLocalCacheData
self.session = URLSession(configuration: config)
}
func request(_ endpoint: Endpoint,
completion: @escaping (Result) -> Void) {
guard let request = endpoint.urlRequest else {
completion(.failure(.invalidURL))
return
}
let task = session.dataTask(with: request) { data, response, error in
if let error = error {
completion(.failure(.networkError(error)))
return
}
guard let httpResponse = response as? HTTPURLResponse else {
completion(.failure(.invalidResponse))
return
}
guard (200...299).contains(httpResponse.statusCode) else {
completion(.failure(.httpError(httpResponse.statusCode)))
return
}
guard let data = data else {
completion(.failure(.noData))
return
}
completion(.success(data))
}
task.resume()
}
}
二、协议化网络层(Protocol-Oriented)
2.1 Endpoint 协议定义
每个API作为一个 Endpoint 对象,职责单一且可测试:
protocol Endpoint {
var baseURL: String { get }
var path: String { get }
var method: HTTPMethod { get }
var headers: [String: String]? { get }
var parameters: Parameters? { get }
var parameterEncoding: ParameterEncoding { get }
var timeout: TimeInterval { get }
}
extension Endpoint {
var baseURL: String { "https://api.example.com" }
var headers: [String: String]? {
["Content-Type": "application/json",
"Accept": "application/json"]
}
var timeout: TimeInterval { 30 }
var urlRequest: URLRequest? {
guard let url = URL(string: baseURL + path) else { return nil }
var request = URLRequest(url: url)
request.httpMethod = method.rawValue
request.allHTTPHeaderFields = headers
request.timeoutInterval = timeout
if let params = parameters {
do {
request.httpBody = try parameterEncoding.encode(params, into: &request)
} catch {
return nil
}
}
return request
}
}
// 具体API实现
enum UserAPI: Endpoint {
case login(username: String, password: String)
case fetchUser(id: String)
case updateProfile(User)
var path: String {
switch self {
case .login: return "/auth/login"
case .fetchUser(let id): return "/users/\(id)"
case .updateProfile: return "/users/profile"
}
}
var method: HTTPMethod {
switch self {
case .login: return .post
case .fetchUser: return .get
case .updateProfile: return .put
}
}
var parameters: Parameters? {
switch self {
case .login(let username, let password):
return ["username": username, "password": password]
case .fetchUser: return nil
case .updateProfile(let user):
return user.toDictionary()
}
}
}
三、重试与幂等策略
3.1 指数退避重试
网络抖动时立即重试会导致拥塞。采用指数退避(Exponential Backoff)策略:
class RetryPolicy {
private let maxRetries: Int
private let baseDelay: TimeInterval
private let maxDelay: TimeInterval
init(maxRetries: Int = 3, baseDelay: TimeInterval = 1.0, maxDelay: TimeInterval = 30) {
self.maxRetries = maxRetries
self.baseDelay = baseDelay
self.maxDelay = maxDelay
}
func execute(_ operation: @escaping () async throws -> Void) async throws {
var attempt = 0
var lastError: Error?
while attempt < maxRetries {
do {
try await operation()
return
} catch {
lastError = error
attempt += 1
if attempt >= maxRetries { break }
// 指数退避:1s → 2s → 4s → 8s...(加随机抖动防止惊群)
let delay = min(baseDelay * pow(2.0, Double(attempt - 1)), maxDelay)
let jitter = Double.random(in: 0...0.3) * delay
try await Task.sleep(nanoseconds: UInt64((delay + jitter) * 1_000_000_000))
}
}
throw lastError ?? NetworkError.unknown
}
}
// 判断错误是否可重试
enum RetryableError {
static func shouldRetry(_ error: Error, attempt: Int) -> Bool {
guard attempt < 3 else { return false }
if let networkError = error as? NetworkError {
switch networkError {
case .timeout, .noConnection, .networkError:
return true // 网络错误可重试
case .httpError(let code):
return [408, 429, 500, 502, 503, 504].contains(code)
// 408超时、429限流、5xx服务端错误 → 可重试
default: return false
}
}
return false
}
}
3.2 幂等性保障
重试的前提是接口幂等(同一请求执行多次产生相同结果)。需要后端配合,但iOS端可以通过Idempotency Key防止非幂等操作被重复执行:
// 幂等Key:同一操作的唯一标识
class IdempotentRequestManager {
private var inFlight: [String: Task] = [:]
private let lock = NSLock()
func execute(
_ endpoint: Endpoint,
idempotencyKey: String
) async throws -> T {
lock.lock()
defer { lock.unlock() }
// 防止同一操作并发发送(页面快速抖动)
if let existing = inFlight[idempotencyKey] {
let data = try await existing.value
return try JSONDecoder().decode(T.self, from: data)
}
let task = Task {
var request = endpoint.urlRequest
request?.setValue(idempotencyKey, forHTTPHeaderField: "Idempotency-Key")
// 等待请求完成
return try await withCheckedThrowingContinuation { continuation in
NetworkEngine.shared.request(endpoint) { result in
continuation.resume(with: result)
}
}
}
inFlight[idempotencyKey] = task
do {
let data = try await task.value
inFlight.removeValue(forKey: idempotencyKey)
return try JSONDecoder().decode(T.self, from: data)
} catch {
inFlight.removeValue(forKey: idempotencyKey)
throw error
}
}
}
四、证书校验与安全
4.1 SSL Pinning 实现
防止中间人攻击(MITM)的标准做法是SSL Pinning。推荐使用 TrustKit 的自动刷本方案:
// 方案1:证书公钥Pinning(最稳定,证书续期不受影响)
class PinnedURLSessionDelegate: NSObject, URLSessionDelegate {
private let pinnedPublicKeyHashes: Set
init(hashes: [String]) {
self.pinnedPublicKeyHashes = Set(hashes)
super.init()
}
func urlSession(_ session: URLSession,
didReceive challenge: URLAuthenticationChallenge,
completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
guard challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust,
let serverTrust = challenge.protectionSpace.serverTrust else {
completionHandler(.cancelAuthenticationChallenge, nil)
return
}
// 获取服务器证书链中的所有公钥
let serverCertificateChain = SecTrustCopyCertificateChain(serverTrust) as? [SecCertificate] ?? []
var publicKeyHashesFound: Set = []
for certificate in serverCertificateChain {
if let publicKey = SecCertificateCopyKey(certificate) {
let publicKeyData = SecKeyCopyExternalRepresentation(publicKey, nil) as Data?
let hash = publicKeyData?.sha256.base64EncodedString()
if let hash = hash {
publicKeyHashesFound.insert(hash)
}
}
}
// 判断是否有任何受信任的公钥
if pinnedPublicKeyHashes.isDisjoint(with: publicKeyHashesFound) {
completionHandler(.cancelAuthenticationChallenge, nil)
return
}
completionHandler(.useCredential, URLCredential(trust: serverTrust))
}
}
// 方案2:TrustKit(推荐,开源且维护活跃)
// TrustKit自动处理证书续期、后台刷新、报告服务器异常
4.2 HSTS 与 HTTPS 强制
在 info.plist 中强制使用TLS,防止降级攻击:
// info.plist
NSAppTransportSecurity
NSAllowsArbitraryLoads
NSExceptionDomains
example.com
NSExceptionRequiresForwardSecrecy
NSExceptionMinimumTLSVersion
TLSv1.2
五、网络状态监控
5.1 NWPathMonitor 实时监控
// 使用 Network.framework 的 NWPathMonitor(iOS 12+)
class NetworkMonitor: ObservableObject {
static let shared = NetworkMonitor()
@Published private(set) var isConnected = true
@Published private(set) var connectionType: ConnectionType = .unknown
enum ConnectionType {
case wifi, cellular, ethernet, unknown
}
private let monitor = NWPathMonitor()
private let queue = DispatchQueue(label: "NetworkMonitor")
private init() {
monitor.pathUpdateHandler = { [weak self] path in
DispatchQueue.main.async {
self?.isConnected = path.status == .satisfied
self?.connectionType = self?.getConnectionType(path) ?? .unknown
}
}
monitor.start(queue: queue)
}
private func getConnectionType(_ path: NWPath) -> ConnectionType {
if path.usesInterfaceType(.wifi) { return .wifi }
if path.usesInterfaceType(.cellular) { return .cellular }
if path.usesInterfaceType(.wiredEthernet) { return .ethernet }
return .unknown
}
}
网络层设计检查清单
- 统一错误枚举(NetworkError),不要让NSError四处散落
- 幂等操作用 GET/PUT/DELETE,非幂等操作用 POST 并配 Idempotency Key
- 网络监控器(NWPathMonitor)驱动 App 的离线状态展示
- HTTPS Pinning 必须在 Release 配置中启用
- 使用URLCache统一管理响应缓存,设置合理的过期策略