技术探索与实践的一些思考
为什么写这篇文章?

工作五年来,我经历了从初入职场的懵懂到逐渐成熟独立开发者的转变。这过程中遇到过各种各样的问题——有些看似简单却隐藏着深坑,也有些复杂的问题最终通过一个巧妙的思路得以解决。而更让我感慨的是,在不同项目中,面对技术选型、架构调整和性能优化时做出的决策,很多时候都直接影响了项目的成败。
作为 iOS 开发者,我们每天都在跟 UIKit、Swift、Objective-C、系统 API 打交道,但真正推动成长的,往往不是掌握了某个新特性或新技术,而是如何在实际项目中发现问题、权衡方案、落地实践并持续改进的过程。这篇文章就源于我在某一次项目迭代中的经历,我想分享一个具体的技术挑战、我们的尝试、踩过的坑以及最后收获的经验教训。希望这些内容能给正在或者即将面临类似问题的同学带来一些参考和启发。
一个问题引发的一连串思考

那是在我参与的一个金融类 App 的重构项目中。这个 App 原本是一个典型的“巨石应用”,所有业务模块都耦合在一起,随着功能越来越多,代码库也越来越臃肿,维护成本极高。产品经理经常提出新的需求,而每次改动都需要小心翼翼地避开“雷区”。为了提高开发效率、降低维护成本,我们决定进行一次整体架构升级,目标是实现模块化、提升组件复用率,并为后续的跨平台打下基础。
项目初期,我们面临几个关键选择:是否继续使用 Objective-C,还是全面转向 Swift?是否采用 MVVM 或者 VIPER 架构?是否引入 SwiftUI 或继续沿用 UIKit?同时,还要考虑团队成员的技能水平、现有代码的兼容性以及对发布流程的影响。
最终我们选择了混合 Objective-C 与 Swift(因为老代码太多,迁移需要时间),并采用 VIPER 架构作为新模块的基础结构。不过,在实施过程中,我们遇到了不少意想不到的问题。
模块间通信成了大麻烦

最大的挑战之一,出现在模块解耦后的通信机制设计上。按照最初的设想,每个模块都是独立的 Framework,彼此之间不能直接引用,只能通过接口进行交互。理论上讲这确实是个好主意,但在实际操作中,我们很快就遇到了难题。
举个例子,A 模块要跳转到 B 模块中的某个页面,但 A 不知道 B 是否存在,也不能直接导入 B 模块的头文件。怎么办?最简单的做法是通过 URL 路由(如 router.open(url: "b://details?itemId=123"))的方式进行跳转,这种方式在很多 App 中都有应用。但问题是,我们在处理参数传递、生命周期管理、路由注册等方面遇到了不少问题,比如:
- 路由路径容易出错,尤其是在多人协作的时候
- 页面传参方式不统一,有人用 Dictionary,有人用自定义对象,导致维护困难
- 某些情况下需要返回值(比如弹窗选择结果),而传统的 URL 路由不支持这种模式
- 编译期无法检查是否存在对应的路由,只能运行时报错
这些问题让我们意识到,光靠简单的 URL 路由并不能完全满足我们的需求。
我们是怎么解决的?

经过几轮讨论和技术调研,我们决定采用服务协议 + 动态绑定的方式来替代传统路由机制。核心思想是:每个模块对外暴露一组“服务接口”(Protocol),其他模块通过依赖注入的方式获取该接口实例,从而调用对应的功能。
比如对于跳转功能,我们可以定义一个 BServiceType 协议:
protocol BServiceType {
func showDetailsPage(itemId: String, from viewController: UIViewController)
}
然后在 B 模块中实现它:
class BServiceImpl: BServiceType {
func showDetailsPage(itemId: String, from viewController: UIViewController) {
let vc = DetailsViewController(itemId: itemId)
viewController.navigationController?.pushViewController(vc, animated: true)
}
}
然后我们需要一个机制,将这个实现类注册到全局可用的地方。我们借助了一个轻量级的服务注册器(类似于 Service Locator 模式)来完成:
protocol ServiceLocator {
func register<T>(_ service: T, for type: T.Type)
func resolve<T>(for type: T.Type) -> T?
}
class DefaultServiceLocator: ServiceLocator {
private var services: [String: Any] = [:]
func register<T>(_ service: T, for type: T.Type) {
let key = String(describing: type)
services[key] = service
}
func resolve<T>(for type: T.Type) -> T? {
let key = String(describing: type)
return services[key] as? T
}
}
这样,各个模块就可以通过这个 ServiceLocator 来访问其他模块提供的服务,而无需直接依赖它们的实现类。例如 A 模块在初始化后,可以尝试解析 B 的服务:
if let bService = ServiceLocator.shared.resolve(for: BServiceType.self) {
bService.showDetailsPage(itemId: "123", from: self)
} else {
// 可以在这里降级处理,比如提示用户模块未加载
}
这样的方式有几个明显优势:
- 编译期安全:只要 Protocol 是稳定的,调用方就不需要关心具体的实现逻辑
- 便于测试:可以在测试环境中注入 Mock 实现
- 解耦明确:模块之间的依赖关系清晰,不需要互相引用源码
当然,这也带来了一些额外的工作,比如需要维护服务注册表、处理服务版本更新等问题,但我们可以通过自动化工具辅助生成注册代码来缓解这个问题。
实践中的几个坑

