iOS开发中的网络模块重构与性能优化:一个真实项目踩坑记录

后端便利贴
2025-06-15 17:34
阅读 636

引言:为什么我们要重写网络层?

引言:为什么我们要重写网络层?

在我们公司一个核心业务应用的开发过程中,我负责的其中一部分就是对原有网络请求模块进行重构和性能优化。这个项目已经上线多年,随着功能的增加和用户量的增长,原有的网络架构逐渐暴露出一些严重的问题:接口调用混乱、错误处理不统一、缓存逻辑臃肿、调试困难,甚至有时会导致用户体验卡顿或崩溃。

作为一个iOS开发者,我深知网络请求对于移动端体验的重要性。于是,我和团队决定重新梳理并重构整个App的网络层。这篇文章就来聊聊这次重构过程中遇到的真实问题、技术选型时的纠结、关键实现思路以及踩过的那些“坑”。


项目背景:老网络架构的问题

项目背景:老网络架构的问题

我们的App主要对接内部多个微服务后端,包含大量HTTP请求,包括文件上传下载、数据同步、推送通知等场景。旧的网络层是基于AFNetworking封装的一个类,最初设计比较简单,只支持GET和POST方法,并且每个接口都分散在各自页面中直接调用,导致:

  • 接口重复定义
  • 错误码处理不一致
  • 缓存机制混杂在各个控制器中
  • 单元测试困难
  • 没有集中监控或日志输出机制

而且由于早期版本对超时时间、并发限制没有合理设置,某些场景下会出现多个相同请求同时发出,或者长时间阻塞主线程,严重影响用户体验。


遇到的主要挑战

1. 统一网络接口结构

我们需要把所有的网络请求收拢到一个统一的入口。但面对历史遗留的数十个VC中散乱的AFNetworkin调用,如何做平滑过渡是个难点。

2. 错误码标准化

后端返回的错误信息格式不一,有些是JSON字段名不统一,有些是文案内容变化频繁。前端这边缺乏一套统一的错误处理策略,导致用户看到的提示五花八门。

3. 请求拦截与缓存策略

需要实现统一的请求缓存策略(如GET接口可缓存),同时支持本地缓存过期控制和强制刷新功能。此外,还需要对部分接口进行脱敏和敏感数据加密。

4. 性能优化

原有代码中存在大量“同步等待异步”的情况(比如在主线程等待某个请求结果),经常引发UI卡顿问题。另外,未限制并发请求数量,也会导致系统资源占用过高。


解决方案:打造统一网络中间件

我们最终决定使用Swift的URLSession为核心封装一个轻量级的网络框架,并结合Promise模式实现链式调用,简化异步编程复杂度。下面是我们的整体设计思路:

技术选型与权衡

技术方案 优点 缺点
Alamofire 使用方便、社区活跃、支持链式语法 引入较大,非纯Swift原生
Moya + RxSwift 类型安全、抽象程度高 学习成本高,需要引入响应式编程
URLSession (原生) + PromiseKit 控制力强、轻量、无额外依赖 需要手动实现很多细节

考虑到App本身已有一定体量,为了降低维护成本,我们最终选择了基于原生URLSession + 自定义封装 + PromiseKit的方式。既能保持灵活性,又不会因为过多第三方依赖带来额外风险。


关键代码实践:构建统一网络引擎

我们封装了一个名为 NetworkManager 的类作为统一网络请求入口,大致如下:

class NetworkManager {
    static let shared = NetworkManager()
    
    private init() {}
    
    func request(_ api: APIEndpoint) -> Promise<APIResponse> {
        return Promise { seal in
            var urlRequest = URLRequest(url: api.url)
            urlRequest.httpMethod = api.method.rawValue
            urlRequest.allHTTPHeaderFields = api.headers
            
            if let body = api.body {
                urlRequest.httpBody = try? JSONSerialization.data(withJSONObject: body)
            }
            
            let task = URLSession.shared.dataTask(with: urlRequest) { data, response, error in
                if let error = error {
                    seal.reject(error)
                    return
                }
                
                guard let httpResponse = response as? HTTPURLResponse else {
                    seal.reject(NetworkError.invalidResponse)
                    return
                }
                
                guard (200...299).contains(httpResponse.statusCode) else {
                    seal.reject(NetworkError.serverError(code: httpResponse.statusCode))
                    return
                }
                
                guard let data = data else {
                    seal.reject(NetworkError.noData)
                    return
                }
                
                let json = try? JSONSerialization.jsonObject(with: data, options: .allowFragments)
                seal.fulfill(APIResponse(statusCode: httpResponse.statusCode, data: json))
            }
            
            task.resume()
        }
    }
}

定义统一接口协议

protocol APIEndpoint {
    var method: HTTPMethod { get }
    var path: String { get }
    var parameters: [String: Any]? { get }
    var headers: [String: String] { get }
    var body: [String: Any]? { get }
    
    var url: URL { get }
}

extension APIEndpoint {
    var url: URL {
        var components = URLComponents()
        components.scheme = "https"
        components.host = "api.example.com"
        components.path = "/v1\(path)"
        
        if let params = parameters {
            components.queryItems = params.map { key, value in
                URLQueryItem(name: key, value: String(describing: value))
            }
        }
        
        return components.url!
    }
    
