从零开始重构:一位iOS工程师的技术探索与落地实践

刘浩宇_云计算
2025-06-16 00:18
阅读 716

引言:为什么我想写这篇文章?

引言:为什么我想写这篇文章?

大家好,我是张工,在 iOS 开发领域已经摸爬滚打了五年。做过从无到有的 App 开发,也经历过多个版本迭代、架构演进的项目重构。如果说这几年有什么让我感触最深的,那就是技术选型和工程实践从来不是一蹴而就的事,它总是在一次次踩坑、反复推敲中一点点打磨出来的。

今天想跟大家分享的是我亲身经历的一次大项目重构过程。那是去年我们公司准备升级一个老项目的时候,遇到了很多棘手的问题,比如代码结构混乱、模块间耦合严重、性能问题频发等。这些问题在业务快速发展的背景下被不断放大,最终到了必须动刀的程度。

这篇文章不会空谈理论,也不会堆砌概念。我会结合我们团队当时的实际情况,从项目背景 → 遇到的问题 → 技术决策 → 具体实践 → 坑点总结 → 最终效果这些方面出发,分享整个重构过程中的真实思考和技术路线。


项目背景:旧船遇上风暴,是修还是换?

项目背景:旧船遇上风暴,是修还是换?

我们要改造的是公司内部使用了四年的核心业务 App —— “慧企通”。这是一个面向中小企业的移动办公平台,功能包括审批流程、考勤打卡、客户跟进、消息通知等。随着用户规模的增长和新需求的持续上线,App 的性能下降日益明显,尤其体现在启动时间变长、界面响应变慢、崩溃率上升等方面。

项目最初由多个开发团队共同维护,没有统一的架构规范,导致代码中存在大量重复逻辑和难以维护的状态管理方式。而且由于历史原因,一些底层库已经停止维护(比如 AFNetworking 还未全面替换成 Alamofire),还有部分模块仍然用 Objective-C 写成,Swift 和 OC 混编的复杂性进一步提高了维护成本。

简单来说,这个项目像是一艘“多年未翻修”的旧船,在风浪来临时显得格外脆弱。面对即将推出的新版本和更复杂的业务需求,我们决定对项目进行一次结构性重构。


面临的挑战:问题不只是“旧”,更是“乱”

面临的挑战:问题不只是“旧”,更是“乱”

重构过程中遇到的挑战远超预期,主要有以下几点:

  1. 架构混乱:MVVM、MVC、VIPER 混杂在一起,不同模块采用不同的模式,新人接手困难,状态难以统一。
  2. 技术栈混杂:Swift 和 Objective-C 交替出现,部分组件还依赖于废弃的第三方库。
  3. 性能瓶颈突显:主界面加载耗时过长(平均超过 800ms),列表滑动卡顿,图片加载不流畅。
  4. 测试覆盖率低:几乎没有单元测试,UI 测试只覆盖了几个核心流程。
  5. 协作沟通成本高:不同团队维护不同模块,缺乏统一规范,每次合并代码都是一次灾难。

当时我们的目标很明确:通过合理的架构设计 + 状态管理优化 + 渐进式重构,让 App 在稳定性和可维护性上有质的提升。


解决方案:分阶段、渐进式重构策略

解决方案:分阶段、渐进式重构策略

我们并没有选择“一把梭”式的彻底重写,而是采取了小步快跑、边拆解边重构的方式。大致分为以下几个阶段:

阶段一:架构统一

我们评估了几种流行的架构风格后,最终决定采用 MVVM + Coordinator 模式作为新的基础架构:

  • MVVM 让 UI 层和数据层职责清晰分离;
  • Coordinator 负责导航逻辑和模块间的通信,解决了传统 VC Segue 和 delegate 造成的混乱;
  • 同时我们引入了 Clean Architecture 思想,将业务逻辑下沉,提高复用性。

选择 MVVM + Coordinator 是因为我们既要保持简洁性,又要为后续的 TDD 打下基础。如果一开始上太复杂的框架(比如 Redux 或者 RxSwift 整套体系),反而会增加学习和迁移成本。

阶段二:状态集中化

我们引入了 State Management Layer 来统一全局状态管理。这里我们做了技术选型调研:

方案 优点 缺点
SwiftUI Binding 简洁易用,适合纯 SwiftUI 项目 对 UIKit 支持不佳
Combine / NotificationCenter 官方支持,响应式强 易造成事件爆炸
Redux-like Store (自定义) 单向数据流、易调试 实现成本较高
Mobius.swift(Google 开源) 稳定性强,有回溯机制 文档少,社区不活跃

最终我们选择了自研的简易单向状态更新机制 + ViewModel 监听更新 UI 的模式,兼顾了灵活性和可控性。这套机制后来也被封装成了一个小组件,供其他项目复用。

阶段三:模块拆解 & 依赖注入

我们把原来的“大 AppDelegate”分解成多个 Feature Module,并通过 Dependency Injection Container 统一管理服务注册和依赖注入。

比如登录模块、审批模块、通知模块,各自拥有独立的生命周期、路由入口和对外暴露的接口。这种做法极大降低了模块间的耦合度,同时也便于做隔离测试和灰度发布。

我们自己实现了一个轻量级的 DI 容器(类似 Swinject,但简化了泛型处理),并通过编译脚本生成注入配置文件,避免手动管理依赖带来的疏漏。

阶段四:性能优化 + 图片懒加载

针对启动慢的问题,我们做了如下几件事:

  • 使用 Xcode 的 Time Profiler 工具定位主线程阻塞点,将非关键初始化任务延迟执行或异步加载;
  • 图片加载改用 SDWebImage 新版,启用 prefetcher 提前加载可视区域外的内容;
  • 列表数据分页请求改为预加载策略,减少空白等待时间;
  • 使用 LazyView + Combine 的 debounce 机制优化搜索输入体验。

另外我们在冷启动阶段接入了 MetricKit,用于收集性能指标,为后续优化提供数据支撑。


代码实践:几个关键模块的代码片段

ViewModel 示例(MVVM + Combine)

class HomeViewModel: ObservableObject {
    @Published var tasks: [Task] = []
    private let taskService: TaskServiceProtocol
    
    init(taskService: TaskServiceProtocol) {
        self.taskService = taskService
        fetchTasks()
    }

    private func fetchTasks() {
        taskService.fetchTodayTasks { result in
            DispatchQueue.main.async {
                switch result {
                case .success(let tasks):
                    self.tasks = tasks
                case .failure(let error):
                    print("Error fetching tasks: $error.localizedDescription)")
                }
            }
        }
    }
}

开发工具界面-1

Coordinator 示例

class AppCoordinator {
    private let navigationController: UINavigationController

    init(navigationController: UINavigationController) {
        self.navigationController = navigationController
    }

    func start() {
        let homeViewModel = HomeViewModel(taskService: TaskService())
        let homeVC = HomeViewController(viewModel: homeViewModel)
        navigationController.pushViewController(homeVC, animated: false)
    }

    func showDetail(for task: Task) {
        let detailVM = DetailViewModel(task: task)
        let detailVC = DetailViewController(viewModel: detailVM)
        navigationController.pushViewController(detailVC, animated: true)
    }
}

状态管理示例(简化版)

enum AppStateAction {
    case updateFilter(FilterType)
    case refreshData
}

struct AppState {
    var filter: FilterType = .all
    var data: [DataItem] = []
}

class Store {
    static let shared = Store()

    private(set) var state: AppState = .init()
    private var subscribers: [(AppState) -> Void] = []

    func dispatch(_ action: AppStateAction) {
        switch action {
        case .updateFilter(let filter):
            state.filter = filter
        case .refreshData:
            // 从网络重新拉取数据
            fetchDataAndSetState()
        }

        notifySubscribers()
    }

    func subscribe(_ subscriber: @escaping (AppState) -> Void) {
        subscribers.append(subscriber)
    }

    private func notifySubscribers() {
        subscribers.forEach { $0(state) }
    }
}

踩坑经验:那些年我们一起掉过的坑

在整个重构过程中,我们踩了不少坑,有些甚至一度让我们怀疑人生。下面我列举几个印象特别深刻的例子:

1. Swift 包引用冲突,引发循环依赖

刚开始尝试将业务模块封装成 SPM 包时,因为多个包之间互相依赖 Model 类,结果出现了 circular dependency detected 错误。这个问题其实是因为没有做好接口抽象和公共协议剥离造成的。后来我们通过抽取 Core 模块专门存放 Protocol 和通用模型才解决。

2. Combine 的内存管理问题

