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,放在单例里反而麻烦。

Hilt 组件生命周期层次结构 Application SingletonComponent ActivityRetainedComponent ActivityComponent FragmentComponent ServiceComponent 各组件生命周期绑定关系 ● SingletonComponent:进程级别,唯一且常驻。 ● ActivityRetainedComponent:跨配置变更保留,Activity finish 才销毁。 ● ActivityComponent & FragmentComponent:随 Activity/Fragment 创建销毁。 ● ServiceComponent:随 Service 生命周期创建销毁。

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 持久,大概率不会出大问题。

一句话口诀:重依赖往上移,轻依赖往下放。初始化慢的尽量远离短命组件。