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 有潜规则。最典型的是方法覆盖——如果两个 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 方法给了实现方灵活度——你可以只实现核心方法,高级功能按需提供。
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> 即可,不用管传入的是哪个网络库。
这种分层结构我用一张图总结一下:
模块拆分后在 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 和模块拆分不是孤立的。实际使用中它们经常配合:
- Category 扩展 + Protocol 判断:用 Category 给系统类添加方法,方法内部通过
conformsToProtocol:判断是否支持某些接口。 - Protocol 作为模块间桥梁:每个 Feature 模块只暴露一个符合
MSDKFeatureProtocol的类,其他模块通过协议获取能力。 - Base 层提供 Category 集:公共的字符串处理、日期格式化等放在 Base 层,方便所有模块复用。
嗯,到这里 iOS 端 SDK 架构设计的核心套路就讲完了。你仔细回想一下,是不是发现 Category 的“扩展”特性、Protocol 的“接口抽象”和模块拆分的“分层隔离”正好互补?下次设计 SDK 的时候试试把这三个工具一起用上,应该会少踩不少坑。