Hilt 是 Google 官方推荐的 Android 依赖注入 (Dependency Injection, DI) 框架,它基于 Dagger(另一个 DI 框架),但专门为 Android 优化,简化了配置和使用。

对于熟悉 Spring DI 的开发者来说,理解 Hilt 最大的不同在于:Spring 框架在启动时会扫描和管理整个应用中所有需要注入的 Bean,而在 Android 中,DI 框架(如 Hilt)需要与 Android 组件(如Activity, Fragment, Service)的生命周期深度集成。

与 SpingDI核心思想对比

  • Spring DI: 你通常在类上使用@Component``@Service``@Repository等注解来声明 Bean,然后通过@Autowired或构造函数注入。Spring 容器会处理整个应用级别的 Bean 依赖关系。
  • Hilt: 你需要先声明一个 Application 级别的组件容器,然后指定在 Android 组件(Activity, Fragment 等)中进行注入。Hilt 使用注解来告诉它如何创建和提供依赖,以及在哪里进行注入。

模拟一个简单场景:一个餐厅点餐应用,需要一个Cashier(收银员)来处理付款,Cashier依赖于一个PaymentService(支付服务)。

Info

对于 Module 的疑惑解答:

Spring 的「Module」就是整个 IoC 容器本身;容器启动时会把所有@Component**@Configuration**@Service 一股脑儿扫描进来,自动帮你建 Bean。\ Hilt 的「Module」则是一个 显式声明 的“配方”类:只有写在@Module + @InstallIn里的东西才会被编译期收集到对应作用域的容器里,否则 Hilt 不会自动扫描,即必须显式声明。

Spring:启动时会扫描 classpath,把标注了@Component``@Service``@Mapper 等的类,动态注册到 Bean 容器。

Hilt:编译期用注解处理器 (APT, KAPT) 生成依赖图,只有显式声明过的类才会放进容器,不会做全盘扫描

为什么不和 Spring 一样放在启动时扫描,而必须显式声明?

  1. 性能差异
    • Spring:服务端 JVM 环境,启动时 classpath 扫描成本可接受
    • Android:App 冷启动非常敏感(100~200ms 以内),不可能像 Spring 那样全量扫描。所以 Hilt 用 APT 在编译期生成容器代码,避免运行时扫描
  2. 限制更严格
    • Spring 容器是单一的 ApplicationContext
    • Hilt 容器是分层级的 Component Tree(Application → Activity → Fragment → View → …),每个作用域需要精确控制,自动扫描容易引入不可控的依赖关系

Hilt

@Module

@Module用于标记一个类为”依赖提供模块”,该模块负责提供依赖项的创建逻辑。

@Provides

  • 用途:用于为不能通过构造函数注入的类型(如第三方库类)添加绑定
  • 方法类型:必须是具体实现的方法(非抽象)
  • 参数:可以有参数,用于提供依赖项
  • 返回类型:要提供实例的类型
  • 执行方式:每次需要该实例时都会执行方法体
@Provides
@Singleton
fun providePopupFactory(): PopupFactory {
    return PopupFactory()
}

@Binds

  • 用途:用于绑定接口和其实现(特别是当接口没有构造函数时)
  • 方法类型:必须是抽象方法
  • 参数:只能是实现类的参数
  • 返回类型:接口类型
  • 执行方式:Hilt会直接使用实现类,不会执行方法体
@Module
@InstallIn(SingletonComponent::class)
abstract class AnalyticsModule {
    @Binds
    abstract fun bindsAnalyticsService(analyticsServiceImpl: AnalyticsServiceImpl): AnalyticsService
}

注入模块内的类

@HiltAndroidApp

通过@HiltAndroidApp启用 Hilt 依赖注入。

**作用:**当@HiltAndroidApp注解Application类时,Hilt 会自动生成一个基类,并接管整个应用的依赖注入。它会为整个应用组件提供一个依赖容器,这样应用中的其他组件(如 Activity、Fragment 等)就能通过 Hilt 获得它们需要的依赖项。

/**
 * Application 类
 *
 * @author zhangzhen40@baidu.com
 * @version
 * @since 2025/08/18
 */
@HiltAndroidApp
class MyApp : Application() {
 
