从崩溃到稳定:一次复杂项目重构中的技术踩坑与成长
开头:关于“踩坑”的那些事儿

作为一个有五年iOS开发经验的老兵,我深知“踩坑”几乎是每个项目都无法避免的一部分。尤其是当你接手一个历史悠久的旧项目、面对技术债务堆积如山时,“踩坑”往往变成一种常态。
今天想和大家分享的是我在去年参与一个电商类App重构过程中的一段经历——一段从崩溃频发到性能优化、从代码混乱到架构清晰的技术探索之旅。这不仅是对技术方案的选择过程,更是一次思维模式和工程实践的全面提升。
如果你也曾为遗留代码头疼、为性能瓶颈焦虑,又或者你正在准备一场大型重构,希望这篇记录能给你带来一些启发。
背景介绍:一个“老而乱”的项目

项目背景
我们接手的这个App最初由外包团队开发,历时3年迭代,已经积累了不少功能模块。但由于缺乏统一的技术规范和架构设计,代码结构相当混乱:
- MVC架构被滥用得面目全非,VC(View Controller)臃肿不堪
- 多个网络请求库混用,接口错误处理不一致
- 数据缓存策略分散,有些地方甚至直接写SQL语句
- 线程管理混乱,主线程卡顿严重
- 日志系统缺失,线上崩溃难以定位
上线初期崩溃率一度达到10%,用户反馈频频出现卡顿、白屏、数据错乱等问题。
作为新加入的核心开发成员,我和团队一起承担起了这次“救火+重构”的任务。
遇到的挑战:技术债多如牛毛

在接手后不到一周的时间,我们就遭遇了一系列棘手的问题。
1. 崩溃高发期的痛苦
刚接手中就收到测试团队反馈:某个商品详情页频繁闪退。通过 Crashlytics 查看日志发现崩溃堆栈如下:
Thread 1 Queue : com.apple.main-thread (serial)
0 libobjc.A.dylib 0x000000018290e42c objc_retain + 0
1 UIKitCore 0x000000019a7fdeac -[UICollectionView _createPrepaintedDecorations] + 576
2 UIKitCore 0x000000019a7fc8cc __51-[UICollectionView _updateVisibleCellsNow:]_block_invoke + 84
...

看起来像是 UICollectionView 的布局问题,但奇怪的是这个问题只出现在特定设备上。调试后发现问题根源在于:
- 有一个自定义的
UICollectionViewLayout实现了layoutAttributesForSupplementaryElementOfKind:atIndexPath:方法。 - 某些情况下返回了
nil,但在 UICollectionView 内部会尝试 retain,最终导致崩溃。
解决方式是对所有可能返回 nil 的方法进行安全包装,同时增加 layout 的异常检测机制。
小插曲:这个 bug 我们前后排查了两天,才找到罪魁祸首,教训是:对于 UICollectionView 或 UITableView 的 layout 和 dataSource 相关逻辑,必须做好容错!
2. 接口混乱带来的灾难
另一个大问题是接口混乱。整个 App 里竟然用了三种不同的网络请求库:AFNetworking、Alamofire,还有一个封装得很烂的 HTTPClient 工具类。
更糟的是,这些接口错误处理方式各异,有的返回 nil,有的抛异常,有的直接断言失败。最夸张的一次,我们在登录流程中触发了一个空指针异常,导致用户无法登录,影响面极大。
我们果断决定统一接口层,最终选用了 Moya + Alamofire + Result 这套组合,并引入了统一的错误处理协议:
protocol APIErrorProtocol {
var localizedDescription: String { get }
var retryable: Bool { get }
}
通过中间层适配器将各个请求库抽象为统一入口,使得后续维护和扩展更加灵活。
3. 性能瓶颈:页面加载慢 & 内存飙升
某天产品抱怨:“首页滑动卡得像拖拉机。” 我们开始分析卡顿原因,使用 Instruments 工具抓取主线程调用栈发现:
- 图片加载没有做异步处理,且图片解码占用了大量主线程资源
- 首页商品卡片渲染过程中多次调用 Core Graphics 绘图操作
- 大量的 UILabel 字符串拼接和富文本绘制未做缓存
于是我们引入了以下优化措施:
- 使用 SDWebImage 做图片异步加载与缓存
- 图文混合内容采用 CoreText 自定义绘制组件(后续改为 TextKit)
- 利用 NSCache 缓存高频重复绘制结果
- 对 UICollectionViewCell 的高度计算进行懒加载优化
优化后首页帧率从平均 40 FPS 提升至 58 FPS 以上,内存占用减少约 30%。
解决思路与实现方案
一、架构升级:从 MVC 到 MVVM
为了提升代码可维护性和模块化程度,我们决定从原来的 MVC 架构逐步过渡到 MVVM(Model-View-ViewModel)。
为什么选择 MVVM?
- 明确职责划分:VC 只负责视图控制,业务逻辑下沉到 ViewModel
- 更容易进行单元测试
- 视图与数据绑定天然契合 KVO 或 Combine/RxSwift 等响应式框架
我们并没有一开始就引入复杂的响应式框架,而是先从最基础的数据绑定做起。比如用闭包 + protocol 的方式实现简单的双向绑定:
class LoginViewModel {
var username: String = "" {
didSet {
usernameDidChange?(username)
}
}
var password: String = "" {
didSet {
passwordDidChange?(password)
}
}
var usernameDidChange: ((String) -> Void)?
var passwordDidChange: ((String) -> Void)?
}
这种轻量级 MVVM 实践帮助我们快速理清了数据流向,减少了 VC 中的胶水代码。
二、线程调度优化:告别主队列滥用
在分析线程使用情况时,发现很多本来应该后台执行的操作都放在主线程完成,比如解析 JSON、生成模型、甚至本地数据库查询。
我们逐步把这些操作改造成使用 GCD 分发,并引入了 OperationQueue 来更好地控制并发优先级。
例如,在商品详情页中,我们将数据加载拆分为:
- 本地缓存优先读取(同步)
- 本地无缓存则开启后台线程去拉取网络数据(异步)
- 收到数据后回到主线程更新 UI
DispatchQueue.global(qos: .utility).async {
let data = try? Data(contentsOf: url)
let product = parse(data)
DispatchQueue.main.async {
self.updateUI(with: product)
}
}
同时,我们也规范了全局并发访问的策略,比如数据库访问统一走串行队列,确保线程安全。
三、Crash 稳定性建设:从被动防御到主动监控
最初的崩溃率高达10%左右,主要原因是:
- 异常的数组越界
- NSDictionary setObject:nil forKey:
- 野指针访问(KVO未remove)
我们做了三件事来解决这些问题:
- 全面接入 Crashlytics,并设置报警机制;
- 添加异常捕获模块,包括:
- OC 异常(NSSetUncaughtExceptionHandler)
- Swift 异常(try catch 的合理使用)
- 网络超时兜底机制
- 建立灰度发布流程,每次上线前小范围灰度观察崩溃趋势
此外,针对常见的 NSDictionary nil 键值崩溃,我们还封装了一个 SafeDictionary:
class SafeDictionary<KeyType: Hashable, ValueType> {
private var internalDict: [KeyType: ValueType] = [:]
subscript(key: KeyType) -> ValueType? {
get {
return internalDict[key]
}
set {
guard let value = newValue else { return }
internalDict[key] = value
}
}
}

