3、iOS端SDK架构设计:Category、Protocol与模块拆分

做iOS SDK和做App有个很大的区别——你的代码会被别人合进项目里,你没法控制调用方怎么用。配合不好、命名冲突、Category 覆盖……这些坑我都踩过。今天聊的这三个点,说白了就是让 SDK 既灵活又稳当的设计手段。

3.1 Category — 用扩展代替继承

Objective-C 的 Category,是给已有类添加方法的利器。在 SDK 里我一般这么用:

  • 功能分组:把某个类的方法按业务拆成多个 category 文件。比如 NSString+DeviceInfo、NSString+FormatUtils,方便按需引入。
  • 私有方法隔离:定义在 .m 文件里的 category,外面看不到,相当于内部实现细节。
  • 系统类扩展:给 UIView 加个 sd_top 这种便捷属性,降低调用方的理解成本。
个人习惯:每个 category 文件只放 3~5 个方法,超过就考虑是不是该拆一个新类了。

但 Category 有潜规则。最典型的是方法覆盖——如果两个 category 实现了同名方法,最后编译的那个会生效,而且没有编译警告。我曾经在线上遇到过这种问题:一个第三方 SDK 的 category 和我们的重名了,用户反馈某个按钮不响应,查了半天才发现是方法被静默替换了。

避坑指南:我建议给 SDK 内部 Category 的方法名都加上前缀,比如 msdk_。同时尽量避免覆写系统类的方法,真要覆写就做好兼容测试。

另一个要注意的是 +load 方法。每个 Category 的 +load 都会被执行,而且执行顺序不确定。如果你在 +load 里做全局注册或方法交换,很可能被多个 Category 同时触发,产生竞态条件。我的做法是:所有 Category 的 +load 只做一件事——调用一个注册中心的方法,由注册中心去统一处理。

3.2 Protocol — 面向接口编程

Protocol 在 SDK 里的价值,远远超过普通 App 开发。它让模块之间只靠接口契约通信,具体实现可以随便换。

比如我设计一个日志模块,会定义 MSDKLoggerProtocol:

@protocol MSDKLoggerProtocol <NSObject>
- (void)log:(NSString *)message level:(MSDKLogLevel)level;
@optional
- (void)logWithTag:(NSString *)tag message:(NSString *)message level:(MSDKLogLevel)level;
@end

这样调用方只依赖协议,底层用 CocoaLumberjack 还是自研库,都不影响上游。而且 @optional 方法给了实现方灵活度——你可以只实现核心方法,高级功能按需提供。

关键原则:Protocol 里不要放太多方法。超过 5 个就必须方法,就应该考虑拆成更细粒度的协议,比如 MSDKLogProvider 和 MSDKLogConfigurable。

协议继承也很常见。我会把一些基础功能抽成 MSDKBaseModule 协议,业务模块再继承它:

  • MSDKBaseModule — 声明 start、stop、status 等生命周期方法。
  • MSDKNetworkModule <MSDKBaseModule> — 加上网络请求相关方法。
  • MSDKAuthModule <MSDKBaseModule> — 加上登录/登出方法。

你想想看,这样做带来的好处:业务层只需要知道 id<MSDKAuthModule> 就能调用登录,根本不关心背后是 OAuth 还是短信验证。单元测试也方便——直接用 mock 对象实现协议就行了。

3.3 模块拆分 — 分层与解耦

SDK 做大以后,最头疼的就是“所有东西都混在一起”。我经历过一个项目,一个 .h 文件引用了十几个其他模块的头文件,改一个地方就要重新编译半分钟。后来我们基于业务领域做了拆分:

层级 含义 示例 依赖方向
Base 基础工具类和宏 NSDate+MSDK, MSUtils 无依赖
Core 核心能力接口 MSDKLoggerProtocol, MSDKNetworkProtocol 仅依赖 Base
Impl 具体实现 MSDKLoggerImpl, AFNetworkingAdapter 依赖 Core + Base
Feature 业务功能模块 MSDKShareModule, MSDKPayModule 依赖 Core + Impl
Public 对外暴露的头文件和常量 MSDK.h, MSDKErrorCode.h 依赖所有底层,但只导出公开接口

模块之间只通过 Protocol 通信,具体实现类被依赖注入容器管理。举个例子,分享模块需要一个网络层,它只要在初始化时传入 id<MSDKNetworkProtocol> 即可,不用管传入的是哪个网络库。

这种分层结构我用一张图总结一下:

iOS SDK 模块分层架构 Public 对外公开层 ShareModule (依赖 Core + Impl) PayModule (依赖 Core + Impl) ...Module (可选) Core 核心协议层 具体实现模块 第三方库 Adapter Base 基础层 依赖方向:上 → 下;模块间仅通过 Core 协议通信,实现可替换

模块拆分后在 Podfile 里这么组织:

# Podfile 示例
target 'MyApp' do
  pod 'MSDK/Base',           :path => './MSDK/Base'
  pod 'MSDK/Core',           :path => './MSDK/Core'
  pod 'MSDK/Impl/Logger',    :path => './MSDK/Impl/Logger'
  pod 'MSDK/Impl/Network',   :path => './MSDK/Impl/Network'
  pod 'MSDK/Feature/Share',  :path => './MSDK/Feature/Share'
  pod 'MSDK/Feature/Pay',    :path => './MSDK/Feature/Pay'
end

调用方可以按需引入,不需要的模块根本不会编译进来。我之前维护一个旧 SDK,整个工程就一个 .a,集成后发现包体积多了 3MB,去掉几个不用的功能后体积减了 40% 以上。所以模块拆分带来的不仅是代码清晰,还有实实在在的包体积收益。

3.4 三个设计要点串起来

Category、Protocol 和模块拆分不是孤立的。实际使用中它们经常配合:

  1. Category 扩展 + Protocol 判断:用 Category 给系统类添加方法,方法内部通过 conformsToProtocol: 判断是否支持某些接口。
  2. Protocol 作为模块间桥梁:每个 Feature 模块只暴露一个符合 MSDKFeatureProtocol 的类,其他模块通过协议获取能力。
  3. Base 层提供 Category 集:公共的字符串处理、日期格式化等放在 Base 层,方便所有模块复用。
我的建议:新项目一开始就按这种结构拆分,哪怕只有两三个功能。等后面功能多了,你只会庆幸当初做得对。

嗯,到这里 iOS 端 SDK 架构设计的核心套路就讲完了。你仔细回想一下,是不是发现 Category 的“扩展”特性、Protocol 的“接口抽象”和模块拆分的“分层隔离”正好互补?下次设计 SDK 的时候试试把这三个工具一起用上,应该会少踩不少坑。