    @Inject
    lateinit var appManager: AppManager
 
    override fun onCreate() {
        super.onCreate()
 
    }
 
    override fun onTerminate() {
        super.onTerminate()
        appManager.unregisterReceiver()
    }
}

@Singleton

@Singleton注解来自javax.inject包,是一个标准的 Java 依赖注入注解,Hilt 框架也支持它。

作用:可以确保一个类的实例在整个应用生命周期内只被创建一次。当 Hilt 看到一个类被@Singleton注解时,它会把这个类的实例放在应用的全局容器中,这样每次请求该依赖时,都会返回同一个单例实例。跟 Spring 默认单例一样。

@ApplicationContext

是一个 Hilt 特有的限定符注解(Qualifier)。

作用: 解决依赖注入中的歧义。当你的应用中可能存在不同类型的Context(例如,应用级的ApplicationContext和 Activity 级别的ActivityContext)时,Hilt 需要知道你想要注入哪一个。@ApplicationContext就是告诉 Hilt,你想要的是应用级别的上下文,它与应用的生命周期相同。

/**
 * App 监听管理类
 *
 * Hilt 里字段注入只能使用在 Hilt 管理的组件里(Activity、Fragment..这些组件由 Android 管理生命周期)
 * 普通类只能通过构造器注入的方式
 *
 * @author zhangzhen40@baidu.com
 * @version
 * @since 2025/08/20
 */
@Singleton
class AppManager @Inject constructor(
    @ApplicationContext private val context: Context,
    private val packageChangeReceiver: PackageChangeReceiver
) {
 
    /** 注册状态 */
    private var isRegistered = false
 
    /**
     * 注册应用安装/卸载广播
     */
    fun registerReceiver() {
        val intentFilter = IntentFilter().apply {
            addAction(Intent.ACTION_PACKAGE_ADDED)
            addAction(Intent.ACTION_PACKAGE_REMOVED)
            addDataScheme("package")
        }
        context.registerReceiver(packageChangeReceiver, intentFilter)
        isRegistered = !isRegistered
    }
 
    /**
     * 反注册应用安装/卸载广播
     */
    fun unregisterReceiver() {
        if (!isRegistered) {
            return
        }
        context.unregisterReceiver(packageChangeReceiver)
        isRegistered = !isRegistered
    }
 
    // TODO: 冷启嗅探
 
 
    fun addListener(listener: IAppListener) {
        packageChangeReceiver.addListener(listener)
    }
 
    fun removeListener(listener: IAppListener) {
        packageChangeReceiver.removeListener(listener)
    }
}

@Inject constructor(...)

通过@Inject constructor告诉 Hilt 如何去创建这个类的实例并注入其依赖。

作用:干两件事儿,1. 告诉 Hilt,这个类的构造函数是用来注入依赖的。当你使用 @Inject constructor 时,Hilt 会检查构造函数中的所有参数,并尝试从它的依赖容器中为这些参数提供实例。2. 告诉 Hilt 如何去创建类的实例

/**
 * 包变更广播接收器
 *
 * @author zhangzhen40@baidu.com
 * @version
 * @since 2025/08/20
 */
class PackageChangeReceiver @Inject constructor(): BroadcastReceiver() {
 
    private val listeners = mutableSetOf<IAppListener>()
 
    fun addListener(listener: IAppListener) {
        listeners.add(listener)
    }
 
    fun removeListener(listener: IAppListener) {
        listeners.remove(listener)
    }
 
    override fun onReceive(context: Context?, intent: Intent?) {
        val action = intent?.action ?: return
        val packageName = intent.data?.schemeSpecificPart ?: return
        when (action) {
            Intent.ACTION_PACKAGE_ADDED -> {
                listeners.forEach { it.onAppInstall(packageName) }
            }
 
            Intent.ACTION_PACKAGE_REMOVED -> {
                listeners.forEach { it.onAppUninstall(packageName) }
            }
        }
    }
}

@AndroidEntryPoint

作用是标记一个 Android 组件(如 Activity、Fragment、Service、Application、BroadcastReceiver)为依赖注入的入口,以便 Hilt 能够自动为其及其依赖项生成和注入所需的对象。在某个组件中使用@Inject注入依赖时,需要在该类上加上@AndroidEntryPoint

