Dagger 2 使用及原理

Dagger 2 是 Google 開源的一款依賴注入框架,它的前身是 square 的 Dagger 1,Dagger 2 在 Android 中有着較爲廣泛的應用。

Dagger 2 基於 Java 註解,採用 annotationProcessor(註解處理器) 在項目編譯時動態生成依賴注入需要的 Java 代碼,然後我們在合適的位置手動完成最終的依賴注入,而不是 Dagger 1 中基於反射的解決方案,所以在性能上是有保障的。

關於 annotationProcessor,有興趣的可以看我之前的一篇文章ButterKnife 原理解析

Dagger 2 雖然優秀,但對於新手上手使用可能還有一定的門檻,很容易就從入門到放棄子。這裏記錄下自己學習 Dagger 2 的過程、以及自己的理解。

一、依賴注入

依賴注入通俗的理解就是,當A類需要引用B類的對象時,將B類的對象傳入A類的過程就是依賴注入。依賴注入最重要的作用就是解耦,降低類之間的耦合,保證代碼的健壯性、可維護性、可擴展性。

常用的實現方式有構造函數set方法實現接口等。例如:

// 通過構造函數
public class A {
    B b;
    public A(B b) {
        this.b = b;
    }
}

// 通過set方法
public class A {
    B b;
    public void setB(B b) {
        this.b = b;
    }
}

但這些方式並不是足夠好的,如果有幾百個類需要引用B類的對象,意味着我們需要寫幾百個以B類爲參數的構造函數、或者set方法,所以隨着使用規模的增加會產生大量的模板代碼,這對後期的維護和修改帶來困難。而且在 Activity、Fragment 這樣的類中,用上邊這些方式似乎很難實現依賴注入。

面對這些問題,更好的 Dagger 2 來了。

二、基本使用

這裏以 Android 中的用法爲例,通過一個小例子來學會用 Dagger 2 完成依賴注入的基本流程。

1、添加依賴庫

在 app module 中的 build.gradle 中添加 Dagger 2 的依賴,有兩部分 Dagger 2 的核心庫,以及註解處理器:

implementation 'com.google.dagger:dagger:2.19'
annotationProcessor 'com.google.dagger:dagger-compiler:2.19'

2、創建提供依賴的類

public class Cat {
    @Inject
    public Cat() {
    }

    @Override
    public String toString() {
        return "喵星人來了!";
    }
}

我們定義了一個 Cat 類,並在它的無參構造函數上使用了 @Inject註解,告訴 Dagger 2 這個無參構造函數可以被用來創建 Cat 對象,即依賴的提供方,至於原理後邊再說。

3、創建使用依賴對象的類

public class MainActivity extends AppCompatActivity {
    @Inject
    Cat cat;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
 
        Log.e("cat", cat.toString());
    }
}

在 MainActivity 中創建了一個 cat變量,並加上了 @Inject註解,來告訴 Dagger2 你要爲cat賦值,即依賴注入。所以 MainActivity 就是依賴的需求方

4、創建依賴注入組件

現在依賴的提供方 Cat 類有了,依賴的需求方 MainActivity 也有了,那麼如何將依賴的提供方和需求方關聯起來呢。類似於我們日常的購物,賣家和買家需要通過電商平臺的中介來完成供需信息的交換,完成最終的交易。

在 Dagger 2 中也有類似的中介,那就是Component,它負責完成依賴注入的過程,我們可以叫它依賴注入組件,大致的意思就是依賴需求方需要什麼類型的對象,依賴注入組件就從依賴提供方中拿到對應類型的對象,然後進行賦值。

所以我們需要使用@Component註解來定義一個MainComponent接口:

@Component
public interface MainComponent {
    void inject(MainActivity activity);
}

inject方法的參數是依賴需求方的類型,即例子中的 MainActivity,注意不可是基類的類型。

但這只是個接口,沒法直接使用呀,肯定還需要具體的依賴注入邏輯的,當然,但這些工作框架會幫我們做的。我們前邊說過 Dagger 2 採用了annotationProcessor技術,在項目編譯時動態生成依賴注入需要的 Java 代碼。

此時我們編譯項目,由於使用了@Component註解,框架會自動幫我們生成一個MainComponent接口的實現類DaggerMainComponent,這個類可以在app\build\generated\source\apt\debug\包名目錄下找到。

