本文是 MAD Skills 系列中无关 Hilt 的第三篇文章。咱们将深入探讨 Hilt 的工作原理。如果您需理解本系列前两篇文章,请查阅:
- Hilt 介绍
- Hilt 测试最佳实际
如果您更喜爱通过视频理解此内容,请点击 此处 查看。
所涉主题
- 多种 Hilt 注解协同工作并生成代码的形式。
- 当 Hilt 配合 Gradle 应用,Hilt Gradle 插件如何在幕后工作以改善整体体验。
多种 Hilt 注解协同工作并生成代码的形式
Hilt 应用注解处理器生成代码。对注解的解决产生在编译器将源文件转换为 Java 字节码期间。顾名思义,注解处理器作用于源文件中的注解。注解处理器通常会查看注解,并依据注解类型来执行不同的工作,例如代码查看或生成新文件。
在 Hilt 中,三个最重要的注解就是: @AndroidEntryPoint、@InstallIn 以及 @HiltAndroidApp。
@AndroidEntryPoint
AndroidEntryPoint 在您的 Android 类中启用字段注入,例如 Activity、Fragment、View 以及 Service。
如下示例所示,通过向 PlayActivity
增加 AndroidEntryPoint
注解,即可轻松将 MusicPlayer 注入到咱们的 Activity 中。
@AndroidEntryPoint
class PlayActivity : AppCompatActivity() {
@Inject lateinit var player: MusicPlayer
// ...
}
如果您应用 Gradle,您可能相熟上文所述的简化语法。但这并不是实在的语法,而是 Hilt Gradle 插件为您提供的语法糖。接下来咱们将探讨更多对于 Gradle 插件的内容,在此之前,咱们先来看看这个例子在没有语法糖的状况下应该是什么样子的。
@AndroidEntryPoint(AppCompatActivity::class)
class PlayActivity : Hilt_PlayActivity() {
@Inject lateinit var player: MusicPlayer
// ...
}
当初,咱们看到原始基类 AppCompatActivity 是 AndroidEntryPoint
注解的实在入参。PlayActivity
实际上继承了生成的类 Hilt_PlayActivity
,该类由 Hilt 注解处理器生成,并蕴含所有执行注入操作须要的逻辑。针对上述内容生成的基类,其代码简化示例如下:
@Generated("dagger.hilt.AndroidEntryPointProcessor")
class Hilt_PlayActivity : AppCompatActivity {override fun onCreate() {inject()
super.onCreate()}
private fun inject() {EntryPoints.get(this, PlayActivity_Injector::class).inject(this as PlayActivity);
}
}
在示例中,生成的类继承自 AppCompatActivity
。然而,通常状况下生成的类会继承传入 AndroidEntryPoint
注解的类。这使得注入操作能够在任何您须要的基类中执行。
生成类的次要目标是解决注入操作。为了防止字段在注入之前被意外拜访,有必要尽可能早地执行注入操作。因而,对于 Activity,注入操作在 onCreate
中被执行。
在 inject 办法中,咱们首先须要一个注入器的实例——PlayActivity_Injector
。在 Hilt 中,对于 Activity,注入器是一个入口点,咱们能够应用 EntryPoints
工具类取得一个注入器的实例。
您可能想到了,PlayActivity_Injector
也是由 Hilt 注解处理器生成的。格局如下:
@Generated("dagger.hilt.AndroidEntryPointProcessor")
@EntryPoint
@InstallIn(ActivityComponent::class)
interface PlayActivity_Injector {fun inject(activity: PlayActivity)
}
生成的注入器是一个被装载到 ActivityComponent
的 Hilt 入口点。它仅蕴含一个让咱们注入 PlayActivity
实例的办法。如果您曾在 Android 利用中应用过 Dagger (不通过 Hilt),您可能会相熟这些间接在组件上编写的注入办法。
@InstallIn
InstallIn 用于表明模块或者入口点应该被装载到哪个组件中。在如下示例中,咱们将 MusicDataBaseModule
装载到 SingletonComponent 中:
@Module
@InstallIn(SingletonComponent::class)
object MusicDatabaseModule {// ...}
通过 InstallIn
,利用中任何传递依赖项内都能够提供模块和入口点。然而,局部状况下咱们须要收集所有由 InstallIn
注解提供的内容以获取每个组件的残缺模块和入口点。
Hilt 在特定的包下生成了元数据注解,以便更轻松地收集和发现这些由 InstallIn
注解所提供的内容。生成的注解格局如下:
package hilt_metadata
@Generated("dagger.hilt.InstallInProcessor")
@Metadata(my.database.MusicDatabaseModule::class)
class MusicDatabaseModule_Metadata {}
通过将元数据放进特定的包下,Hilt 注解处理器能够轻松地在您利用中所有的传递依赖项中找到生成的元数据。至此,咱们能够应用元数据注解中所蕴含的信息来找到由 InstallIn
注解所提供内容的本身援用。在本示例中指的是 MusicDatabaseModule
。
HiltAndroidApp
最初,HiltAndroidApp 注解能够让您的 Android Application 类启用注入。此处,您能够将其视为与 AndroidEntryPoint
注解完全相同。第一步,开发者仅需在 Application 类上增加 @HiltAndroidApp
注解。
@HiltAndroidApp
class MusicApp : Application {@Inject lateinit var store: MusicStore}
然而,HiltAndroidApp 还有另外一个重要的作用——生成 Dagger 组件。
当 Hilt 注解处理器遇到 @HiltAndroidApp
注解时,会在包装类中生成一些列组件,该包装类与 Application 类同名,前缀为 HiltComponents_
。如果您之前应用过 Dagger,这些组件就是增加了 @Component
和 @Subcomponent
注解的类,而在 Dagger 中通常须要您手动编写。
为了生成这些组件,Hilt 在上述元数据包中查找所有被增加 @InstallIn
注解的类。增加了 @InstallIn
注解的模块被搁置在相应组件申明的模块列表中。增加了 @InstallIn
注解的入口点被搁置在申明相应组件的父类型的地位。
从这里开始,Dagger 处理器接管并依据 @Component
和 @Subcomponent
注解生成组件的具体实现。如果您曾应用过 Dagger (不通过 Hilt),那么大概率您曾经间接解决了这些类。然而,Hilt 对开发者暗藏了这种简单操作。
这是一篇对于 Hilt 的文章,咱们就不具体介绍 Dagger 生成的代码了。如果您有趣味,详情请查阅:
- Ron Shapiro 和 David Baker 的 演讲。
- Dagger codegen 101 的 备忘单。
Hilt Gradle 插件
当初您曾经理解了 Hilt 中代码生成的工作原理,接下来让咱们看看 Hilt Gradle 插件。Hilt Gradle 插件执行很多有用的工作,包含字节码改写和类门路聚合。
字节码改写
顾名思义,字节码改写就是改写字节码的过程。与注解解决只能生成新代码不同,字节码改写能够批改现有代码。如果审慎应用,这将是十分弱小的性能。
为了阐明咱们为何在 Hilt 中应用字节码改写,让咱们回到 @AndroidEntryPoint
。
@AndroidEntryPoint(AppCompatActivity::class)
class PlayActivity : Hilt_PlayActivity {override fun onCreate(…) {val welcome = findViewById(R.id.welcome)
}
}
尽管继承 Hilt_PlayActivity
基类在实践中无效,但它可能会导致 IDE 报错。因为生成的类在您胜利编译代码后才存在,因而您常常会在 IDE 中看到红色波浪线。此外,您将无奈享有诸如办法重载这种主动补全的能力,并且您将无法访问基类中的办法。
失去这些性能不仅会升高您的编码速度,而且这些红色波浪线也会极大水平地扩散您的注意力。
Hilt Android 插件通过在您的类上增加 AndroidEntryPoint
注解来启动字节码改写。启用 Hilt Android 插件后,您只须要在类上增加 @AndroidEntryPoint
注解,同时您能够使其继承一般的基类。
@AndroidEntryPoint
class PlayActivity : AppCompatActivity { // <-- 无需援用生成的基类
override fun onCreate(…) {val welcome = findViewById(R.id.welcome)
}
}
因为此语法无需援用生成的基类,所以不会引起 IDE 报错。在字节码改写期间,Hilt Gradle 插件会将您的基类替换为 Hilt_PlayActivity
。因为此过程间接操作字节码,对开发者是不可见的。
然而,字节码改写仍有一些毛病:
- 该插件必须批改底层字节码,而不是源代码,这容易出错。
- 因为在改写操作时字节码曾经被编译,所以问题通常呈现在运行时而不是编译时。
- 改写操作使调试变得复杂,因为当呈现问题时,源文件可能并不代表以后正在执行的字节码。
因为这些起因,Hilt 尝试尽可能减少依赖字节码改写。
类门路聚合
最初,让咱们看看 Hilt Gradle 插件的另一个有用性能: 类门路聚合。要理解什么是类门路聚合,以及为什么须要它,让咱们看另一个示例。
在本示例中 :app
依赖一个独立的 Gradle 模块 :database
,:app
和 :database
都提供了被 InstallIn
注解的模块。
如您所见,Hilt 会在特定的 hilt_metadata 包下生成元数据,在生成组件时,会用它们查找所有被增加 @InstallIn 注解的模块。
不应用类门路聚合的解决对于单层依赖关系依然能够失常工作,当初让咱们看看当增加另一个 Gradle 模块 :cache
作为 :database
的依赖项时会产生什么。
当 :cache
被编译时,尽管它会生成元数据,但在编译 :app
时该元数据无奈应用,因为它是一个传递依赖项。因而,Hilt 无奈通晓 CacheModule
,它会意外地从生成的组件中排除。
当然,您能够应用 api 而不是 implementation
申明 :cache
的依赖关系,从而在技术层面解决这个问题,但不举荐这样做。应用 api 不仅会让增量构建变得更蹩脚,还把保护工作也变成一场噩梦。
这就是 Hilt Gradle 插件发挥作用的中央。
即便应用 implementation
,Hilt Gradle 插件也能够主动从 :app
的传递依赖项中聚合所有的类。
此外,与间接应用 api
相比,Hilt Gradle 插件还具备许多长处。
首先,比照在整个利用中手动应用 api
依赖关系,类门路聚合更不容易出错并且不须要保护。您能够像平常一样简略地应用 implementation
,其余的将由 Hilt Gradle 插件解决。
其次,Hilt Gradle 插件仅在利用级别聚合类,因而与应用 api 不同,我的项目中库的编译不受影响。
最初,类门路聚合为您的依赖项提供了更好的封装,因为不可能在源文件中意外援用这些类,并且它们不会呈现在代码补全提醒中。
总结
本文咱们揭示了各种 Hilt 注解协同工作以生成代码的形式。咱们还关注了 Hilt Gradle 插件,并理解它是如何在幕后应用字节码改写和类门路聚合,让 Hilt 的应用变得更平安、更轻松。
以上是本文的全部内容,咱们行将推出更多 MAD Skills 文章,敬请关注后续更新。
欢迎您 点击这里 向咱们提交反馈,或分享您喜爱的内容、发现的问题。您的反馈对咱们十分重要,感谢您的反对!