前言
之前我們已經學習了 Dagger 的基礎知識、模塊化管理,本章將是 Dagger 基礎使用的最后一章。
Scope 被誤稱 Dagger 的黑科技,但實際上它非常簡單,但錯誤理地解它的人卻前仆后繼。希望小伙伴們認真閱讀這一章,第一次學習時一定要正確理解,不然后邊再糾正會感覺世界觀都被顛覆了。
@Scope
終于來了。Scope 正如字面意思,它可以管理所創建對象的“生命周期”。Scope 的定義方式類似 Qualifier,都需要利用這個注解來定義新的注解,而不是直接使用。
重點!!!這里所謂的「生命周期」是與 Component 相關聯的。與 Activity 等任何 Android 組件沒有任何關系!
下面是典型的錯誤案例:
定義一個 @PerActivity 的 Scope,
于是認為凡是被這個 PerActivity 注解的 Provides 所創建的實例,
就會自動與當前 Activity 的生命周期同步。
上述想法非常可愛,非常天真,所以很多很多程序猿們都是可愛的 (o′?ェ?`o) 要是僅僅靠一個注解就能全自動同步生命周期,那也太智能了。
下面開始好好學習啦。先來說說正常的注入流程:目標類首先需要創建一個 Component 的實例,然后調用它定義的注入方法,傳入自身。Component 就會查找需要注入的變量,然后去 Module 中查找對應的函數,一旦找到就調用它來獲取對象并注入。
這里我們可以發現一個關鍵,也就是對象最終是 Module 里的函數提供的。這個函數當然也是我們自己編寫的,大部分情況下在這里會直接 new 一個出來。因此,如果多次注入同一類型的對象,那么這些對象將分別創建,各不相同。看下面的例子:
class MainAty : AppCompatActivity() {
@Inject
lateinit var chef1: Chef
@Inject
lateinit var chef2: Chef
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
DaggerMainComponent.builder().mainModule(MainModule()).build().inject(this)
}
執行這段代碼會進行兩次注入,最終 chef1 與 chef2 將是兩個完全不同的對象。
那如果我們想獲得一個「局部單例」呢?這時候就需要 Scope 了。首先我們要定義一個 Scope:
@Scope
@Retention(AnnotationRetention.RUNTIME)
annotation class ActivityScope
再次強調!ActivityScope 只是一個名字而已,不代表它會自動與 Activity 的生命周期綁定!
然后在 Module 對應的方法上加上 @ActivityScope:
@Module
class MainModule {
@Provides
@ActivityScope
fun provideChef() = Chef()
@Provides
@ActivityScope
fun provideStove() = Stove()
}
最后根據要求,如果 Module 有 Scope,那么對應的 Component 也必須有,所以給 Component 也加上:
@Component(modules = [MainModule::class])
@ActivityScope
interface MainComponent {
fun inject(activity: MainAty)
}
[注] Component 可以關聯多個 Scope。
此時我們再執行上述代碼,會發現 chef1 與 chef2 是同一個對象。這就實現了局部單例,也就是 Scope 的作用。神奇吧!雖然我們只簡單地 new 了一個對象,卻能實現單例。其實也不奇怪,看看源碼就可以發現,加上了 Scope 后 Dagger 內部會自動把創建的對象緩存起來。
何為局部單例
局部單例意思是,在同一個 Component 下是單例的,也呼應了前面所說 這里所謂的「生命周期」是與 Component 相關聯的
。因為我們在這個 Activity 中只創建了一個 Component 因此注入的對象是單例的。但若換一個 Activity 那么還是會生成新的對象,其本質原因是 Component 實例變了。
為什么能實現 Activity 生命周期同步
這個是真的能實現的,但和 Dagger 沒關系。一起思考下:我們在 Activity 的 onCreate()
方法中進行了注入,此時對象被創建,也就是創建周期同步了√。創建后有兩個對象會持有它的引用:① Activity ② Component(為了實現局部單例會緩存),而 Component 實際上并沒有被我們保存引用,它在注入完成后隨時會被回收掉。因此最終注入的對象只有 Activity 在引用,那自然當 Activity 被銷毀時就會被同步銷毀√。進而實現了所謂的「生命周期同步」。
結論很明顯了,Scope 不能管理生成對象的真正生命周期,只能控制對于同一個 Component 是否是局部單例的,請各位務必準確理解這一點。
@Singleton
理解了前面的 @Scope,那么這個 @Singleton 就沒有任何難度了。
上面為了實現局部單例,我們自定義了一個 Scope 名為 @ActivityScope。這很麻煩對不對?因為幾乎所有程序有會用到單例對象,為了方便,Dagger 幫我們預定義了一個 Scope ,這就是 @Singleton。
所以 @Singleton 沒有任何特殊之處(其實有一點點點點的特殊,最后講),它僅僅是為了方便而已。你可以把 @Singleton 直接替換成任何一個自定義的 Scope 代碼邏輯不會發生任何改變!
任何 Provides 都不會因為被 Scope 而自動地變成「全局單例」,@Singleton 亦然。
@Reusable
它的作用類似 Scope 但不完全相同。Scope 目的是在 Component 的生命周期內保證對象的單例,其實它緩存了生成的對象,并使用 DoubleCheck
來檢查保證單例。因此被 Scope 標注的 Provides 是綁定到 Component 的。
而 Reusable 只是為了盡可能地重用對象。它沒有進行多線程檢查,因此無法保證單例。最關鍵的是 Reusable 并不綁定 Component。因此一個被 Reusable 注解的 Provides 所提供的對象,會盡可能地在全局范圍內重用,這將擁有比 Scope 更好的性能。
因為 Reusable 不與 Component 綁定,因此需要在 Component 也標記注解,只需在 Module 標記即可。現在我們把上面的例子改成 Reusable:
@Module
class MainModule {
@Provides
@Reusable // 替換 Scope
fun provideChef() = Chef()
@Provides
@Reusable
fun provideStove() = Stove()
}
@Component(modules = [MainModule::class])
//@ActivityScope 不再需要額外的注解
interface MainComponent {
fun inject(activity: MainAty)
}
OK~ 就這么簡單。現在 Chef 已經可以全局重用了,但不保證是單例的。
全局單例
既然 Scope 只能保證局部單例,但我們如何實現全局單例呢。
我們已經知道了,局部單例是與 Component 綁定的,因此只要 Component 是全局單例的,那么它對應的 Module 下生成的所有對象都會變成全局單例,舉個例子:已知 a < b,那如何實現 a < 100?答:只需令 b = 100 即可。
那如何保證 Component 全局單例?因為 Component 是 Dagger 自動生成的,我們不可能直接把他改為傳統的單例模式,那就只能從應用生命周期入手。我們只需規定:只在 Application
類的 onCreate()
函數中實例化 Component,那個這個 Component 一定是單例的。其他地方如果需要用到,完全可以 (getApplication() as MyApp).component
這樣獲取。
下面是一個例子:
@Module
class AppModule(val context: Context) {
@Provides
@Singleton
fun provideContext() = context
}
@Component(modules = [AppModule::class])
@Singleton
interface AppComponent {
fun context(): Context
}
class MyApplication: Application {
lateinit var component: AppComponent
override fun onCreate() {
super.onCreate();
component = DaggerAppComponent.builder().appModule(AppModule(this)).build();
}
}
如此一來,我們就實現了單例的 Component,其他 Component 可以依賴這個,進而能夠在任何地方拿到 Context 來用。根據業務需要,我們可以在 AppModule 里定義更多的 Provides 來注入全局單例的對象,例如數據庫等。
@BindsInstance
BindsInstance 用于簡化編寫含參構造函數的 Module。 遇到這種情況我們應該首選 BindsInstance 方式,而不是在 Module 的構造函數中增加參數。上面的 AppModule
是一個典型的例子。下面我們將改寫它:
@Component()
@Singleton
interface AppComponent {
fun context(): Context
@Component.Builder // 自定義Builder
interface Builder {
@BindsInstance
fun context(context: Context): Builder
fun build(): AppComponent
}
}
看到沒,這下連 Module 都免了。
之前我們注入時是這樣寫的:
DaggerAppComponent.builder().appModule(AppModule(this)).build();
現在只需這樣寫:
DaggerAppComponent.builder().context(this).build();
注意: 在調用 build()
之前,必須先調用所有 BindsInstance 的函數來傳入所需參數。
Scope 的要求
多個 Scope 和多個 Component 使用時有一些要求需要遵守:
- Component 和他所依賴的 Component 不能用相同的 Scope。編譯時會報錯,因為這有可能破壞 Scope 的范圍,詳見 issues。
- @Singleton 的 Component 不能依賴其他 Component。這個好理解,畢竟 Singleton 設計及就是用來做全局的。如果有需求請自定義 Scope。(這算是 Singleton 的一點點特殊)
- 無 Scope 的 Component 不能依賴有 Scope 的 Component,這也會導致 Scope 被破壞。
- Module 以及通過構造函數注入依賴的類以及其 Component 必須有相同 Scope。
總結
寫了一晚上一夜,終于寫完就 Dagger 基礎了。下面會繼續寫 Android 方面 Dagger 的特殊功能。
回想起自己學 Dagger 的歷程,真的是非常頭疼。各種概念越看越暈。網上還有很多不負責任的教程自己都沒搞懂就開始誤導別人。希望這個系列文章能給 Dagger 的初學者帶來一點清新的感覺吧。