所以,DaggerMainComponent就是真正的依賴注入組件,最後在 MainActivity 中添加最終完成依賴注入的代碼:

public class MainActivity extends AppCompatActivity {
    @Inject
    Cat cat;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
 
        DaggerMainComponent.builder()
                .build()
                .inject(this);

        Log.e("cat", cat.toString());
    }

然後運行項目,就可以看到如下的 Log,說明依賴注入成功了。


5、通用的依賴提供方

前邊我們的 Cat 類並沒有帶參的構造函數,可以直接在其無參的構造函數使用@Inject註解,進而當做依賴提供方來提供對象。但實際的情況可能並沒有這麼簡單,可能構造函數需要參數,或者類是第三方提供的我們無法修改等,導致我們無法使用@Inject,這些情況下就需要我們自定義依賴提供方了。

在 Dagger 2 中,如果一個類使用了@Module註解,那麼這個類就可以用來提供依賴對象:

@Module
public class MainModule {
}

如果現在有如下的 Flower 類:

public class Flower {
    private String name;
    private String color;

    public Flower(String name, String color) {
        this.name = name;
        this.color = color;
    }

    @Override
    public String toString() {
        return "Flower{" + "name='" + name + "', color='" + color + "'}";
    }
}

如果要讓 MainModule 類可以提供 Flower 對象,可以做如下修改:

@Module
public class MainModule {
    @Provides
    public Flower provideRedRose() {
        return new Flower("玫瑰", "紅色");
    }
}

即聲明瞭一個provideRedFlower()方法,並使用了@Provides註解,這樣在底層 MainModule 類就可以調用provideRedRose()方法,來提供對應的依賴對象了。

接下來通過如下方式將 MainModule 和 MainComponent 關聯起來,這樣依賴注入組件就知道從哪個依賴提供方取數據了:

@Component(modules = {MainModule.class})
public interface MainComponent {
    void inject(MainActivity activity);
}

編譯項目後修改依賴注入配置代碼:

public class MainActivity extends AppCompatActivity {
    @Inject
    Flower flower;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        DaggerMainComponent.builder()
                 // 設置 MainModule 對象
                .mainModule(new MainModule())
                .build()
                .inject(this);

        Log.e("flower", flower.toString());
    }
}

運行後可以看到如下 Log 信息:


所以通過 MainModule 類來提供依賴對象,更加的通用,這樣我們也可以去掉 Cat 類中的 @Inject註解,進而通過 MainModule 類來提供依賴對象,更容易維護。

相信到這裏你已經掌握了 Dagger 2 的基本使用流程,知道了@Inject@Component@Module@Provides這幾個註解的用途。

三、原理

相比機械性的記住這些註解以及使用流程,Dagger 2 背後的工作原理更加重要,這樣我們將明白各個模塊的職責,以及框架是如何將它們關聯起來幫助我們完成依賴注入的,明白了原理後使用 Dagger 2 就會更加得心應手了。

所以我們這裏先了解 Dagger 2 背後的基本原理,然後再學習其它內容。

以前邊的demo爲例,項目編譯後會在app\build\generated\source\apt\debug\包名目錄下生成依賴注入的相關類,如下:

按照之前說法,DaggerMainComponent是完成依賴注入的核心,所以從這個類開始分析,它的源碼如下:

public final class DaggerMainComponent implements MainComponent {
  private MainModule mainModule;

  private DaggerMainComponent(Builder builder) {
    initialize(builder);
  }

  public static Builder builder() {
    return new Builder();
  }

  @SuppressWarnings("unchecked")
  private void initialize(final Builder builder) {
    this.mainModule = builder.mainModule;
  }
  // 重寫inject方法,完成依賴注入
  @Override
  public void inject(MainActivity activity) {
    injectMainActivity(activity);
  }
  // 完成依賴注入的重要方法
  private MainActivity injectMainActivity(MainActivity instance) {
    // 給 MainActivity 中的 cat 成員變量賦值
    MainActivity_MembersInjector.injectCat(instance, new Cat());
    // 給 MainActivity 中的 flower 成員變量賦值
    MainActivity_MembersInjector.injectFlower1(
        instance, MainModule_ProvideRedRoseFactory.proxyProvideRedRose(mainModule));
    return instance;
  }
 
  public static final class Builder {
    private MainModule mainModule;