如果忘记在组件上加@AndroidEntryPoint,直接使用@Injecte注入时,会报类似如下错误:

Hilt does not know how to provide an instance of ...

注意点

在 Hilt 里,字段注入 (@Inject lateinit var …)只能出现在 Hilt 管理的组件@AndroidEntryPoint 标记的Activity``Fragment``Service``BroadcastReceiver,以及 Hilt 支持的ViewModel里。
Hilt 不会主动去 new 一个不在自身管理范围的类,所以不能随便在一个普通 Kotlin 类里写字段注入。

注入第三方 SDK

当你想注入一个第三方 SDK 中的类时,你不能直接在那个类的构造函数上添加 @Inject 注解,因为你没有权限修改 SDK 的源代码。

在这种情况下,你需要显式地告诉 Hilt 如何创建这个类的实例。通常,有两种主要方式来做到这一点:

文档

1. 使用@Provides注解的方法
这是最常见也最推荐的方式。你可以在一个 Module(模块)类中创建一个方法,并用@Provides注解来声明这个方法会“提供”一个特定类型的依赖。

步骤:

  1. 创建一个用@Module注解的 Kotlinobjectclass(声明图结构)
  2. 在其中创建一个方法,用@Provides注解标记它
  3. 该方法的返回类型就是你想提供的依赖类型(例如,第三方 SDK 中的ThirdPartyService
  4. 方法的参数就是这个依赖所需要的其他依赖项,Hilt 会自动注入这些参数
    示例:

假设你有一个名为Retrofit的第三方库,你需要一个Retrofit实例来使用它。

@Module
@InstallIn(SingletonComponent::class) // 告诉 Hilt 这个模块的作用范围
object NetworkModule {
 
    @Provides // 告诉 Hilt,这个方法提供了 Retrofit 的实例
    @Singleton // 如果你希望这个实例是单例的
    fun provideRetrofit(): Retrofit {
        return Retrofit.Builder()
            .baseUrl("https://api.example.com/")
            .build()
    }
}

现在,Hilt 知道当有其他类需要Retrofit实例时,它应该调用NetworkModule.provideRetrofit()方法来获取它。
2. 使用@Binds注解的抽象方法
如果你想注入的是一个接口,并且有一个具体的实现类,可以使用@Binds注解。

步骤:

  1. 创建一个用@Module注解的抽象类
  2. 创建一个用@Binds注解的抽象方法
  3. 方法的参数是你想要注入的实现类,返回类型是你想要注入的接口
    示例:

假设你的应用有一个AnalyticsService接口,但你使用了FirebaseAnalytics这个第三方实现。

interface AnalyticsService {
    fun trackEvent(eventName: String)
}
 
class FirebaseAnalyticsService @Inject constructor() : AnalyticsService {
    override fun trackEvent(eventName: String) {
        // 使用 Firebase SDK 追踪事件
    }
}
 
@Module
@InstallIn(SingletonComponent::class)
abstract class AnalyticsModule {
 
    @Binds // 告诉 Hilt,当你需要 AnalyticsService 接口时,请使用 FirebaseAnalyticsService
    @Singleton
    abstract fun bindAnalyticsService(impl: FirebaseAnalyticsService): AnalyticsService
}

现在,当你向任何类注入AnalyticsService接口时,Hilt 都会自动提供FirebaseAnalyticsService 的实例。

实际例子 - 多模块调用

**案例:**在hilt-bestpractices 模块引用popupwindow-bestpractices中的PopupFactory来创建 PopupWindow。

  1. 在被引用模块的build.gradle中的 application 改为 library,声明在打包时以 aar 的方式引用。并删除 applicationId
plugins {
    id("com.android.library")
    alias(libs.plugins.kotlin.android)
}
defaultConfig {
    // pplicationId "com.example.popupwindow_bestpractices"
    minSdk 24
    targetSdk 35
    versionCode 1
    versionName "1.0"
 
    testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
  1. @HiltAndroidApp启用 Hilt 依赖注入,并在AndroidManifest.xml中声明 Application 的 name
/**
 * MuApp Class
 *
 * @author zhangzhen40@baidu.com
 * @version
 * @since 2025/08/27
 */
@HiltAndroidApp
class MyApp :Application(){
}
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
 
    <application
        android:name=".MyApp"
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/Theme.MyApplication">
        <activity
            android:name=".MainActivity"
            android:exported="true">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
 
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>
 
</manifest>
  1. 声明PopupFactory.class如何创建
/**
 * 引入 PopupWindow 模块
 *
 * @author zhangzhen40@baidu.com
 * @version
 * @since 2025/08/23
 */
@Module
@InstallIn(SingletonComponent::class)
object PopupWindowModule {
 
    @Provides
    @Singleton
    fun providePopupFactory(): PopupFactory {
        return PopupFactory
    }
}
  1. 通过@AndroidEntryPoint声明@Inject注入对象实例
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
 
    @Inject
    lateinit var popupFactory: PopupFactory
 
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        enableEdgeToEdge()
        setContentView(R.layout.activity_main)
        ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main)) { v, insets ->
            val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
            v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom)
            insets
        }
        val openPopupWindowTextview = findViewById<TextView>(R.id.open_popup_window_textview)
        openPopupWindowTextview.setOnClickListener {
            val popup = popupFactory.create(PopupType.BlankPopup, this, PopupModel())
            popup.show()
        }
    }
}