虽然这个方案整体上取得了不错的效果,但在落地过程中也遇到了一些意料之外的问题:
1. Protocol 太多太杂
一开始我们没有很好地区分哪些功能适合抽象为接口,导致接口数量剧增,甚至出现了重复定义的情况。比如有的接口只有单个方法,而且只在一个地方被调用。后来我们做了规范化处理:
- 只有需要跨模块调用的功能才需要抽象成 Protocol
- 接口命名统一加上 Module 名前缀(如
AccountServiceType,PaymentServiceType) - 每个 Protocol 集中管理,避免分散在各个文件夹中
2. 服务注册时机不当
某些服务在 App 启动时就被注册了,而有些则是懒加载的。如果不小心在服务未注册时就调用了接口,就会出现 nil 引用。后来我们规定了标准的模块初始化流程,确保服务注册在合适的时机完成。
3. Swift 和 Objective-C 混合调用的问题
虽然大多数模块已经迁移到了 Swift,但还有一些关键模块仍然保留 Objective-C。而 Protocol 在 Swift 和 OC 混合使用时,需要注意类型匹配问题。例如 Swift 类实现的 Protocol 如果想被 OC 调用,必须继承自 NSObject 并加 @objc 标记。否则会出现无法解析的情况。这点刚开始没注意,导致部分服务在 OC 侧调用失败。
4. 某些场景无法完全解耦
比如推送通知进入某个页面的情况。由于推送点击事件通常是在 AppDelegate 层处理的,这时候如果目标模块还没加载,会导致无法正确跳转。为此我们做了一个统一的中间页处理,先展示 Loading 界面,等目标模块加载后再真正跳转过去。
结果怎么样?
这套方案上线后,我们明显感受到以下几点变化:
- 维护效率提升:模块间的边界更加清晰,修改某一个模块不再牵一发而动全身
- 新人上手更快:有了服务接口和规范化的交互方式,新人理解整个系统的难度降低了
- 可扩展性更强:新功能的添加更容易找到切入点,尤其是跨团队协作时
- 稳定性有所提高:接口的抽象让部分错误提前暴露在编译阶段,而不是运行时才发现
此外,我们也开始尝试基于这套服务机制做一些高级玩法,比如远程配置动态服务绑定、AB 测试等功能模块自动切换等,进一步增强了 App 的灵活性。
几点经验分享
如果你也在做模块化/组件化相关的改造,这里有几个建议供你参考:
1. 分清楚“内部调用”和“外部服务”
并不是所有功能都需要抽象成服务接口。有些只是模块内部的私有行为,没必要对外暴露。一定要区分清楚什么该暴露,什么不该暴露,否则很快就会陷入 Protocol 泛滥的困境。
2. 设计接口时要考虑扩展性和兼容性
接口一旦定下来,修改起来很麻烦。所以在设计接口时要考虑它的扩展性,比如是否预留泛型支持、是否允许未来增加新方法而不影响旧代码等。也可以考虑使用 extension 加默认实现的方式减少接口变更带来的冲击。
3. 模块初始化流程要统一
每个模块都应该有一个标准的初始化入口,用于注册服务、监听事件等。这个过程要可控,最好能按需初始化,而不是一股脑地全部加载。
4. 工具链要跟上
像 Protocol 注册、依赖分析这些事情手动去做会很累也很容易出错。建议配合脚本或 Xcode 插件来自动生成注册代码、检测模块依赖情况,甚至可以结合 CI 自动校验。
5. 给自己留条退路
有时候模块化做过了头反而会让代码变得难以维护。所以我们要保持一定的灵活性,比如在某些场景下允许短时间内的强依赖(比如 Debug 环境),或者提供临时的“桥接层”过渡。总之,别把架构做得太死,适配实际情况最重要。
写在最后

这次的技术探索和实践让我深刻体会到:所谓优秀的架构,不是靠堆砌一堆高大上的概念,而是能够真正服务于当前业务的发展,适应团队的能力水平,并具备良好的演进空间。
回头来看,当时做的许多技术决策并不完美,也有不少弯路,但正是这些不断试错、不断调整的过程,让我积累了宝贵的经验。
希望这篇文章能给大家带来一些启发。无论是模块化、服务解耦,还是架构设计上的取舍,都没有绝对正确的答案。关键是根据自己的项目背景、团队状况和技术趋势,找到最合适的那一套方案。
毕竟,技术始终是服务于业务的,真正的价值在于解决问题,而不是追求“最酷炫”的实现。

评论 0