目錄
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
中的内容全部介紹完了,下表是一個對其内容的小總結:
中的内容 | 小總結 |
---|---|
| 1. 注解在構造函數上,以生成此類的工廠類( ),此工廠類用以生産需要注入的依賴對象(注意一個類中僅能 注解一個構造函數) 2. 注解在變量或方法上,以表明該變量或方法内的所有參數都是需要被注入的,進而生成該類的注入器( ) |
& | 用來定義相同類型的别名,以解決注入類型相同的問題,通常此注解用在自定義注解上, 是 對其的一個樣例實作 |
& | 被 注解的資料提供方法( 或 )在相同 注解的 橋接類中是一個局部單例,僅會調用一次工廠類生成對象執行個體, 是 對其的一個樣例實作 |
| 用以定義“能夠提供資料的對象”的接口,其實作類很多,比如 、 等 |
接下來本系列會介紹dagger中另一個特性——MultiBinding,看看在dagger中怎樣把依賴注入進
map
和
set
容器中