也可以把 Module 模块放在某个被引用的包中,相当于对外暴露有个可以依赖注入的接口(告诉调用方怎么创建对象)。这种情况说明你有权限可以直接更改第三方的代码库,只需要在需要依赖注入的类上打上注解即可, 引用的地方直接@Inject即可,代码如下:

QA 问题:

Q:@Module是什么?为什么需要@Module

A:上面解释过 Hilt 等这类 Android 的依赖注入工具是在编译期做 DI,那么就需要一个东西来显式声明,这个东西就是@Module。相当于在 Hilt 的SingletonComponent依赖图中,显式声明一条从IPopupServicePopupServiceImpl的绑定规则,并指定其作用域为@Singleton

等价的抽象语义是:

Component: SingletonComponent
 
Node: IPopupService
Provider: PopupServiceImpl
Scope: Singleton
 
Rule:
  IPopupService  ←  PopupServiceImpl

所以编译期,Hilt 记住了这个类型映射。当依赖图中出现IPopupService节点时,就会使用PopupServiceImpl作为其 concrete provider。

为什么需要@Module

  • 将依赖项的创建逻辑集中管理,使代码更清晰、更易于维护
  • 避免在多个类中重复编写创建依赖项的代码
  • 允许将依赖项的创建与使用解耦,实现更好的可测试性
  • 可以控制依赖项的生命周期(如@Singleton)

Q:这样看一个应用写一个 Moudle 就够了?

A:不是的,一个应用通常需要多个Module,而不是只写一个。

  • 模块化:将不同功能的依赖项分组管理,使代码结构更清晰
  • 关注点分离:网络、数据库、UI组件等不同部分的依赖项应该分开管理
  • 复用性:不同模块可以被不同组件使用
  • 可维护性:当需要修改某个依赖项的提供方式时,只需修改对应的Module

Q:这个写法是固定的吗?抽象方法?然后方法参数是实现类?

A:@Binds的写法是“结构上固定、形式上可选”。

维度@Binds@Provides
是否抽象必须不需要
是否有方法体
是否允许逻辑
生成代码更少更多
推荐场景接口→实现构造 / 条件 / 工厂

Q:如果多个实现类,必须声明多个方法吗?

是的,如果有多个实现类,通常需要为每个实现类提供一个@Provides方法。但需要区分这些实现类,通常使用@Qualifier注解:

// 定义不同的Qualifier
@Retention(AnnotationRetention.BINARY)
@Qualifier
annotation class DefaultPopup
 
@Retention(AnnotationRetention.BINARY)
@Qualifier
annotation class CustomPopup
 
// 在Module中提供多个实现
@Module
@InstallIn(SingletonComponent::class)
object PopupWindowModule {
 
    @Provides
    @Singleton
    @DefaultPopup
    fun provideDefaultPopupFactory(): PopupFactory {
        return DefaultPopupFactory()
    }
 
