Hilt 工作原理 | MAD Skills

image

本文是 MAD Skills 系列中有关 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

  // ...
}

现在,我们看到原始基类 AppCompatActivityAndroidEntryPoint 注解的真实入参。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 中通常需要您手动编写。

image

为了生成这些组件,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 插件的另一个有用功能: 类路径聚合。要了解什么是类路径聚合,以及为什么需要它,让我们看另一个示例。

image

在本示例中 :app 依赖一个独立的 Gradle 模块 :database:app:database 都提供了被 InstallIn 注解的模块。

如您所见,Hilt 会在特定的 hilt_metadata 包下生成元数据,在生成组件时,会用它们查找所有被添加 @InstallIn 注解的模块。

不使用类路径聚合的处理对于单层依赖关系仍然可以正常工作,现在让我们看看当添加另一个 Gradle 模块 :cache 作为 :database 的依赖项时会发生什么。

image

:cache 被编译时,虽然它会生成元数据,但在编译 :app 时该元数据无法使用,因为它是一个传递依赖项。因此,Hilt 无法知晓 CacheModule,它会意外地从生成的组件中排除。

当然,您可以使用 api 而不是 implementation 声明 :cache 的依赖关系,从而在技术层面解决这个问题,但不推荐这样做。使用 api 不仅会让增量构建变得更糟糕,还把维护工作也变成一场噩梦。

这就是 Hilt Gradle 插件发挥作用的地方。

image

即使使用 implementation,Hilt Gradle 插件也可以自动从 :app的传递依赖项中聚合所有的类。

此外,与直接使用 api 相比,Hilt Gradle 插件还具有许多优点。

首先,对比在整个应用中手动使用 api 依赖关系,类路径聚合更不容易出错并且不需要维护。您可以像往常一样简单地使用 implementation,其余的将由 Hilt Gradle 插件处理。

其次,Hilt Gradle 插件仅在应用级别聚合类,因此与使用 api 不同,项目中库的编译不受影响。

最后,类路径聚合为您的依赖项提供了更好的封装,因为不可能在源文件中意外引用这些类,并且它们不会出现在代码补全提示中。

总结

本文我们揭示了各种 Hilt 注解协同工作以生成代码的方式。 我们还关注了 Hilt Gradle 插件,并了解它是如何在幕后使用字节码改写和类路径聚合,让 Hilt 的使用变得更安全、更轻松。

以上是本文的全部内容,我们即将推出更多 MAD Skills 文章,敬请关注后续更新。

欢迎您 点击这里 向我们提交反馈,或分享您喜欢的内容、发现的问题。您的反馈对我们非常重要,感谢您的支持!

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 220,458评论 6 513
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 94,030评论 3 396
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 166,879评论 0 358
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 59,278评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 68,296评论 6 397
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 52,019评论 1 308
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,633评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,541评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 46,068评论 1 319
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,181评论 3 340
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,318评论 1 352
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,991评论 5 347
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,670评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,183评论 0 23
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,302评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,655评论 3 375
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,327评论 2 358

推荐阅读更多精彩内容