    private Builder() {}
    // 完成DaggerMainComponent對象的創建
    public MainComponent build() {
      if (mainModule == null) {
        this.mainModule = new MainModule();
      }
      return new DaggerMainComponent(this);
    }
    // 設置 mainModule 對象
    public Builder mainModule(MainModule mainModule) {
      this.mainModule = Preconditions.checkNotNull(mainModule);
      return this;
    }
  }
}

典型的 Builder 構建模式,再結合之前 DaggerMainComponent 的用法來分析:

DaggerMainComponent.builder()
                .mainModule(new MainModule())
                .build()
                .inject(this);

先通過builder()方法創建一個 Builder 對象,再通過其mainModule()方法設置mainModule 對象,接下來用 build()方法就是創建DaggerMainComponent 對象,這樣它裏邊也有了一個mainModule對象。

在 DaggerMainComponent 中還重寫了 MainComponent 接口的inject()方法,裏邊調用的injectMainActivity()方法是完成依賴注入的關鍵:

private MainActivity injectMainActivity(MainActivity instance) {
    MainActivity_MembersInjector.injectCat(instance, new Cat());
    MainActivity_MembersInjector.injectFlower1(
        instance, MainModule_ProvideRedRoseFactory.proxyProvideRedRose(mainModule));
    return instance;
  }

首先是調用MainActivity_MembersInjector類的injectCat()方法直接創建一個 Cat 對象,完成 MainActivity 中 cat 的賦值,injectCat()方法聲明如下:

public static void injectCat(MainActivity instance, Cat cat) {
    instance.cat = cat;
  }

然後是調用injectFlower()方法,完成 MainActivity 中 flower 的賦值,那麼 flower 對象的值從哪裏來呢?這裏調用了MainModule_ProvideRedRoseFactoryproxyProvideRedRose()方法:

public static Flower proxyProvideRedRose(MainModule instance) {
    return Preconditions.checkNotNull(
        instance.provideRedRose(), "Cannot return null from a non-@Nullable @Provides method");
  }

裏邊最終是調用了我們在 MainModule 中聲明的 provideRedRose() 方法,所以在 DaggerMainComponent 內部是通過 MainActivity_MembersInjector 完成了最終的依賴注入。所以當在 Activity 中執行inject(this)方法時,就是開始創建依賴對象,並完成注入工作。

到這裏整個依賴注入的流程就結束了,從源碼的角度來看,整個過程更像是醫生給患者注射藥物。我們可以把依賴注入組件 DaggerMainComponent 看做“醫生”,把 MainActivity_MembersInjector 看做“注射器”,MainModule 就是“藥物”,MainActivity 就是“患者”,醫生用注射器把藥物送到患者體內。

源碼中的細節,還是建議自己結合使用流程看一篇,可能有些疑問就恍然大悟了。

四、一些使用場景

Dagger 2 提供了一些好用的註解、模式來幫我們解決一些實際中遇到的問題。

1、@Named、@Qualifier

試想一下,如果 MainActivity 中需要注入多個 Flower 對象,例如紅玫瑰、白玫瑰、藍玫瑰,我們必然需要在 MainModule 中提供多個類似provideXXXRose()的方法,但是問題來了,我們需要將 MainModule 中多個provide 方法和 MainActivity 中聲明的多個 Flower 變量對應起來呢,否則 Dagger 2 也不知道把那個 provide 方法的返回值賦給那個 Flower 變量。

爲了解決這個問題 Dagger 2 提供了@Named註解,該註解需要設置一個屬性值來區分類型。來看如何使用,首先給 MainModule 中的方法加上@Named

@Module
public class MainModule {
    @Named("red")
    @Provides
    public Flower provideRedRose() {
        return new Flower("玫瑰", "紅色");
    }

    @Named("white")
    @Provides
    public Flower provideWhiteRose() {
        return new Flower("玫瑰", "白色");
    }

@Named("red")就表示紅玫瑰,@Named("white")就表示白玫瑰。我們還需要給 MainActivity 中的 Flower 變量加上對應的註解,這樣就可以把它們關聯起來了:

public class MainActivity extends AppCompatActivity {
    @Named("red")
    @Inject
    Flower flower1;