虽然不能覆盖所有场景,但至少避免了最常见的几个崩溃点。
效果总结:从“破船”到“战舰”
经过三个月的努力,整个项目的稳定性有了显著提升:
| 指标 | 优化前 | 优化后 |
|---|---|---|
| 崩溃率 | ~10% | <0.5% |
| 平均帧率 | 38 FPS | >58 FPS |
| 单个 ViewController 代码行数 | 平均 1000+ 行 | 控制在 300 行以内 |
| 团队协作效率 | 各自为政 | 统一 Code Style + 架构分层 |
更重要的是,我们建立起了一整套可持续改进的工程体系,比如:
- CI/CD 流程:提交即构建 + TestFlight 自动发布
- Code Review 制度:重点代码强制 Review
- 技术文档沉淀:架构演进、常见问题等都有归档
经验分享:踩坑是成长的必经之路
回顾这段重构旅程,我想给同行几点建议:
1. 不要惧怕“脏活”,先干起来再谈优雅
很多时候我们总想着“等我把架构设计好再开始”,其实根本不需要。先把最痛的点解决掉,边干边优化才是正道。
比如我们一开始没急着引入 MVVM 或 Clean Architecture,而是先从最小改动做起,逐步清理 VC 中的副作用逻辑。
2. 工程质量要靠制度保障,而不仅是个人觉悟
- 统一命名规范
- 强制 Code Review
- 设置 Lint 规则(SwiftLint + OC lint)
- 单元测试覆盖率纳入标准
这些都不是额外负担,而是工程质量的保障手段。
3. 崩溃监控和日志跟踪必须前置
不要等到线上出事才想起埋点和监控。尽早接入 Crashlytics、Bugsnag 这类工具,并学会利用 Xcode Organizer、Instruments 这些原生工具做深度分析。
4. 技术选型要有长期视野,别图一时爽快
有时候我们会因为某个第三方库“简单好用”而盲目采用,但一定要考虑它是否适合长期维护。比如选择网络库时不仅要关注接口友好度,还要看社区活跃度和兼容性。
5. 学会用工具武装自己
- Instrument:追踪内存泄漏、CPU热点、FPS 等
- Reveal:查看界面层级,排查渲染问题
- Charles / Proxyman:抓包调试网络接口
- Swift Playground:验证复杂逻辑和算法
这些工具就像战士手中的武器,掌握它们能让你在战斗中更有底气。
写在最后:技术路没有捷径,只有踏实前行
这一年下来,我对“开发者”的理解也发生了变化。以前总觉得只要代码写得好就够了,现在明白:写出稳定的系统、推动团队形成良好的工程文化、不断优化用户体验,才是真正意义上的“技术驱动”。
这篇文章写到这里已经快两千字了,愿你也曾在自己的工作中经历过这样“一边踩坑一边成长”的过程。如果有收获欢迎留言讨论,也欢迎分享你在重构项目或解决技术难题上的实战经验。
毕竟,每个程序员的成长路上,都少不了那么几次踩坑的修行。

评论 0