    var headers: [String: String] {
        return [
            "Content-Type": "application/json",
            "Authorization": "Bearer \(UserDefaults.standard.string(forKey: "token") ?? "")"
        ]
    }
}

实际调用示例

struct GetUserInfo: APIEndpoint {
    typealias ResponseType = UserInfoModel
    
    var method: HTTPMethod { .get }
    var path: String { "/user/info" }
    var parameters: [String : Any]? { nil }
}

// 调用时:
NetworkManager.shared.request(GetUserInfo())
    .map { response in
        UserInfoModel.from(json: response.data)
    }
    .done { model in
        print("获取用户信息成功:$model.name)")
    }
    .catch { error in
        print("出错了:$error.localizedDescription)")
    }

这样做的好处是:

  • 所有网络请求都被收归一处
  • 接口定义更加清晰规范
  • 可扩展性强,后期可以接入Mock数据、埋点等功能

踩坑经验分享

技术原理图-1

坑 1:主线程阻塞导致卡顿

有一次我们在一个列表页发起多个并发请求,结果UI变得异常卡顿。排查后发现,我们在回调里用了类似这样的代码:

var result: Data?
let semaphore = DispatchSemaphore(value: 0)

NetworkManager.shared.request(someAPI).then { data in
    result = data
    semaphore.signal()
}.wait()

// 然后继续操作result...

这种“伪异步”的方式在Swift中非常危险,尤其是在主线程上调用.wait()会完全阻塞事件循环,导致页面失去响应。

解决方法:全面删除所有同步等待逻辑,改为真正的异步调用:

NetworkManager.shared.request(someAPI)
    .done { data in
        // 主线程更新UI没问题
        self.updateUI(with: data)
    }
    .catch { error in
        print("出错了")
    }

坑 2:HTTPS证书校验失败

测试环境有时候使用自签名证书,导致APP无法正常联网。如果不处理SSL Pinning就会被系统拦截报错。

解决方案:针对测试环境允许忽略证书检查:

func urlSession(_ session: URLSession, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
    if challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust {
        if challenge.protectionSpace.host.contains("test") {
            let credential = URLCredential(trust: challenge.protectionSpace.serverTrust!)
            completionHandler(.useCredential, credential)
        } else {
            completionHandler(.performDefaultHandling, nil)
        }
    } else {
        completionHandler(.performDefaultHandling, nil)
    }
}

⚠️ 注意:生产环境必须启用完整的证书校验逻辑。


坑 3:并发数量过多导致CPU飙升

初期我们开启了大量并发请求,在低端设备上出现内存暴涨甚至Crash的情况。

优化手段

  1. 限制最大并发数:通过OperationQueue设置最大操作数。
  2. 设置超时时间:对单个任务设置超时,防止一直挂起。
  3. 合理取消任务:当页面关闭时取消当前页面未完成的请求。
let queue = OperationQueue()
queue.maxConcurrentOperationCount = 5

queue.addOperation {
    // 发起请求
}

效果总结:重构后的收益

经过大约两个迭代周期的重构,我们得到了明显提升:

指标 改造前 改造后
平均接口调用时间 870ms 620ms
页面加载闪退率 2.1% 0.3%
开发者协作效率 高耦合难复用 易读易测
错误提示一致性 不统一 全部统一风格
测试覆盖率 30% 提升到75%+

更重要的是,我们建立了一套可持续演进的网络基础设施,后续如果要新增Mock、埋点、性能监控等功能,都可以在统一入口轻松拓展。


经验总结与建议

1. 抽象先行,统一接口结构

无论你用什么网络库,先定义好统一的接口规范,有助于后续维护和升级。

2. 异步编程别怕麻烦

Swift的异步生态越来越完善了,不管是Combine、async/await还是PromiseKit,选一个适合你项目的异步方案,坚决避免同步等待。

3. 错误处理一定要统一

尽早统一错误码体系,尤其是跨平台项目,最好和Android、Web共用一套标准。

4. 别忽视性能瓶颈

小流量不代表不需要优化。即使是一个轻量App,也可能会在低配设备上暴露出性能问题。

5. 日志和监控必不可少

加入网络请求耗时统计、接口成功率埋点,这些都能帮助你更快定位线上问题。


写在最后:一次重构的价值远不止眼前所见

这次网络模块的重构虽然花了我们不少时间和精力,但也让我深刻体会到,“看似简单”的网络模块背后其实隐藏着无数细节。它不仅关系到App的稳定性、性能表现,还直接影响到产品迭代的速度和质量。

如果你正在接手一个“历史悠久”的项目,不妨从网络模块开始慢慢重构——这不仅能改善代码结构,还能让你更深入地理解整个产品的通信机制。

希望这篇来自实战的经验分享能帮你在工作中少走一些弯路,如果你也经历过类似的重构故事,欢迎一起交流!

🔚

评论 0

最热最新
暂无评论
匿名用户Lv.1
0
影响力
0
文章
0
粉丝