在 ViewModel 中监听 Combine 的 @Published 属性时,很容易忘记 cancellable 的持有方式,造成 retain cycle。比如我们有位同事写的 ViewModel 在销毁时没有释放订阅,导致 VC 泄露。后来统一用 AnyCancellable(store:in:) 管理生命周期才搞定。

3. 图片缓存机制设计不合理

最初的 SDWebImage 使用方式非常粗暴,图片路径变化后缓存没清除,导致旧图一直残留。后来我们加了一层 URLHasher,根据不同业务参数生成唯一的 key,再走缓存,从根本上解决问题。

4. 多人协作的 Merge地狱

重构过程中分支太多,每个 feature branch merge 回 develop 都要解决各种冲突,尤其是 storyboard 文件几乎无法自动合并。于是我们果断停用 storyboard,全部改用 xib + code base UI,从此解放了生产力。


结果与收益:重构后的改变

经过差不多半年的时间,我们完成了这次重构。最终的结果超出预期:

  • 启动时间从平均 850ms 降低到 470ms;
  • 页面切换流畅度明显提升,基本消除卡顿现象;
  • Crash 率下降 62%,性能类异常显著减少;
  • 重构后新增模块开发效率提升约 30%,代码复用率提高;
  • 单元测试覆盖率从不足 10% 提升到 58%,CI/CD 更加顺畅;
  • 最重要的是:新同学入职上手速度明显加快,文档齐全 + 架构清晰,不再是“看脸学代码”。

经验分享:给读者的一些实用建议

如果你也在考虑重构自己的项目,或者刚接到一个老项目不知道从哪儿下手,以下是我在这些年踩坑路上积累下来的一些实战经验:

✅ 1. 不要盲目追求“新技术”

很多同学一听“重构”就想上 RxSwift、ReactiveX、Redux、SwiftUI…… 但实际上,技术只是工具,关键是能否解决你当下的问题。我们之所以没有一开始就引入 SwiftUI,就是因为它对现有 UIKit 项目的兼容性和适配成本太高。

✅ 2. 架构清晰比库炫技更重要

好的架构能让团队高效协作,而不是每个人都在造轮子。哪怕你用的是最朴素的 MVC,只要你能把它写得层次分明、易于扩展,那也是好架构。

✅ 3. 渐进式迁移比一锤子买卖更稳妥

不要试图一次性全部重写。我们采用了 Feature by Feature 的替换方式,先做最小可行重构(MVP),再逐步推进。这样即使出了问题也能快速回滚。

✅ 4. 代码即文档,注释即责任

很多人觉得代码只要有类型提示就 OK,但在重构过程中你会发现,没有足够注释和文档的项目就像一个没有地图的迷宫。我们后来强制要求所有公开 API 必须加 Documentation Comment,并定期生成文档站点。

✅ 5. 测试不是形式主义,而是护城河

单元测试和集成测试看似浪费时间,实则是重构过程中的安全网。一旦哪天你改出个 bug,回头看看测试是否覆盖,心里就有底了。别怕写测试,它是保护你的盔甲。


尾声:重构不仅是代码的事,更是心路的修炼

最后我想说,作为一个开发者,我觉得重构是一个特别锻炼心性的过程。它不仅仅需要你在技术上的判断力和执行力,也需要你在面对压力、不确定性甚至是质疑时依然能坚持初心。

记得有一次,产品提了个紧急需求:“明天就要上线新功能,你们重构进度怎么样?”我当时内心真的是五味杂陈。但我们顶住了压力,在不牺牲质量的前提下,用两周内完成了一个完整 Feature 的重构并上线,而且稳定性表现远超预期。

这场战役结束后,我们团队真正实现了从“救火队”到“基建队”的转变。我不敢说我们做的每一个技术选型都是完美无瑕的,但我相信,每一次权衡、每一次讨论、每一段代码的背后,都有我们对产品负责的态度。

所以如果你想问我——要不要重构?我会说:

如果你觉得现在的代码让你束手束脚,那它就值得重构。

如果你愿意多花一点时间把根基打牢,那么未来一定会感谢现在努力的你。


参考资料 & 推荐阅读


作者信息
张工 | 5年 iOS 工程师 | 曾主导多款企业级 App 架构重构
GitHub: @zhanggong
欢迎留言交流,一起成长 🚀

评论 0

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