2. Android端SDK架构设计(模块化、接口分层)
模块化这事儿,说白了就是把一个庞杂的SDK拆成几个独立的小块。我早期做SDK时,习惯把所有代码塞进一个module,结果每次修改都要全量编译,那叫一个痛苦。后来想清楚了:SDK的核心使命是“稳定对外,灵活对内”。外面要提供干净、完整的API;里面要能快速迭代、独立测试、甚至按需裁剪。
模块化不是目的,可维护性才是。你想想看,如果你的SDK只有两个Gradle module——一个api,一个impl,那算不上真正的模块化。真正的模块化要遵循“单一职责 + 接口隔离”两个原则。每个模块只做一件事,并且只通过接口与外部通信。
2.1 模块划分的实战思路
我推荐把Android SDK分成以下核心模块,每个模块编译成一个aar(或一个library,视Gradle配置而定)。
| 模块名称 | 职责 | 对外暴露 |
|---|---|---|
| sdk-core | SDK初始化、生命周期管理、事件总线、线程调度 | 只暴露初始化接口和配置类 |
| sdk-network | 网络请求、重试策略、请求拦截、缓存 | 提供网络请求接口(例如 INetworkProvider) |
| sdk-storage | 本地持久化、数据库访问、文件缓存 | 提供存储接口(例如 IStorageProvider) |
| sdk-utils | 日志、加密、权限、设备信息、线程工具 | 内部使用,不对外暴露 |
| sdk-public | 对外API声明、回调接口、数据实体 | 唯一对外发布的aar,依赖其他模块 |
这里的 sdk-public 是唯一一个让集成方直接依赖的模块。其他模块都标记为 api 或者 implementation 方式被 sdk-public 引用。这样外部只能看到public模块里的接口,内部的实现细节完全隐藏。
关键原则:谁依赖谁,要画清楚。不要让sdk-network直接依赖sdk-storage,两者应该通过sdk-core这个中介通信,或者通过接口回调解耦。
2.2 接口分层:从混乱到清晰
只有模块划分还不够,每一层之间的接口定义才是灵魂。我曾经接手过一个SDK,接口类和方法混在一起,一个类里有十几个方法,有的返回 void 有的返回 Callback,调用方根本搞不清楚该用哪一个。改造之后,我用了三层接口结构:
- 第1层:Provider接口 —— 比如
INetworkProvider、IStorageProvider、ILogProvider。这些接口定义了SDK内部各模块能提供的服务。 - 第2层:Service接口 —— 比如
IAuthService、IDataSyncService。这些是面向业务场景的粗粒度接口,组合调用Provider层的能力。 - 第3层:Public API接口 —— 也就是
sdk-public里暴露给外部调用的入口。通常只有1~3个类,例如XXXSDK.getInstance().initialize()、XXXSDK.startService()。外部根本不需要知道Provider和Service的存在。
举个实际例子:当用户调用 XXXSDK.trackEvent() 时,Public API层会调用 IAnalyticsService,IAnalyticsService 内部组合了 INetworkProvider 和 IStorageProvider。如果网络不可用,数据先写本地,等网络恢复再上传。这些细节调用方完全无感知。
我的小习惯:用接口名加前缀 I,实现类名加后缀 Impl。比如 INetworkProvider 对应 NetworkProviderImpl。这样一看就明白哪个是接口哪个是实现,不用翻代码。
2.3 依赖注入:模块间通信的最佳实践
模块之间不能直接用 new 来创建实现,否则耦合就高了。我建议在 sdk-core 里维护一个轻量级的依赖注入容器(或者直接用 Dagger/Hilt,但SDK里我更喜欢手写一个简单容器,避免引入太大的框架)。
// 简单依赖容器示例
public class SDKContainer {
private static final Map<Class<?>, Object> services = new ConcurrentHashMap<>();
public static <T> void register(Class<T> clazz, T impl) {
services.put(clazz, impl);
}
@SuppressWarnings("unchecked")
public static <T> T get(Class<T> clazz) {
return (T) services.get(clazz);
}
}
// 初始化时注册实现
SDKContainer.register(INetworkProvider.class, new NetworkProviderImpl());
SDKContainer.register(IStorageProvider.class, new StorageProviderImpl());
然后在Service层通过 SDKContainer.get(INetworkProvider.class) 获取实现。这样每个模块只依赖接口,不依赖具体实现。更换某个模块(比如把OkHttp换成Volley)只需要在初始化时改一行注册代码,其他代码完全不动。
避坑指南:我曾经一个项目里为了复用代码,把十几个接口都放在了一个大模块里,结果每次改一个接口所有模块都要重新编译。后来我把接口单独抽成了 sdk-public 模块,体积很小,改动频率又低,编译时间直接从5分钟降到了30秒。
2.4 整体架构图
下面这张SVG图展示了我推荐的Android SDK模块化与接口分层架构。你可以把它当成一个模板来用。
2.5 模块化带来的实际收益
模块化不仅让SDK变清晰,还带来几个硬收益:
- 编译加速:只有修改过的模块需要重新编译。Gradle增量编译效率高多了。我实测过,全量编译3分钟,增量编译只要20秒。
- 单元测试更简单:每个模块可以独立写UT,Mock掉Provider接口就行,不用担心依赖混乱。
- 多人协作:不同开发者负责不同模块,互不干扰。接口定义好了,每个人就能并行开发。
- 按需裁剪:某些客户不需要网络功能,你把
sdk-network去掉就行,不用改一行代码。
一个小总结:接口分层加上模块化,是让Android SDK从“能跑”进化到“好维护”的关键一步。别急着堆代码,先花半天把模块边界画清楚,后面省的时间不止半天。