天天看點

學習Dagger2筆記:【7】@Scope目錄目标:使用dagger注入“單例”Kotlin中的單例需求:優化代碼借助object的全局單例使用@Scope注解全局單例@Singleton與@Reusable總結

目錄

0. 前言

1. 依賴與注入

2. @Inject

3. @Module & @Provides

4. @Component

5. @Qualifier

6. Provider & Lazy

7. @Scope

8. 注入到Set和Map容器

9. Bind系列注解

10. dagger中依賴關系與繼承關系

11. dagger.android

目标:使用dagger注入“單例”

通過我們之前對

@Provides

生成的工廠類、

@Inject

注解變量或方法生成的注入器以及

@Component

生成的橋接類的分析,我們發現dagger每次在注入依賴時,其實都會通過工廠類建立一個新的執行個體,是以在上一篇文章最後,我抛出了dagger中單例的問題,本篇也是圍繞着此問題展開的

Kotlin中的單例

衆所周知,在kotlin中通過

object

可以非常簡單的建構單例,例如:

object Singleton { // object類不能有構造函數,其也不能被外部構造
    const val NAME = "Cmd"
    val AGE = 24
    fun info() = "$NAME-$AGE"
}
           

我們看下對應的java代碼長啥樣:

public final class Singleton {
   @NotNull
   public static final String NAME = "Cmd"; // 對于有const标記的變量,可見性為public,省去了get方法調用
   private static final int AGE = 24; // 沒有const标記的變量,可見性為private,會生成get方法(如果是var還會有set方法)
   public static final Singleton INSTANCE;

   public final int getAGE() {
      return AGE;
   }

   @NotNull
   public final String info() {
      return "Cmd-" + AGE;
   }

   private Singleton() { // 注意構造函數的可見性為private
   }

   static {
      Singleton var0 = new Singleton(); // 在static塊中初始化單例
      INSTANCE = var0;
      AGE = 24;
   }
}
           

代碼不難懂,唯一要注意的是kotlin的

object

是餓漢式單例,在記憶體占用、線程安全、序列化與反序列化等方面表現并不是很好,是以如果對單例有更高的要求,建議不要直接使用

object

這個文法糖。至于其他單例實作方式不是本系列重點,就不詳細說明了

需求:優化代碼

我覺得學習一個架構,就應該先想清楚這個架構能用在什麼地方,是以通常我都會編一些需求來模拟實際使用場景

回想我們現在的代碼,

Activity

不僅有邏輯控制部分(根據時間選擇

Computer

、使用

Handler

實作定時器等)還有顯示視圖部分(顯示

Computer

資訊、顯示

timestamp

資訊等),這豈不是有違單一職責原則?是以我決定将這兩部分拆分,邏輯控制部分留在

Activity

中,而顯示視圖部分則交給新的類——

Monitor

借助object的全局單例

Monitor

類僅負責顯示視圖,為了節省記憶體我們不妨借助

object

封裝成一個“工具類”:

object Monitor {
    fun show(textView: TextView, computer: Computer) { // 在textView中顯示Computer資訊
        val builder = StringBuilder()
        computer.execute(builder)
        textView.text = builder.toString()
    }

    fun startRefresh(textView: TextView, timestamp: Date) { // 在textView中添加timestamp資訊
        val builder = StringBuilder(textView.text)
        builder.append(timestamp.toString()).append("\n")
        textView.text = builder.toString()
    }
    
    fun toastInfo(context: Context) { // 通過toast顯示一些資訊
        val builder = StringBuilder()
        builder.append("Monitor: ").append(this.hashCode())
        Toast.makeText(context, builder.toString(), Toast.LENGTH_SHORT).show()
    }
}
           

當然我們可以在

Activity

中直接使用這個單例,但因為本系列主題是dagger,更應該考慮怎樣用dagger來注入這個單例

通過之前幾篇文章的分析我們應該清楚:dagger實際上最終調用到的是我們自己寫的

@Provides

注解的方法來建立新的執行個體的,是以我們建立

MonitorModule

并修改其他部分:

@Module
class MonitorModule {
    @Provides fun provideMonitor() = Monitor // @Provides每次都傳回這個單例
}

@Component(modules = [ComputerModule::class, TimestampModule::class, MonitorModule::class]) // @Component.modules添加MonitorModule資料倉庫
interface CaseActivityComponent {
    /* ... */
    fun getMonitor(): Monitor // 添加一個擷取Monitor執行個體的方法
}