    @Provides
    @Singleton
    @CustomPopup
    fun provideCustomPopupFactory(): PopupFactory {
        return CustomPopupFactory()
    }
}

在使用时,通过@Qualifier指定要注入的实现类:

@Inject
@DefaultPopup
lateinit var defaultPopupFactory: PopupFactory
 
@Inject
@CustomPopup
lateinit var customPopupFactory: PopupFactory

Q:****@Provides方法和@Binds什么区别?

A:第三方库(无法通过修改代码来支持依赖注入)的,需要通过@Provides为第三方类或需要复杂创建逻辑的类提供绑定

  • 如果你有接口和实现类,优先使用@Binds,因为它更简洁且不涉及实例创建逻辑
  • 如果你无法修改第三方库的类,或者需要复杂创建逻辑,使用@Provides

厂内 pyramid

厂内也有一个依赖注入框架,叫pyramid,引入代码如下

// 依赖注入
implementation "com.baidu.pyramid:pyramid-annotation:${LIB_NADCORE_PYRAMID_DI_ANNOTATION_VERSION}"
implementation "com.baidu.searchbox.pyramid:service:${LIB_NADCORE_PYRAMID_SERVICE_VERSION}"

差异点

其实使用的方法差不多,只不过大部分的时候依赖注入都是用来注入第三方依赖,或者厂内 SDK,需要显式的写出具体构造的方法,所以有了AdRuntimeHolder.class。我理解,这个类其实就等同于 Hilt 的 Module,用来显式的声明如何把其他的类加载到容器中。

@Autowired
public class AdRuntimeHolder {
 
    /**  debug标签  */
    public static final boolean DEBUG = AppConfig.isDebug();
 
    /**
     * 获取 IAdRuntime 单例对象。
     * @return IAdRuntime 单例
     */
    @Inject(force = false)
    @NonNull
    public static IAdRuntime getAdRuntime() {
        // 依赖注入框架会将 null 替换为AdRuntime的单例对象
        return IAdRuntime.EMPTY;
    }
 
    /**
     * 获取 IAdDownloader 单例对象
     * @return IAdDownloader 单例
     */
    @Inject(force = false)
    @Singleton
    @NonNull
    public static IAdDownloader getAdDownloader() {
        return IAdDownloader.EMPTY;
    }
}

如上代码

Hilt 等依赖通常通过构造函数注入或者字段注入,调用时写@Inject lateinit var xxx。但是这里的依赖通过静态方法 + 注解暴露出来,调用时就像普通工具类调用:

IAdRuntime runtime = AdRuntimeHolder.getAdRuntime();