    @Named("white")
    @Inject
    Flower flower2;
}

可能有人要說了,@Named需要屬性值,或者不想使用 Named 這個名字,也很簡單,那就需要使用@Qualifier自定義註解,怎麼自定呢,先看@Named是如何實現的:

@Qualifier
@Documented
@Retention(RUNTIME)
public @interface Named {
    String value() default "";
}

照貓畫虎,我們可以自定義一個@QualifierBlue註解,來區分藍玫瑰:

@Qualifier
@Retention(RetentionPolicy.RUNTIME)
public @interface QualifierBlue {
}

用法也類似:

@QualifierBlue
@Provides
public Flower provideBlueRose() {
    return new Flower("玫瑰", "藍色");
}
@QualifierBlue
@Inject
Flower flower3;

2、@Singleton、@Scope

現在有一個 Book 類,在 Activity 中需要注入多個 Book 對象,但要求注入的是同一個 Book 對象,按照下面的寫法可以實現嗎?

@Module
public class DetailModule {
    @Provides
    public Book provideBook() {
        return new Book("Kotlin 指南", 66.8f);
    }
}
@Component(modules = {DetailModule.class})
public interface DetailComponent {
    void inject(DetailActivity activity);
}
public class DetailActivity extends AppCompatActivity {
    @Inject
    Book book1;

    @Inject
    Book book2;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_detatil);

        DaggerDetailComponent.builder()
                .detailModule(new DetailModule())
                .build()
                .inject(this);

        Log.e("book1", book1.toString());
        Log.e("book2", book2.toString());
    }
}

觀察輸入的 Log:


可以看到 book1、book2 並不是同一個對象,爲了實現這樣的功能, Dagger 2 提供了一個@Singleton註解,只需要給 Component 接口、Module 中的方法加上@Singleton註解就可以了:

@Singleton
@Component(modules = {DetailModule.class})
public interface DetailComponent {
    void inject(DetailActivity activity);
}
@Module
public class DetailModule {
    @Singleton
    @Provides
    public Book provideBook() {
        return new Book("Kotlin 指南", 66.8f);
    }
}

再運行看效果:


我們的目標實現了,所以@Singleton註解註解可以實現“單例”的效果,但這個單例只是局部的,只在單個 Activity 有效,就是說如果是兩個 Activity 想得到同一個對象,就失效了。關於這個問題我們後邊再討論。

同時我們還可以自定義類似@Singleton的註解,爲什麼要自定義呢,可以是爲了代碼的可讀性,在不同的場景有更明確的指向性。例如可以定義一個 DetailActivityScope 註解,替換掉上邊的@Singleton,這樣就能更好的體現 Book 對象在 DetailActivity 中是單例的。

要定義 DetailActivityScope 註解可以使用@Scope註解,具體參考@Singleton的實現:

@Scope
@Retention(RetentionPolicy.RUNTIME)
public @interface DetailActivityScope {
}

現在就可以用@DetailActivityScope替換掉上邊的@Singleton,最終的效果是一樣的。

3、dependencies

我們現在看上邊遺留的問題,通過 Dagger 2 如何實現全局單例,比如多個 Activity 如何共享同一個對象。在多個 Module 類裏定義相同的 provide 方法是行不通的,例如:

@Singleton
@Provides
public Book provideBook() {
    return new Book("Flutter 指南", 68.8f);
}

因爲在多個 Activity 中 Component、 Module 類都已經不是同一個了,自然不能保證 Book 是同一個了。

Dagger 2 的@Component註解可以設置dependencies屬性,來依賴其它的 Component,這樣我們可以定義一套公共的 Component + Module,讓需要的 Component 來依賴公共的 Component,這樣問題就解決了。具體如何做,接下來詳細說。

首先定義公共的 Module 和 Component:

@Module
public class CommonModule {
    @Singleton
    @Provides
    public Book provideBook() {
        return new Book("Flutter 指南", 68.8f);
    }
}
@Singleton
@Component(modules = {CommonModule.class})
public interface CommonComponent {
    Book provideBook();
}

都使用了@Singleton註解。注意,這個 CommonComponent 和我們之前的不太一樣,並沒有inject方法,可以這樣理解,CommonComponent 並不是直接用來對應Activity 完成以依賴注入的,而是告訴依賴它的 Component 我可以給你提供什麼依賴對象,所以這裏定義了一個provideBook()方法,和 CommonModule 中的方法對應。

接下來讓 MainComponent、ShareComponent 依賴 CommonComponent:

