Dagger2 in Android(三)Scope與生命周期

前言

之前我們已經學習了 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 的初學者帶來一點清新的感覺吧。

?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,283評論 6 530
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 97,947評論 3 413
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,094評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,485評論 1 308
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,268評論 6 405
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,817評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 42,906評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,039評論 0 285
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,551評論 1 331
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,502評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,662評論 1 366
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,188評論 5 356
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 43,907評論 3 345
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,304評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,563評論 1 281
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,255評論 3 389
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,637評論 2 370

推薦閱讀更多精彩內容