class CaseActivity : BaseActivity() {
    /* ... */
    private lateinit var component: CaseActivityComponent // 将Component橋接類儲存下來,以便調用getMonitor擷取Monitor執行個體

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_case)

        component = DaggerCaseActivityComponent
            .builder()
            .computerModule(ComputerModule(6666, 8888))
            .build()
        component.inject(this)

        show()
        startRefresh()
        
        text_view.setOnClickListener { component.getMonitor().toastInfo(this) } // 每次點選TextView就顯示Monitor相關資訊
    }

    private fun show() {
        val computer = if (System.currentTimeMillis() % 2 == 0L) linux.get() else windows.get()
        component.getMonitor().show(text_view, computer) // 顯示部分交由Monitor處理
    }

    private fun startRefresh() {
        val handler = Handler()
        handler.postDelayed(object : Runnable {
            override fun run() {
                val date = timestamp.get()
                component.getMonitor().startRefresh(text_view, date) // 顯示部分交由Monitor處理
                if (!isDestroy) {
                    handler.postDelayed(this, 1000)
                }
            }
        }, 1000)
    }
    /* ... */
}
           

(這裡我們不是通過

@Inject

注解讓dagger自動注入,主要是方面講解單例相關内容)

運作起來,我們反複點選文本,發現每次toast内容确實都一樣,說明kotlin的

object

結合dagger确實是可以實作單例。但這樣的單例是一個全局單例,什麼意思呢,假如我們有很多個

Activity

都需要使用到

Monitor

,那麼這些

Activity

拿到的

Monitor

都是同一個執行個體,如果說我隻需要在某個

Activity

或某個範圍内實作局部單例,例如将

Monitor

改為如下寫法:

當然我們又可以改寫

@Provides

注解的方法中的邏輯(例如用

HashMap

記錄類似于

<<Context, TextView>, Monitor>

這樣的鍵值對,或者在

@Modules

注解的類中添加成員變量等),但這說明你還不知道dagger中的

@Scope

注解

使用@Scope注解

為了配合

Monitor

的改動,我們各部分也需要修改一下:

//object Monitor {
class Monitor(private val context: Context, private val textView: TextView) { // object改為class,将context和textView以成員變量形式儲存下來
    //  fun show(textView: TextView, computer: Computer) {
    fun show(computer: Computer) { /* ... */ }

    //  fun startRefresh(textView: TextView, timestamp: Date) {
    fun startRefresh(timestamp: Date) { /* ... */ }

    //  fun toastInfo(context: Context) {
    fun toastInfo() { // 這些方法入參都能用成員變量替換掉,這裡我們多toast出一些資訊
        val builder = StringBuilder()
        builder.append("Monitor: ").append(this.hashCode()).append("\n")
        builder.append("Context: ").append(context.hashCode()).append("\n")
        builder.append("TextView: ").append(textView.hashCode())
        Toast.makeText(context, builder.toString(), Toast.LENGTH_SHORT).show()
    }
}

@Module
class MonitorModule(private val context: Context, private val textView: TextView) { // @Module和@Provides也做相應修改
    @Provides fun provideMonitor() = Monitor(context, textView)
}

class CaseActivity : BaseActivity() { // Activity中修改相應方法
    /* ... */
    private lateinit var component: CaseActivityComponent

    override fun onCreate(savedInstanceState: Bundle?) {
        /* ... */
        component = DaggerCaseActivityComponent
            .builder()
            .computerModule(ComputerModule(6666, 8888))
            .monitorModule(MonitorModule(this, text_view)) // 因為@Module沒有無參構造函數了,需要在建構Component時手動傳入執行個體
            .build()
        component.inject(this)
		/* ... */
//        text_view.setOnClickListener { component.getMonitor().toastInfo(this) }
        text_view.setOnClickListener { component.getMonitor().toastInfo() }
    }

    private fun show() {
        val computer = if (System.currentTimeMillis() % 2 == 0L) linux.get() else windows.get()
//        component.getMonitor().show(text_view, computer)
        component.getMonitor().show(computer)
    }

    private fun startRefresh() {
        val handler = Handler()
        handler.postDelayed(object : Runnable {
            override fun run() {
                val date = timestamp.get()
//                component.getMonitor().startRefresh(text_view, date)
                component.getMonitor().startRefresh(date)
                if (!isDestroy) {
                    handler.postDelayed(this, 1000)
                }
            }
        }, 1000)
    }
    /* ... */
}
           

改完了就運作起來吧,反複點選文本,發現雖然

context

textView

每次都是一樣的,但是

monitor

卻每次都不一樣,顯然每次調用

getMonitor()

方法都建立了一個新的

Monitor

執行個體,畢竟我們

@Provides

