Hilt组件生命周期:SingletonComponent、ActivityComponent等
Hilt 里的 Component 并不是什么新概念,它就是 Dagger 里的 @Component。但 Hilt 帮你把组件的创建和销毁和 Android 的生命周期绑死了。说白了,你的依赖能活多久,取决于它被放在哪个 Component 里。
我经常被问到:“为什么我的 ViewModel 里注入的网络库,退出 Activity 半天了还在打印日志?” 答案很简单——你把它放到了 SingletonComponent 里。它根本不会随 Activity 销毁。所以,搞懂这几个组件的生命周期,是避免初始化延迟问题的基础。
1. Hilt 组件层次结构概览
Hilt 定义了一套完整的组件树。每个组件都对应一个 Android 生命周期节点。从根到叶大致是这样的:
- SingletonComponent — 绑定 Application,应用进程唯 one。
- ActivityRetainedComponent — 绑定 ViewModelStoreOwner,跨配置变更存活。
- ActivityComponent — 绑定 Activity,每次 onCreate 创建,onDestroy 销毁。
- FragmentComponent — 绑定 Fragment,每次 onAttach 创建,onDetach 销毁。
- ServiceComponent — 绑定 Service。
- ViewComponent / ViewWithFragmentComponent — 用于自定义 View。
注意,ActivityComponent 和 FragmentComponent 是短命组件,而 SingletonComponent 是长命组件。这个差异直接决定了依赖初始化的时机和开销。
核心规律:组件创建越频繁,其内部依赖的初始化就越容易引起性能问题。
下面这个表格把各组件的生命周期和典型场景列清楚了:
| 组件名称 | 绑定对象 | 创建时机 | 销毁时机 | 典型作用域 |
|---|---|---|---|---|
| SingletonComponent | Application | 进程启动 | 进程销毁 | @Singleton |
| ActivityRetainedComponent | ViewModelStoreOwner | Activity 首次创建 | Activity finish | @ActivityRetainedScoped |
| ActivityComponent | Activity | Activity.onCreate() | Activity.onDestroy() | @ActivityScoped |
| FragmentComponent | Fragment | Fragment.onAttach() | Fragment.onDetach() | @FragmentScoped |
| ServiceComponent | Service | Service.onCreate() | Service.onDestroy() | @ServiceScoped |
个人习惯:我通常在 SingletonComponent 里放配置信息、日志框架、应用级别的数据库实例。但网络库?不一定。因为用户切换账号时可能需要重置 CookieStore,放在单例里反而麻烦。
2. SingletonComponent:全局单例的基石
这是整个组件树的根,由 Hilt 自动生成,在 Application 创建时初始化,进程销毁才释放。你用 @Singleton 标记的依赖全部放在这里。
举个例子,我们要注入一个全局的 AppConfig:
@Module
@InstallIn(SingletonComponent.class)
public class AppModule {
@Provides
@Singleton
public AppConfig provideAppConfig() {
// 耗时操作,比如读取本地配置文件
return new AppConfig();
}
}
这个 AppConfig 只在应用启动时初始化一次。但注意——如果初始化很重,你的 Application.onCreate() 会被阻塞。我曾经在项目里把数据库 Migration 和 Retrofit 初始化都塞进了 SingletonComponent,结果冷启动慢了 800ms。后来我把网络层拆到了 ActivityRetainedComponent 里,才把启动时间降下来。
避坑指南:我曾经把用户登录状态也放在 SingletonComponent 里,导致切换账号后旧的 还留在内存里。Singleton 的生命周期是“进程级”,不是“用户级”。状态类数据慎用。
3. ActivityRetainedComponent:配置变更的救星
这个组件比较特殊,它和 ViewModel 类似,在 Activity 旋转或配置改变时不会重建。Hilt 把它绑定到了 ViewModelStoreOwner(通常是 Activity)上。
它的生命周期:
- 创建时机:Activity 首次 onCreate 时。
- 销毁时机:Activity 被 finish(比如用户按返回键)时。
- 注意:它和 ActivityComponent 是平级关系,不是从属关系。
为什么这个组件重要?因为很多跨配置变更的依赖(比如数据库 DAO、网络缓存)放这里正好。如果全放 SingletonComponent 太“重”,放 ActivityComponent 又会每次旋转重建,浪费性能。
我自己习惯把需要“保持连接但不全局唯一”的依赖放这里。例如 WebSocket 连接、文件下载管理器。
4. ActivityComponent 和 FragmentComponent:短命组件
这两个组件是“随用随建”的典型。每次 Activity/Fragment 重建时,Hilt 都会创建新的 Component,并把所有 @ActivityScoped / @FragmentScoped 的依赖重新初始化一遍。
这里最容易出初始化延迟问题。你想啊,如果用户频繁进入退出 Activity,或者 Fragment 被频繁切换,每次重建都要执行 Module 里的初始化逻辑,卡住 UI 线程就很正常了。
我还记得有一次,同事在 FragmentComponent 里注入了一个图片加载引擎的初始化,结果每次切换 Tab 都要等 300ms 才能看到图。后来移到 ActivityRetainedComponent 里,整个流畅度就上来了。
经验总结:凡是非 UI 相关的重型依赖,尽量不要放在 ActivityComponent 和 FragmentComponent 里。它们只适合放那些“必须短命”的资源,比如 Activity 级别的 camera 实例、Fragment 级别的列表适配器等。
5. 生命周期对初始化延迟的影响分析
我把各组件的初始化风险整理成了表格,方便你决策时对照:
| 组件 | 初始化次数 | 初始化耗时容忍度 | 典型延迟场景 |
|---|---|---|---|
| SingletonComponent | 1 次(应用启动) | 低(阻塞冷启动) | 启动白屏、ANR |
| ActivityRetainedComponent | 1 次(Activity 创建) | 中(Activity 初次打开) | 页面跳转卡顿 |
| ActivityComponent | 每次 Activity.onCreate | 高(频繁重建) | 旋转屏幕、返回重进 |
| FragmentComponent | 每次 Fragment 附加 | 很高(频繁切换) | Tab 切换、Fragment 栈操作 |
| ServiceComponent | 每次 Service 启动 | 中(后台启动) | 前台服务启动延迟 |
你看,FragmentComponent 是最危险的。如果一个 Fragment 里注入了一个需要 200ms 初始化的依赖,而用户又在快速滑动切换 Tab,那每次都要等 200ms。屏幕闪烁或白块就是这么来的。
6. 如何选择合适的组件作用域
选作用域其实是在回答一个问题:这个依赖应该活多久?
- 进程级(@Singleton):配置信息、日志初始化、崩溃收集、全局常量。注意不要放可变状态。
- Activity 级跨配置(@ActivityRetainedScoped):网络层、数据库 DAO、缓存管理器、用户 session(谨慎)。
- Activity 级短命(@ActivityScoped):Activity 内部的 UI 控制器、无障碍管理器、短暂的单例服务。
- Fragment 级(@FragmentScoped):尽量只放 Fragment 独享的轻量对象,比如 ViewModelFactory 或简单的数据源。
有个简单判断法:如果你不确定放哪里,先放到 ActivityRetainedComponent 里。它比 Singleton 轻,比 ActivityComponent 持久,大概率不会出大问题。
一句话口诀:重依赖往上移,轻依赖往下放。初始化慢的尽量远离短命组件。