SDK 生命周期:从加载到销毁的阶段
做 SDK 开发这么多年,我最深的感触是——搞懂生命周期,比背一百个 API 都有用。你想想看,一个 SDK 从被宿主 App 加载进来,到最后被卸载清理,中间经历了多少个关键节点?这些节点里,哪个最容易出幺蛾子?
说白了,生命周期就是 SDK 的“生老病死”。你只有摸清了每个阶段的特征,才能在出现重复注册、初始化顺序混乱的时候,一眼看穿问题在哪。
我个人习惯把 SDK 的生命周期分成四个大阶段:加载、初始化、运行、销毁。每个阶段都有它自己的“脾气”,咱们一个一个来拆。
一、加载阶段:SDK 刚进内存的时候
这是最容易被忽略的阶段。很多工程师觉得,SDK 只要编译进了 APK,就算“加载”了。其实不是的。真正的加载,是从 ClassLoader 把 SDK 的类首次加载进 JVM 开始算起。
1. 静态代码块执行(static block)
2. 静态变量的初始化
我遇到过有的团队,喜欢在静态代码块里偷偷干“大事”——启动线程、创建单例、甚至调用网络请求。结果呢?App 一启动就死锁了。为什么会这样?因为静态代码块的执行时机完全不受你控制,它由 ClassLoader 决定。你想想看,宿主 App 主线程还没准备好,你的 SDK 就抢着干活,不出问题才怪。
二、初始化阶段:SDK 的“开张”
初始化阶段,才是 SDK 真正开始干活的起点。这里的水非常深。我见过的重复注册问题,十有八九都出在这个阶段。
为什么?因为初始化通常要完成:
- 读取配置(本地缓存 or 远程下发)
- 注册广播/回调/监听器
- 创建线程池或连接池
- 预加载资源或数据
嗯,这里要注意。如果初始化被调了两次,或者回调的顺序搞反了,你就等着崩溃吧。我个人习惯在入口方法里加一个 幂等保护:
public class SdkCore {
private static AtomicBoolean isInit = new AtomicBoolean(false);
public static synchronized void init(Context context, SdkConfig config) {
if (isInit.getAndSet(true)) {
// 防止重复注册
Log.w("SdkCore", "已经初始化过了,忽略本次调用");
return;
}
// 真正的初始化逻辑...
}
}
getAndSet(true) 是线程安全的,比用 if + 赋值更可靠。我曾经在一个项目里用普通布尔变量判断,结果多线程环境下被坑惨了——两个线程同时过了 if 判断,导致注册了两次,回调执行了两遍,资源泄漏了一堆。
三、运行阶段:平稳还是暴雷?
初始化成功之后,SDK 就进入了“稳定运行期”。但稳定的假象背后,隐藏着很多雷。比如:
- 宿主 App 的 Activity 生命周期变化,SDK 是否跟着变?
- 多个 SDK 之间,有没有共享全局变量?
- 重复注册是否发生在运行时?
我印象很深的一个案例:某次线上反馈,App 偶尔闪退。查了两天,最后发现是 SDK 内部的一个 Handler 在 Activity 销毁后没有移除回调,导致内存泄漏。等下一个 Activity 创建时,又注册了新的 Handler,两个线程同时操作同一个 List,直接 ConcurrentModificationException。
- 所有回调/监听器必须支持“去重注册”
- 所有全局资源必须按生命周期“成对”申请和释放
四、销毁阶段:善始善终
很多 SDK 长于开疆拓土,却短于功成身退。销毁阶段,说白了就是把初始化时占用的资源,全部干干净净地还回去。
如果销毁不彻底,后果很严重:
| 资源类型 | 未释放的后果 |
|---|---|
| 线程池 | 线程泄漏,OOM |
| 文件句柄 | 文件无法删除,磁盘爆满 |
| 网络连接 | 连接池耗尽,请求超时 |
| BroadcastReceiver | 重复注册 + 内存泄漏 |
销毁接口必须设计成“幂等”的,调用一次和调用十次效果相同。而且最好提供一个 强制重置 的选项,防止宿主 App 在某些极端情况下需要完全重启 SDK。
核心逻辑总结
为了让你看得更清楚,我把这四个阶段以及它们之间的关键检查点,画成了一张图:
总结一下
SDK 的生命周期,不是你写完了就完事的。它是一个闭环,从加载到销毁,每一步都需要你精心设计。什么时候该做什么事、做了之后怎么还原、重复调用怎么防护——这三条想清楚,你的 SDK 至少能避免 80% 的重复注册和初始化顺序问题。
嗯,下一阶段我们就开始逐个攻破这些具体问题了。先记住这张生命周期图,后面所有调试技巧都建立在这个框架之上。