商業轉載請聯繫作者獲得授權,非商業轉載請註明出處。
目錄
3.3 案例C :@Qualifiers 和 @Name的使用
4.3.4 在application中提供唯一的baseComponent類
一 Dagger2
1.1 簡介:
1:學習Dagger2需要註解、反射相關的知識。推薦閱讀:
2:Dagger2起源於Dagger,是一款基於Java註解來實現的完全在編譯階段完成依賴注入的開源庫。主要用於模塊間解耦、提高代碼的健壯性和可維護性。
3:Dagger2在編譯階段通過apt利用Java註解自動生成Java代碼,然後結合手寫的代碼來自動幫我們完成依賴注入的工作。
4:Dagger2 需要先編譯一次代碼,不然就會報錯。編譯代碼是爲了生成中間代碼,然後在中間代碼的基礎上按照正常的流程開發。
5:Dagger2 原理利用了反射,而反射處理相對於正常開發速度很慢。Dagger2 把反射處理移動到編譯器編譯代碼時的階段,而程序運行時並不涉及到反射:它在編譯時產生一些新的代碼,然後再在程序運行過程中正常使用。所以這就是Dagger2,或者其他類似於 GreenDao、Butterknife等三方庫運用到了反射,卻仍然高效的祕訣。
6:Dagger2 是基於註解開發的,而 Dagger2 中所涉及到的註解其實是基於 javax.inject 上開發的,它出自 JSR330。JSR330 是規範,建議大家怎麼做,而 Dagger2 則實現了這個規範,對於普通開發者而言,學習 Dagger2 其實就是學習相關注解的意義與用途。
1.2 起源
起初Square公司受到Guice的啓發而開發了Dagger,但是Dagger這種半靜態半運行時的框架還是有些性能問題(雖說依賴注入是完全靜態的,但是其有向無環圖(Directed Acyclic Graph)還是基於反射來生成的,這無論在大型的服務端應用還是在Android應用上都不是最優方案)。因此Google工程師Fork了Dagger項目,對它進行了改造。於是變演變出了今天我們要討論的Dagger2,所以說Dagger2其實就是高配版的Dagger。
二 Dagger2註解初識
Dagger2是基於Java註解來實現依賴注入的,那麼在正式使用之前我們需要先了解下Dagger2中的註解。Dagger2使用過程中我們通常接觸到的註解主要包括:@Inject, @Module, @Provides, @Component, @Qulifier, @Scope, @Singleten。
2.1 @Inject:
@Inject有兩個作用:
一是用來標記需要依賴的變量,以此告訴Dagger2爲它提供依賴;
二是用來標記構造函數,Dagger2通過@Inject註解可以在需要這個類實例的時候來找到這個構造函數並把相關實例構造出來,以此來爲被@Inject標記了的變量提供依賴;
對應請看下面的 案例A 加深學習。
2.2 @Module:
@Module用於標註提供依賴的類。
你可能會有點困惑,上面不是提到用@Inject標記構造函數就可以提供依賴了麼,爲什麼還需要@Module?
比如,很多時候我們需要提供依賴的構造函數是第三方庫的,我們沒法給它加上@Inject註解;
又比如,需要被注入的依賴類提供的構造函數是帶參數的,那麼他的參數又怎麼來呢?
@Module正是幫我們解決這些問題的。
對應請看下面的 案例B 加深學習。
2.3 @Provides:
@Provides用於標註Module所標註的類中的方法,該方法在需要提供依賴時被調用,從而把預先提供好的對象當做依賴給標註了@Inject的變量賦值;
對應請看下面的 案例B 加深學習。
2.4 @Component:
@Component用於標註接口,是依賴需求方和依賴提供方之間的橋樑。被Component標註的接口在編譯時會生成該接口的實現類(如果@Component標註的接口爲CarComponent,則編譯期生成的實現類爲DaggerCarComponent),我們通過調用這個實現類的方法完成注入;
2.5 @Qulifier:
@Qulifier用於自定義註解,也就是說@Qulifier就如同Java提供的幾種基本元註解一樣用來標記註解類。我們在使用@Module來標註提供依賴的方法時,方法名我們是可以隨便定義的(雖然我們定義方法名一般以provide開頭,但這並不是強制的,只是爲了增加可讀性而已)。
那麼Dagger2怎麼知道這個方法是爲誰提供依賴呢?答案就是返回值的類型,Dagger2根據返回值的類型來決定爲哪個被@Inject標記了的變量賦值。但是問題來了,一旦有多個一樣的返回類型Dagger2就懵逼了。@Qulifier的存在正式爲了解決這個問題,我們使用@Qulifier來定義自己的註解,然後通過自定義的註解去標註提供依賴的方法和依賴需求方(也就是被@Inject標註的變量),這樣Dagger2就知道爲誰提供依賴了。詳細可看下面的案例C進行理解。
一個更爲精簡的定義:當類型不足以鑑別一個依賴的時候,我們就可以使用這個註解標示;
2.6 @Scope:
@Scope同樣用於自定義註解,我能可以通過@Scope自定義的註解來限定註解作用域,實現單例(分局部和全局);
@Scope需要Component和依賴提供者配合才能起作用,對於@Scope註解的依賴,Component會持有第一次創建的依賴,後面注入時都會複用這個依賴的實例,實質上@Scope的目的就是爲了讓生成的依賴實例的生命週期與 Component 綁定
如果Component重建了,持有的@Scope的依賴也會重建,所以爲了維護局部單例需要自己維護Component的生命週期。
2.7 @Singleton:
@Singleton其實就是一個通過@Scope定義的註解,我們一般通過它來實現全局單例。但實際上它並不能提前全局單例,是否能提供全局單例還要取決於對應的Component是否爲一個全局對象。
三 Dagger2使用
3.1 引入Dagger2
Dagger2 官網地址是 https://google.github.io/dagger/,它是應於 Java 和 Android 開發的依賴注入框架,而不單單是Android。
dependencies {
api 'com.google.dagger:dagger:2.x'
annotationProcessor 'com.google.dagger:dagger-compiler:2.x'
}
3.2 案例A:入門demo
Car類是需求依賴方,依賴了Engine類;因此我們需要在類變量Engine上添加@Inject來告訴Dagger2來爲自己提供依賴。 Engine類是依賴提供方,因此我們需要在它的構造函數上添加@Inject
public class Engine {
/**
* Dagger2通過@Inject註解可以在需要這個類實例的時候來找到這個構造函數並把相關實例構造出來,
* 以此來爲被@Inject標記了的變量提供依賴
*
*/
@Inject
public Engine() {
}
@NonNull
@Override
public String toString() {
return "Engine{}";
}
public void run() {
Log.i("tag", "引擎轉起來了~~~ ");
}
接下來我們需要創建一個用@Component標註的接口CarComponent,這個CarComponent其實就是一個注入器,這裏用來將Engine注入到Car中。
@Component
public interface CarComponent {
void inject(Car car);
}
完成這些之後我們需要Build下項目,讓Dagger2幫我們生成相關的Java類。接着我們就可以在Car的構造函數中調用Dagger2生成的DaggerCarComponent來實現注入(這其實在前面Car類的代碼中已經有了體現)
public class Car {
/**
* @Inject:@Inject有兩個作用,一是用來標記需要依賴的變量,以此告訴Dagger2爲它提供依賴
*/
@Inject
Engine engine;
public Car() {
DaggerCarComponent.builder().build().inject(this);
}
public Engine getEngine() {
return this.engine;
}
public static void main(String ... args){
Car car = new Car();
System.out.println(car.getEngine());
}
}
3.3 案例B
3.3.1 @Module、@Provide 的使用
如果創建Engine的構造函數是帶參數的呢?或者Eggine類是我們無法修改的呢?這時候就需要@Module和@Provide上場了。
可以看到下面Engine類的代碼和上面的入門demo中的Engine代碼幾乎一樣,只是多了個帶參數的構造方法。
public class Engine {
private String name;
@Inject
Engine(){}
Engine(String name) {
this.name = name;
}
@Override
public String toString() {
return "Engine{" +
"name='" + name + '\'' +
'}';
}
public void run() {
System.out.println("引擎轉起來了~~~");
}
}
接着我們需要一個Module類來生成依賴對象。前面介紹的@Module就是用來標準這個類的,而@Provide則是用來標註具體提供依賴對象的方法(這裏有個不成文的規定,被@Provide標註的方法命名我們一般以provide開頭,這並不是強制的但有益於提升代碼的可讀性)。
@Module
public class MarkCarModule {
String engineType;
public MarkCarModule(String engineType) {
this.engineType = engineType;
}
@Provides
Engine provideEngine() {
return new Engine(engineType);
}
}
接下來我們還需要對CarComponent進行一點點修改,之前的@Component註解是不帶參數的,現在我們需要加上modules = {MarkCarModule.class},用來告訴Dagger2提供依賴的是MarkCarModule這個類。
@Component(modules = MarkCarModule.class)
public interface CarComponent {
void inject(Car car);
}
Car類的構造函數我們也需要修改,相比之前多了個markCarModule(new MarkCarModule())方法,這就相當於告訴了注入器DaggerCarComponent把MarkCarModule提供的依賴注入到了Car類中。
public class Car {
@Inject
Engine engine;
public Car() {
DaggerCarComponent.builder().markCarModule(new MarkCarModule("國產發動機"))
.build().inject(this);
}
public Engine getEngine() {
return this.engine;
}
public static void main(String ... args){
Car car = new Car();
System.out.println(car.getEngine());
}
}
這樣一個最最基本的依賴注入就完成了,接下來我們測試下我們的代碼。 輸出:
Engine{name='國產發動機'}
3.3.2 Dagger2 的依賴查找順序說明
我們提到@Inject和@Module都可以提供依賴,那如果我們既在構造函數上通過標記@Inject提供依賴,又通過@Module提供依賴Dagger2會如何選擇呢?具體規則如下:
-
步驟1:首先查找@Module標註的類中是否存在提供依賴的方法。
-
步驟2:若存在提供依賴的方法,查看該方法是否存在參數。
-
-
a:若存在參數,則按從步驟1開始依次初始化每個參數;
-
b:若不存在,則直接初始化該類實例,完成一次依賴注入。
-
-
步驟3:若不存在提供依賴的方法,則查找@Inject標註的構造函數,看構造函數是否存在參數。
-
-
a:若存在參數,則從步驟1開始依次初始化每一個參數
-
b:若不存在,則直接初始化該類實例,完成一次依賴注入。
-
總結:Dagger2 依賴查找的順序是先查找 Module 內所有的 @Provides 提供的依賴,如果查找不到再去查找 @Inject 提供的依賴。
3.3 案例C :@Qualifiers 和 @Name的使用
在同一個 Module 中 ,@Provides 提供的依賴是由返回值決定的。這樣就會出現問題,同一種類型不同實例,怎麼去區別?比如如果一臺汽車有兩個引擎(也就是說Car類中有兩個Engine變量),3.3.1案例B中的MarkCarModule你可能就會這麼寫:
@Module
public class MarkCarModule {
public MarkCarModule(){ }
@Provides
Engine provideEngineA(){
return new Engine("國產發動機");
}
@Provides
Engine provideEngineB(){
return new Engine("德國發動機");
}
}
大家應該會這樣編碼,但是根本編譯不過。因爲 Dagger2 是根據返回的類型來進行依賴關係確定的。如果存在兩個方法返回一樣的類型,那麼正常情況下 Dagger2 沒法知道選哪個(比如上述代碼中,不知道選provideEngineA()方法還是provideEngineB()方法)。
Dagger2 給出瞭解決方案: @Name 註解 或者 @Qulifier註解:
3.3.1 @Qulifier註解
我們通過@Qulifier註解來自定義兩個註解,用於區分返回類型相同但又是不同對象的兩個具體類,在例子裏就是國產發動機和德國發動機。
public class Engine {
/**
* 使用@Qulifier定義兩個註解
*/
@Qualifier
@Retention(RetentionPolicy.RUNTIME)
public @interface QualifierA { }
@Qualifier
@Retention(RetentionPolicy.RUNTIME)
public @interface QualifierB { }
private String name;
Engine(String name) {
this.name = name;
}
@Override
public String toString() {
return "Engine{" +
"name='" + name + '\'' +
'}';
}
public void run() {
System.out.println("引擎轉起來了~~~");
}
}
同時我們需要對依賴提供方做出修改,用定義的不同的註解來給不同的provideEngine方法貼上標籤,這樣Dagger2在尋找同類型對象時,就知道到底應該找哪一個了。
@Module
public class MarkCarModule {
public MarkCarModule(){ }
@Engine.QualifierA
@Provides
Engine provideEngineA(){
return new Engine("國產發動機");
}
@Engine.QualifierB
@Provides
Engine provideEngineB(){
return new Engine("德國發動機");
}
}
接下來依賴需求方Car類同樣需要修改,需要在注入對象前加上自定義的註解便籤,以便Dagger2知道選擇哪個provide方法:
public class Car {
@Engine.QualifierA
@Inject
Engine engineA;
@Engine.QualifierB
@Inject
Engine engineB;
public Car() {
DaggerCarComponent.builder().markCarModule(new MarkCarModule())
.build().inject(this);
}
public Engine getEngineA() {
return this.engineA;
}
public Engine getEngineB() {
return this.engineB;
}
public static void main(String... args) {
Car car = new Car();
System.out.println(car.getEngineA());
System.out.println(car.getEngineB());
}
}
3.3.1 @Name註解
看下Named標籤的源碼:
@Qualifier
@Documented
@Retention(RUNTIME)
public @interface Named {
/** The name. */
String value() default "";
}
可以看到:@Name 實際上也是 @Qualifier 註解的一個註解。原理還是 @Qualifier,只不過往上封裝了一層使用起來可能更加方便。
@Module
public class MarkCarModule {
public MarkCarModule(){ }
@Provides
@Named("QualifierA")
Engine provideEngineA(){
return new Engine("國產發動機");
}
@Provides
@Named("QualifierB")
Engine provideEngineB(){
return new Engine("德國發動機");
}
}
public class Car {
@Named("QualifierA")
@Inject
Engine engineA;
@Named("QualifierB")
@Inject
Engine engineB;
public Car() {
DaggerCarComponent.builder().markCarModule(new MarkCarModule())
.build().inject(this);
}
public Engine getEngineA() {
return this.engineA;
}
public Engine getEngineB() {
return this.engineB;
}
public static void main(String... args) {
Car car = new Car();
System.out.println(car.getEngineA());
System.out.println(car.getEngineB());
}
}
四 進階使用之單例
4.1 @Singleton實現的局部單例
利用Dagger2我們可以捨棄以往雙重檢查鎖等常見的繁瑣的單例創建方式。有關單例的學習可以參考這邊文章:單例模式學習與進階。
Dagger2中提供了@Singleton註解來實現單例,實現單例需要兩步 :
1 : 在Module對應的Provides方法標明@Singleton,表明提供的是一個單例對象。
2 : 同時在Component類標明@Singleton,表明該Component中有Module使用了@Singleton。
下面我們來寫一個簡單的局部單例的例子:
4.1.1 創建UserBean對象
public class UserBean {
private String name;
public UserBean(String name) {
this.name = name;
}
}
4.1.2 創建Module
利用@Singleton來提供一個單例對象:
@Module
public class UserModule {
@Singleton
@Provides
UserBean providesUserA() {
return new UserBean("Lucas");
}
}
4.1.3 創建Component
注意:如果 moudule所依賴的Comonent 中有被@Singleton單例的對象,那麼Conponnent也必須是單例的@Singleton,表明該Component中有Module使用了@Singleton
@Singleton
@Component(modules = UserModule.class)
public interface UserComponent {
void join(UserActivity userActivity);
}
這裏大家可能會有疑問,爲什麼要用 @Singleton 同時標註 @Provides 和 @Component ?
Component 是聯繫需求與依賴的紐帶,對於@Scope(/@Singleton)註解的依賴,Component會持有第一次創建的依賴,後面注入時都會複用這個依賴的實例,如果Component重建了,持有的@Scope(/@Singleton)的依賴也會重建,所以需要@Scope(/@Singleton)來標註Component。標註@Provides是爲了提供唯一的userBean對象。 你甚至可以理解成單例的單例,這麼說不嚴謹,但是有助於理解:我要單例的userBean,所以Component得是個單例,同activity拿到的UserComponent得是同一個具體實現類。同樣對provide方法而言,userModule也是UserComponent中的依賴,同一個UserComponent中的userModule也必須是單例。
4.1.3 UserActivity中進行測試
public class UserActivity extends AppCompatActivity {
private TextView textView;
@Inject
UserBean userBeanA;
@Inject
UserBean userBeanB;
@Inject
UserBean userBeanC;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_monkey);
textView = findViewById(R.id.textView);
DaggerUserComponent.create().join(this);
}
public void checkSingleton(View view) {
textView.setText("userBeanA:" + userBeanA.hashCode() + "\n"
+ "userBeanB:" + userBeanB.hashCode() + "\n"
+ "userBeanC:" + userBeanC.hashCode() + "\n");
}
}
運行後可以看到,userBeanA、userBeanB、userBeanC三者的hashCode值都是一樣的,證明了在此activity中,我們剛剛所寫的單例,成功!
值得注意的是:這裏的單利對象只能在同一個Activity中有效。
解釋一下:比如上面的這個例子,關鍵在於UserActivity中的 DaggerUserComponent.create().join(this) 這一行,它創建了橋樑UserComponent,所以在這個activity中,所有的userBean對象都是通過這個UserComponent去注入的,而UserComponent已經知道了UserModule是個被@Singeton註解修飾的單例。如果再新建另外一個UserActivity2,並在這個新建的activity2中再DaggerUserComponent.create().join(this) 一下,那麼就會產生新的橋樑:UserComponent2(UserComponent是個接口,在不同的activity中傳入不同的activity實例,由此也就產生了不同的UserComponent具體實現),這時的userBean對象就不是剛剛那個UserActivity中的userBean了!
不同的Activity 持有的對象不同,使用@Singeton註解或者定自定義的@Scope的, 只能在同一個activity(或者fragment)的一個生命週期中保持單例。
4.2 自定義@Scope註解實現的局部單例
我們看下@Singeton註解的具體實現:
@Scope
@Documented
@Retention(RUNTIME)
public @interface Singleton {}
可以看到, @Singleton 只是 @Scope 一個默認的實現而已,但是因爲它更具可讀性,能夠讓開發者一眼就明白它的作用是爲了單例。那麼我們現在用@Scope來實現一個單例,首先我們需要通過@Scope定義一個CarScope註解:
public class Engine {
/**
* 1. @Scope定義一個CarScope註解
*/
@Scope
@Retention(RetentionPolicy.RUNTIME)
public @interface CarScope {
}
private String name;
Engine(String name) {
this.name = name;
System.out.println("Engine create: " + name);
}
@Override
public String toString() {
return "Engine{" +
"name='" + name + '\'' +
'}';
}
public void run() {
System.out.println("引擎轉起來了~~~");
}
}
接着我們需要用這個@CarScope去標記依賴提供方MarkCarModule,表明這個provide方法提供的對象是單例。
@Module
public class MarkCarModule {
public MarkCarModule(){ }
/**
* 2. @CarScope去標記依賴提供方MarkCarModule
* @return
*/
@Engine.CarScope
@Provides
Engine provideEngine(){
return new Engine("國產發動機");
}
}
同時還需要使用@Scope去標註注入器Compoent,表明該Component中有Module使用了這個自定義的@Scope:
/**
* 3. 同時還需要使用@Scope去標註注入器Compoent
*/
@Engine.CarScope
@Component(modules = MarkCarModule.class)
public interface CarComponent {
void inject(Car car);
}
public class Car {
@Inject
Engine engineA;
@Inject
Engine engineB;
public Car() {
DaggerCarComponent.builder().markCarModule(new MarkCarModule())
.build().inject(this);
}
public Engine getEngineA() {
return this.engineA;
}
public Engine getEngineB() {
return this.engineB;
}
public static void main(String... args) {
Car car = new Car();
System.out.println(car.getEngineA());
System.out.println(car.getEngineB());
}
}
如果我們不使用@Scope,上面的代碼會實例化兩次Engine類,因此會有兩次"Create Engine"輸出。現在我們在有@Scope的情況測試下勞動成果: 輸出
Engine create: 國產發動機
Engine{name='國產發動機'}
Engine{name='國產發動機'}
4.3 全局單例
上面的局部單例例子中,DaggerUserComponent在兩個activity中被各自被實例化一次, 因此產生了兩個不同的對象, 我們需要做到讓Component能夠實現單例,Android中, 我們知道在整個App生命週期中都只有一個Appclication實例,所以在Application中獲得一個唯一的component實例,用它來提供我們需要的單例:
4.3.1 創建UserBean類
public class UserBean {
}
4.3.2 創建BaseModule
和局部單例是一樣的。
@Module
public class BaseModule {
@Singleton
@Provides
UserBean providesUser() {
return new UserBean();
}
}
4.3.3 創建BaseComponent
這個component是用來讓別的component來依賴的, 只需要告訴別的component他可以提供哪些類型的依賴即可。
@Singleton
@Component(modules = BaseModule.class)
public interface BaseComponent {
UserBean getSingletonUser();
}
4.3.4 在application中提供唯一的baseComponent類
public class MyApplication extends Application {
private BaseComponent baseComponent;
@Override
public void onCreate() {
super.onCreate();
baseComponent = DaggerBaseComponent.create();
}
public BaseComponent getBaseComponent() {
return baseComponent;
}
}
4.3.5 自定義一個Scope
4.3.7 中需要創建的UserComponet是需要依賴BaseComponent的,而BaseComponent又是被@singleton標註的,所以UserComponet也需要被@singleton標註,但是已經被@singleton標註過的依賴,不能被被依賴方(下面的UserComponet)使用了,只能自定義一個。
/**
* Scope 標註是Scope
* @Retention(RUNTIME) 運行時級別
*/
@Scope
@Retention(RetentionPolicy.RUNTIME)
public @interface BaseScope {
}
4.3.6 創建UserModule
這裏寫的是自定義業務。
@Module
public class UserModule {
}
4.3.7 創建UserComponet
在我們自己的Component, 使用dependencies依賴於baseComponent, (在@Component註解參數中, 可以依賴多個module和component, 根據自己的業務需求定義即可)
一個Component可以依賴一個或多個Component,並拿到被依賴Component 暴露出來的實例。
tips,如果modules有多個,可以用大括號括起來:@Component(modules = {AModule.class,BModule.class})
@BaseScope
@Component(modules = UserModule.class, dependencies = BaseComponent.class)
public interface UserComponet {
void join(UserActivity userActivity);
void join(User2Activity user2Activity);
}
4.3.8 activity中測試
public class UserActivity extends AppCompatActivity {
private TextView textView;
@Inject
UserBean userBeanA;
@Inject
UserBean userBeanB;
@Inject
UserBean userBeanC;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_student);
((TextView) findViewById(R.id.text)).setText("第一個界面");
textView = findViewById(R.id.textView);
DaggerUserComponet.builder().baseComponent(((MyApplication) getApplication()).getBaseComponent()).build().join(this);
}
public void 點擊全局單例(View view) {
textView.setText("userA:" + userBeanA.hashCode() + "\n"
+ "userB:" + userBeanB.hashCode() + "\n"
+ "userC:" + userBeanC.hashCode() + "\n");
}
public void 下一個全局單例(View view) {
startActivity(new Intent(this, User2Activity.class));
}
}
一開始看上去有點繞有點多,實際上可以這樣理解:用了兩個局部單例:不管什麼activity,在使用DaggerUserComponet注入的時候,因爲UserComponet依賴了baseComponent,而baseComponent是由application創建的,只會有一個。對baseComponent而言,它有的userbean又是@Singleton標註的,也是唯一。以此實現了app整個生命週期的全局單例。
參考文章:
Dagger2源碼分析(三)從源碼角度分析註解在Dagger2中的使用
https://docs.oracle.com/javaee/7/api/javax/inject/package-summary.html
https://jcp.org/en/jsr/detail?id=330
dagger2從入門到放棄-Component的繼承體系、局部單例