Hilt 工作原理 | MAD Skills

Android開發者發表於2021-10-22

本文是 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 中通常需要您手動編寫。

為了生成這些元件,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 文章,敬請關注後續更新。

歡迎您 點選這裡 向我們提交反饋,或分享您喜歡的內容、發現的問題。您的反饋對我們非常重要,感謝您的支援!

相關文章