方法中就是去建立新的執行個體。那這樣豈不是很浪費記憶體?難道就不能實作局部單例了嗎?這裡就得

@Scope

登場了,我們先看看怎麼用:

@Scope
annotation class MonitorScope // 使用@Scope自定義一個注解,表示一個【作用範圍】

@Module
class MonitorModule(private val context: Context, private val textView: TextView) {
    @[Provides MonitorScope] fun provideMonitor() = Monitor(context, textView) // 在@Provides注解的方法上追加我們自定義的@Scope注解,表明此方法用于某個範圍内
}

@MonitorScope // 在@Component注解的接口上也追加上自定義的@Scope注解,表明此橋接類使用某個範圍
@Component(modules = [ComputerModule::class, TimestampModule::class, MonitorModule::class])
interface CaseActivityComponent { /* ... */ }
           

沒錯,隻是這兩處小修改,再次運作後還是反複點選文本,神奇的事情發生了,每次

Monitor

都是一樣的!隻有我們重新進入這個

Activity

時,才會有新的

Monitor

執行個體,我們通過

@Scope

實作了局部單例

這麼神奇的事情怎麼能不看下源碼呢?我們知道

@Provides

注解的方法實際上會生成

Factory

工廠類,但我們打開生成的

MonitorModule_ProvideMonitorFactory

工廠類發現其實作并沒有改變。于是我們的目标鎖定在

@Component

生成的橋接類上了:

public final class DaggerCaseActivityComponent implements CaseActivityComponent {
  /* ... */
  private Provider<Monitor> provideMonitorProvider;
  /* ... */
  private void initialize(
      final ComputerModule computerModuleParam,
      final TimestampModule timestampModuleParam,
      final MonitorModule monitorModuleParam) {
    /* ... */
    this.provideMonitorProvider =
        DoubleCheck.provider(MonitorModule_ProvideMonitorFactory.create(monitorModuleParam)); // 注意這一行
  }
  /* ... */
  @Override
  public Monitor getMonitor() {
    return provideMonitorProvider.get();
  }
  // 注意對比上下這兩個方法
  @Override
  public Lazy<CPU> getLazyCPU() {
    return DoubleCheck.lazy(CPU_Factory.create());
  }
  /* ... */
}
           

生成的

DaggerCaseActivityComponent

中,

Provider<Monitor>

并不是直接儲存

Monitor

的工廠類了,而是通過

DoubleCheck.provider()

又進行了一次封裝!為什麼是“又”呢,想想上一篇文章中我們介紹了

Lazy

,介紹了其在生成的

Component

中的

DoubleCheck.lazy()

封裝,還有

DoubleCheck.get()

的雙檢測實作,但回頭看下是不是找不到

DoubleCheck.provider()

這個方法?其實是因為我沒貼出來……是以這裡補上其源碼:

/* DoubleCheck的其他部分就不貼出來了,上一篇已經分析過了 */
public static <P extends Provider<T>, T> Provider<T> provider(P delegate) {
    checkNotNull(delegate);
    if (delegate instanceof DoubleCheck) {
        return delegate;
    }
    return new DoubleCheck<T>(delegate);
}
           

其實和

DoubleCheck.lazy()

差不了多少,都是

DoubleCheck

Provider

的一次封裝,記得上篇文章我們說過**

DoubleCheck.lazy()

絕對不是一個單例**,似乎打了自己的臉;但這裡對比下

getMonitor()

getLazyCPU()

兩個方法,相信你很快能明白為什麼

DoubleCheck.provider()

就是單例而

DoubleCheck.lazy()

卻不是,因為是否是單例并不是又

DoubleCheck

決定的,而是使用它們的

Component

橋接類決定的

再來看下

@Scope

的注釋(渣翻譯一下,就不貼出來了),該注解是

javax.inject

定義的,預設情況下,注入器是沒有範圍(Scope)限制的, 即每次都會建立一個執行個體并注入,之後注入器就再也不關注此執行個體了;但如果有範圍(Scope)限制,注入器就有可能需要記住這個執行個體,并用于下一次注入中;另外注入器應該考慮到線程安全問題,以保證同個注入器在多個線程中注入時不會生成新的執行個體,這也就是為什麼用

DoubleCheck

去封裝的原因

全局單例

通過上面分析我們了解到使用

@Scope

自定義注解隻能實作局部單例,那麼怎樣實作全局單例呢?比如我希望所有

Activity

中都用同一個執行個體,或者某幾個

Activity

中都用同一個執行個體,dagger又怎樣做到呢?回想一下,其實

@Scope

的局部指的是

Component