@CommonScope
@Component(modules = {MainModule.class}, dependencies = {CommonComponent.class})
public interface MainComponent {
    void inject(MainActivity activity);
}
@CommonScope
@Component(dependencies = {CommonComponent.class})
public interface ShareComponent {
    void inject(ShareActivity activity);
}

編譯項目後,就可以進行依賴注入的配置了:

DaggerMainComponent.builder()
                .mainModule(new MainModule())
                .commonComponent(???)
                .build()
                .inject(this);

問題來了,commonComponent()方法需要一個 CommonComponent 對象,同理在 ShareActivity 中也需要一個 CommonComponent 對象,這裏我們要保證兩個 CommonComponent 對象是同一個,原因前邊已經說了,不同的 Component 對象自然無法提供相同的依賴對象。要獲得相同的 CommonComponent 對象,簡單的做法可以通過 Application 來實現,因爲在單進程的應用中,Application 的只被初始化一次,可以保證唯一性:

public class App extends Application {
    private CommonComponent commonComponent;

    @Override
    public void onCreate() {
        super.onCreate();
        commonComponent = DaggerCommonComponent.builder().commonModule(new CommonModule()).build();
    }

    public CommonComponent getCommonComponent() {
        return commonComponent;
    }
}

別忘了在 AndroidManifest 中配置自定義的 Application。
所以 CommonComponent 對象的配置可以這樣寫:
commonComponent(((App) getApplication()).getCommonComponent())

我們在兩個 Activity 中分別依賴一個 Book 對象,並通過 Log 輸出:




是我們想要的結果。

4、@Subcomponent

@Subcomponent是用來實現一個子依賴注入組件的,就是說使用@Subcomponent註解的 Component 可以有一個父依賴注入組件,但這種父子關係並不是通過傳統的繼承方式實現的。自然的,子依賴注入組件會擁有父依賴注入組件的功能。

首先定義一個使用@Subcomponent註解的依賴注入組件接口:

@Subcomponent(modules = {SubModule.class})
public interface MySubComponent {
    void inject(SubActivity activity);
}

它依賴的 SubModule 爲:

@Module
public class SubModule {
    @Provides
    public Flower provideFlower() {
        return new Flower("臘梅", "紅色");
    }
}

如果我們想讓 DetailComponent 做爲 MySubComponent 的父組件,則需要在 DetailComponent 中定義一個返回 MySubComponent 的方法,方法參數爲其依賴的 Module 類型:

@DetailActivityScope
@Component(modules = {DetailModule.class})
public interface DetailComponent {
    void inject(DetailActivity activity);

    // 定義返回子組件的方法,參數爲子組件需要的module
    MySubComponent getSubComponent(SubModule module);
}

到這裏我們的子組件就實現完成了,如何使用呢?子依賴注入注入組件是不能單獨直接使用的,因爲編譯後並不會生成類似DaggerMySubComponent的輔助類,所以需要通過父組件來獲取,這也是我們需要在父組件中定義返回子組件方法的原因。具體的用法如下:

public class SubActivity extends AppCompatActivity {
    @Inject
    Book book;

    @Inject
    Flower flower;

    public static void start(Context context) {
        context.startActivity(new Intent(context, SubActivity.class));
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_sub);
        // 創建父組件對象
        DetailComponent detailComponent = DaggerDetailComponent.builder().detailModule(new DetailModule()).build();
        // 得到子組件,並完成依賴注入
        detailComponent.getSubComponent(new SubModule()).inject(this);

        Log.e("SubActivity-book", book.toString());
        Log.e("flower", flower.toString());
    }
}

我們並沒有在 SubModule 中定義提供 Flowerd 對象的方法,但是同過這種“繼承”,MySubComponent 就可以提供 Flower 對象了。運行結果如圖:


五、小結

Dagger 2 的主要內容就這些了,使用 Dagger 2 必然要編寫相關的輔助類、接口、使用各種註解,雖然沒有直接 new 一個對象或者傳統的依賴注入方式簡單,但 Dagger 2 帶來的是更好的代碼解耦,更有利於後期的擴展維護,對於那些需要長期維護的項目這一點是更加重要的。

其實,基於 Dagger 2 還有一個 DaggerAndroid 擴展庫,可以進一步簡化依賴注入的步驟,減少模板代碼,更適合 Android 開發。關於 DaggerAndroid 的用法和原理,下篇文章見。

示例代碼在 https://github.com/SheHuan/Dagger2Demo 的 master 分支上。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章