一些猜测:

  1. @Autowired标记类
    • 表明这个类要交给 IoC 容器管理。
    • 类似于 Spring 的@Component / Dagger 的@Module
  2. @Inject(force = false)标记方法
    • 框架会扫描这些静态方法,把返回值(本来是占位的 EMPTY)替换成真正的实现类单例。
    • force = false可能表示:如果 IoC 容器里没有这个依赖,不要报错(可选依赖)。
  3. 返回的是接口类型(IAdRuntimeIAdDownloader
    • 对外只暴露接口,底层实现由框架在运行时或编译期注入。
    • 如果没有实现,就返回EMPTY(一个空实现,避免 NullPointerException)。
  4. @Singleton
    • 标识容器注入时保持单例。

“静态 Holder + EMPTY 默认实现 + IoC 框架注入” 的完整形态。

我们来逐行拆解:

/**
 * 短小融合一跳广告参数存储接口
 *
 * @return INadVideoFlowSchemeParamsProxy
 */
@Inject(force = false)          // 表示依赖注入(如果容器里没有实现,也不会报错,用默认值兜底)
@Singleton                      // 注入的对象保持单例(全局唯一)
@NonNull                        // 不允许返回 null,至少保证 EMPTY
public static INadVideoFlowSchemeParamsProxy getVideoFlowSchemeParamsProxy() {
    return INadVideoFlowSchemeParamsProxy.Companion.getEMPTY();
}

📌 运行机制(和AdRuntimeHolder一致)

  1. 定义接口
interface INadVideoFlowSchemeParamsProxy {
    fun getAdResource(nid: String): String?
    fun saveAdResource(nid: String?, adResource: String?)
    fun clearCache()
 
    companion object {
    val EMPTY = object : INadVideoFlowSchemeParamsProxy {
        override fun getAdResource(nid: String) = null
        override fun saveAdResource(nid: String?, adResource: String?) = Unit
        override fun clearCache() = Unit
    }
}
}
  1. Holder 暴露静态方法
@Autowired
public class NadVideoFlowSchemeHolder {
    @Inject(force = false)
    @Singleton
    @NonNull
    public static INadVideoFlowSchemeParamsProxy getVideoFlowSchemeParamsProxy() {
        // 默认返回 EMPTY(空实现)
        return INadVideoFlowSchemeParamsProxy.Companion.getEMPTY();
    }
}
  1. 容器启动时扫描注解
    • 框架会发现@Inject标记的静态方法。
    • 如果 IoC 容器里已经有INadVideoFlowSchemeParamsProxy的真实实现,就会替换掉 EMPTY
    • 如果没有实现(可能广告 SDK 没有接入),就保留EMPTY,调用时返回nullUnit,不会崩溃。
  2. 调用方用起来很简单
INadVideoFlowSchemeParamsProxy proxy = NadVideoFlowSchemeHolder.getVideoFlowSchemeParamsProxy();
proxy.saveAdResource("123", "{广告数据}");
String ad = proxy.getAdResource("123");
proxy.clearCache();

👉 不需要知道背后是用 SP 存储、数据库存储还是走内存缓存。

使用

整个流程分为 3个角色

① 实现类(lib_ad_runtime 仓库)

@Service
@Singleton
class NadVideoFlowSchemeParamsProxy: INadVideoFlowSchemeParamsProxy {
    ...
}
  • @Service 注解会告诉 IoC 容器:
    “当有人需要INadVideoFlowSchemeParamsProxy时,可以用这个实现类NadVideoFlowSchemeParamsProxy来提供。”
  • @Singleton说明 IoC 框架内部会保证全局只创建一个实例。

也就是说,这里定义了「提供方」

② 接口(lib_ad_runtime 仓库)

interface INadVideoFlowSchemeParamsProxy { ... }
  • 只是规范,不带任何注解。
  • 同时定义了一个EMPTY静态对象,作为 兜底空实现

③ 使用方(lib_ad 模块)

@Inject(force = false)
@Singleton
@NonNull
public static INadVideoFlowSchemeParamsProxy getVideoFlowSchemeParamsProxy() {
    return INadVideoFlowSchemeParamsProxy.Companion.getEMPTY();
}
  • @Inject注解告诉 IoC 框架:
    “这里需要一个INadVideoFlowSchemeParamsProxy实例,请注入。”
  • force = false 表示如果找不到实现类,就用方法体里的默认返回值(这里就是 EMPTY)。
  • 这就是你写的 AdRuntimeHolder,作用相当于 统一入口,其他业务模块只要调用:
AdRuntimeHolder.getVideoFlowSchemeParamsProxy()

就能拿到 IoC 框架注入的真实实现类(NadVideoFlowSchemeParamsProxy)。
如果注入失败,就退回 EMPTY

🧩 链路总结

  1. 定义接口INadVideoFlowSchemeParamsProxy
  2. 提供实现(带@Service):NadVideoFlowSchemeParamsProxy
  3. 提供入口(带@Inject):AdRuntimeHolder.getVideoFlowSchemeParamsProxy()
  4. 业务调用:直接调 AdRuntimeHolder.getVideoFlowSchemeParamsProxy().saveAdResource(...)
    所以:
  • lib_ad_runtime提供实现的仓库
  • lib_ad对外暴露 IoC 入口的仓库
  • IoC 框架在编译或运行时会扫描 @Service,把实现类注册到容器里。
  • 业务侧永远不直接 new,而是通过 AdRuntimeHolder 拿到代理实例。

👉 也就是说,AdRuntimeHolder 里的 @Inject 方法,其实就是 “注入点 + fallback 兜底”,而不是 Hilt 的 @Provides

更新: 2026-01-09 19:15:18
原文: https://www.yuque.com/dongpozhouzi-mshe3/zhm85g/ez1wh7mn15laau72


相关笔记