橋接類這個範圍内,是以要想共享這個局部單例,自然就要使用同一個

Component

,那麼這個

Component

自然是要放在所有

Activity

(或者所有使用者)都能通路到的地方,比如

Application

(這也是後續會介紹到的

dagger.android

所做的優化):

@Scope
annotation class ApplicationScope // 定義好範圍(Scope)注解

@Module
class ApplicationModule(private val appName: String) { // 提供應用名的資料倉庫
    @Qualifier
    annotation class ApplicationName

    @[Provides ApplicationName ApplicationScope] fun provideAppName() = appName // 使用上自定義的@Scope注解
}

@ApplicationScope // 橋接類使用自定義@Scope注解
@Component(modules = [ApplicationModule::class]) // 橋接類中使用上述資料倉庫
interface ApplicationComponent {
    @ApplicationModule.ApplicationName fun getAppName(): String // 提供擷取應用名的接口
}

class LearnDaggerApplication : Application() {
    // 下面寫法是kotlin中一種隐藏真正實作的小技巧
    private lateinit var _component: ApplicationComponent
    val component: ApplicationComponent
        get() = _component

    override fun onCreate() {
        super.onCreate()

        val name = resources.getString(R.string.app_name)
        _component = DaggerApplicationComponent // 将橋接類建構好并存放在Application中
            .builder()
            .applicationModule(ApplicationModule(name))
            .build()
    }
}

// 比如在某個Activity中需要使用到應用名
class CaseActivity : BaseActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        /* ... */
        val name = (applicationContext as LearnDaggerApplication).component.getAppName()
        Toast.makeText(this, name, Toast.LENGTH_SHORT).show()
    }
}
           

如上述代碼,其實是将

Component

放在一個生命周期很長的對象中(

Application

),以至于這個對象生命周期與應用生命周期一樣長,進而實作一種“全局單例”;而其他對象如果需要使用這個“全局單例”,就需要通過那個生命周期很長的對象去擷取到

Component

再拿到資料

總而言之,我們需要時刻記住,通過dagger架構的

@Scope

實作的單例僅僅隻是針對于某個

Component

的“局部單例”,如果真的想要全局唯一單例,我更推薦使用

object

或其他傳統寫法

@Singleton與@Reusable

@Scope

javax.inject

中有一個派生注解

@Singleton

,在dagger中也有一個派生注解

@Reusable

@Singleton

千萬要厘清這個“單例”指的是橋接類(

Component

)中的局部單例,可以把其看做是

javax.inject

中對

@Scope

的預設實作,源碼如下:

@Scope
@Documented
@Retention(RUNTIME)
public @interface Singleton {}
           

這個注解的使用與作用與我們自定義

@Scope

注解一模一樣

@Reusable

@Reusable

比較特别,其不用像

@Scope

一樣,在

@Component

注解的接口上也加上

@Reusable

的注解,源碼如下:

@Documented
@Beta
@Retention(RUNTIME)
@Scope
public @interface Reusable {}
           

注意

@Beta

注解表示其還處于實驗階段,我個人看法就是省去了我們手動将

@Scope

@Component

連接配接起來的顯示聲明,大家可以自己試試,這裡就不多說了

總結

通過

@Scope

,我們可以限制某個資料在某個

Component

中被建立的次數,進而實作“局部單例”。另外自定義的

@Scope

注解也可用于

@Inject

注解的構造函數上,大家不妨去試一下

至此我們已經将

javax.inject

中的内容全部介紹完了,下表是一個對其内容的小總結:

javax.inject

中的内容
小總結

@Inject

1. 注解在構造函數上,以生成此類的工廠類(

Factory

),此工廠類用以生産需要注入的依賴對象(注意一個類中僅能

@Inject

注解一個構造函數)

2. 注解在變量或方法上,以表明該變量或方法内的所有參數都是需要被注入的,進而生成該類的注入器(

MembersInjector

@Qualifier

&

@Named

@Qualifier

用來定義相同類型的别名,以解決注入類型相同的問題,通常此注解用在自定義注解上,

@Named

javax.inject

對其的一個樣例實作

@Scope

&

@Singleton

@Scope

注解的資料提供方法(

@Provides

@Inject

)在相同

@Scope

注解的

@Component

橋接類中是一個局部單例,僅會調用一次工廠類生成對象執行個體,

@Singleton

javax.inject

對其的一個樣例實作

Provider

用以定義“能夠提供資料的對象”的接口,其實作類很多,比如

Factory

DoubleCheck

接下來本系列會介紹dagger中另一個特性——MultiBinding,看看在dagger中怎樣把依賴注入進

map

set

容器中