Dagger 2 系列(五) -- 進階篇:@Scope 和 @Singleton

leeGYPlus發表於2018-09-21

Dagger2

  • 該系列部落格的最終目標: 搭建 MVP + Dagger2 框架
  • 該系列部落格包含以下幾篇內容:
  1. Dagger 2 系列(一) -- 前奏篇:依賴注入的基本介紹
  2. Dagger 2 系列(二) -- 基礎篇:@Inject、@Component
  3. Dagger 2 系列(三) -- 基礎篇:@Module 和@Provides
  4. Dagger 2 系列(四) -- 基礎篇:@Named 和 @Qualifier
  5. Dagger 2 系列(五) -- 進階篇:@Scope 和 @Singleton

在這篇文章中你會看到什麼:

  1. @Scope 是什麼
  2. @Singleton 是什麼
  3. @Scope@Component 如何協同作戰。

Dagger2 的學習曲線確實是比較陡的,我認為陡的點一是對 依賴注入(控制反轉)概念的理解,所以有了Dagger 2 系列(一) -- 前奏篇:依賴注入的基本介紹,另外一個就是 對 Scope 的理解,對於此我也是翻看了大量的部落格,大部分部落格看完之後的感受依舊是雲裡霧裡的,自己也是經過了學習、擱淺、再學習的往復過程,同時也看了一些國外的部落格,對 Scope 的概念有了基本的認識。

1. @Scope

我們首先看一下 froer_mcs 一文中談到 Scope 能給我們帶來什麼

  • In Dagger 2 scopes mechanism cares about keeping single instance of class as long as its scope exists. In practice it means that instances scoped in @ApplicationScope lives as long as Application object. @ActivityScope keeps references as long as Activity exists (for example we can share single instance of any class between all fragments hosted in this Activity).

  • In short - scopes give us “local singletons” which live as long as scope itself.

  • Annotated dependencies are single-instances but related to component lifecycle (not the whole application).

個人翻譯

Dagger2 中 Scope 機制保證在 Scope 的作用域內類會保持單例。在實際開發中這意味著在 @ApplicationScope 對應的作用域中類的例項物件的生命會像 Application 一樣長,在 @ActivityScope 的作用域內的類例項的生命週期和相應的 Activity 一樣長。(不要想當然的認為 Dagger2 會根據 Scope 註解的字面意義實現相應的類例項的單例效果,實現這樣的效果是需要具體實現的。) 總的來說, Scope 機制會保證在 Scope 的生命週期內實現 "本地單例" 在 Component 的生命週期內,Scope 註解依賴會保證單例。(也就是說,此處的單例是 Component 生命週期內的單例,如果 Component 例項物件重新例項化的,則單例效果失效。)

通過以上的引用和翻譯不知道你是否重新認識了 Scope ,在上文中一個反覆強調的概念:

在 Dagger2 中 Scope 機制可以保證在 Scope 標記的 Component 作用域內 ,類會保持單例 。 (敲黑板,這句話很重要)

2. @Singleton

重申一遍:

**在 Dagger2 中 Scope 機制可以保證在 Scope 標記的 Component 作用域內 ,類會保持單例 **

如果理解了這句話,那麼回過頭來看 @Singleton 這個註解,是不是有一種豁然開朗的感覺。並不是只有 @Singleton 註解標記的相關類生產的例項是單例的,是所有的 Scope(自定義 Scope) 標記的相關類生產的例項 都是單例 的,只不過這個單例是有條件的 -- 在 Scope 註解標記 Component 的作用域內生產的例項是單例的

Scope 機制下的單例其實和 @Singleton 的字面意義 沒有半毛錢關係,當初自己就是被這種錯誤的思想誤導了很長時間。其實如果你願意你,可以把 @Singleton 換成任意單詞,什麼 @Dog@Cat@XXx 都可以,你只要保證這個註解標記的 Component 在 App 程式中為單例的,並且得到正確的實現(被正確的標記到 類構造器 或 Module 中的 @Provides 標記的方法),那麼它對應生成的類例項就是 單例 的。

@Singleton 之所以被預設實現,只是因為這可以讓人根據它的字面意思,知道被他標記的相關生成的類例項為單例,這符合了 Java 的命名規範。

3. 示例程式碼

上面談到的全都是理論,那麼我們就是用相應的程式碼來驗證他們。

  • 自定義 Scope
@Scope
@Retention(RetentionPolicy.RUNTIME)
public @interface AnyOneScope {
}
複製程式碼

這裡為了表明最後的 單例Scope 的命名沒有任何關係,名字避免使用了容易給人造成疑惑的 ApplicationScopeActivityScope 等,而使用了 AnyOneScope,但是其實這些名字都是無所謂的 。

  • POJO -- AppleBean
public class AppleBean {

    private String color;
    private int weight;

    public AppleBean() {
        Log.e("TAG", "AppleBean");
    }
}
複製程式碼
  • POJO -- OrgranBean
public class OrgranBean {

    private String color;
    private int weight;

    public OrgranBean() {
        Log.e("TAG", "OrgranBean");
    }
}
複製程式碼
  • Module
@Module
public class FruitModule {

    @AnyOneScope
    @Provides
    AppleBean provideApple() {
        return new AppleBean();
    }

    @AnyOneScope
    @Provides
    OrgranBean provideOrgran() {
        return new OrgranBean();
    }
}
複製程式碼

Module 提供 AppleBeanOrgranBean 例項物件的方法,兩個方法使用 @AnyOneScope 進行註解。

  • Component
@AnyOneScope
@Component(modules = {FruitModule.class})
public interface FruitComponent {
    void inject(FuriteScopeActivity mTestScopeActivity);
}
複製程式碼
  • 目標類 (注入類)
public class FuriteScopeActivity extends AppCompatActivity {

    @Inject
    AppleBean mAppleBeanA;
    @Inject
    AppleBean mAppleBeanB;
    @Inject
    OrgranBean mOrgranBeanA;
    @Inject
    OrgranBean mOrgranBeanB;
    FruitComponent mFruitComponent;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_test_scope);
        mFruitComponent = DaggerFruitComponent.builder().fruitModule(new FruitModule()).build();
        mFruitComponent.inject(this);// 完成注入,沒有這句話是不行的

        Log.e("TAG", "mFruitComponent1:" + mFruitComponent.toString());
        Log.e("TAG", "mAppleBeanA:" + mAppleBeanA.toString());
        Log.e("TAG", "mAppleBeanB:" + mAppleBeanB.toString());
        Log.e("TAG", "mOrgranBeanA:" + mOrgranBeanA.toString());
        Log.e("TAG", "mOrgranBeanB:" + mOrgranBeanB.toString());
    }
}
複製程式碼

如程式碼所示,在完成注入後分別對 AppleBeanOrgranBean 分別呼叫了兩次例項,按照上文中 我們對 @Scope 的理解,那麼在這裡兩個類分別生成的類例項為同一個,下面我們執行程式碼,檢視日誌來驗證一下。

列印日誌如下:

E/TAG: mFruitComponent1:com.example.administrator.dagger2demo.practiceFifth.DaggerFruitComponent@7c6e469
    mAppleBeanA:com.example.administrator.dagger2demo.practiceFifth.AppleBean@b7d6eee
    mAppleBeanB:com.example.administrator.dagger2demo.practiceFifth.AppleBean@b7d6eee
    mOrgranBeanA:com.example.administrator.dagger2demo.practiceFifth.OrgranBean@648ef8f
    mOrgranBeanB:com.example.administrator.dagger2demo.practiceFifth.OrgranBean@648ef8f

複製程式碼

可以看到日誌分別列印了 FruitComponent 的例項 -- mFruitComponentAppleBean 的兩個例項 -- mAppleBeanAmAppleBeanBOrgranBean 的兩個例項 -- mOrgranBeanAmOrgranBeanB,發現 mAppleBeanAmAppleBeanB 的雜湊值相同即為 同一個物件mOrgranBeanAmOrgranBeanB 的雜湊值相同即為 同一個物件,驗證了我們對 @Scope 的認識 -- 實現單例

同時,為了驗證 單例 是有作用域的 -- Component 的作用域內,在 FuriteScopeActivity 新增以下方法:

public void jump(View view) {
        mFruitComponent = DaggerFruitComponent.builder().fruitModule(new FruitModule()).build();
        mFruitComponent.inject(this);// 完成注入,沒有這句話是不行的

        Log.e("TAG", "mFruitComponent2:" + mFruitComponent.toString());
        Log.e("TAG", "mAppleBeanA:" + mAppleBeanA.toString());
        Log.e("TAG", "mAppleBeanB:" + mAppleBeanB.toString());
        Log.e("TAG", "mOrgranBeanA:" + mOrgranBeanA.toString());
        Log.e("TAG", "mOrgranBeanB:" + mOrgranBeanB.toString());
    }
複製程式碼

重新執行程式,觀察列印日誌:

  • 執行程式後觸發的日誌資訊:
09-21 14:50:54.903 14184-14184/com.example.administrator.dagger2demo E/TAG: AppleBean
    OrgranBean
    mFruitComponent1:com.example.administrator.dagger2demo.practiceFifth.DaggerFruitComponent@7c6e469
    mAppleBeanA:com.example.administrator.dagger2demo.practiceFifth.AppleBean@b7d6eee
    mAppleBeanB:com.example.administrator.dagger2demo.practiceFifth.AppleBean@b7d6eee
    mOrgranBeanA:com.example.administrator.dagger2demo.practiceFifth.OrgranBean@648ef8f
09-21 14:50:54.904 14184-14184/com.example.administrator.dagger2demo E/TAG: mOrgranBeanB:com.example.administrator.dagger2demo.practiceFifth.OrgranBean@648ef8f
複製程式碼

生成的物件和雜湊值的對應關係為:

  • DaggerFruitComponent --> 7c6e469
  • AppleBean --> b7d6eee
  • OrgranBean --> 648ef8f
  • 觸發 jump() 方法後的日誌資訊如下:
09-21 14:53:37.624 14184-14184/com.example.administrator.dagger2demo E/TAG: AppleBean
    OrgranBean
    mFruitComponent2:com.example.administrator.dagger2demo.practiceFifth.DaggerFruitComponent@8196f9e
    mAppleBeanA:com.example.administrator.dagger2demo.practiceFifth.AppleBean@50ada7f
    mAppleBeanB:com.example.administrator.dagger2demo.practiceFifth.AppleBean@50ada7f
    mOrgranBeanA:com.example.administrator.dagger2demo.practiceFifth.OrgranBean@16bea4c
    mOrgranBeanB:com.example.administrator.dagger2demo.practiceFifth.OrgranBean@16bea4c
複製程式碼

生成的物件和雜湊值的對應關係為:

  • FruitComponent --> 8196f9e
  • AppleBean --> 50ada7f
  • OrgranBean --> 16bea4c

很明顯 AppleBeanOrgranBean 重新生成了新的物件,難道不是單例了?難道上文的結論是錯誤的?其實不是這樣的,因為 FruitComponent 生成了新的物件,所以其作用域下的類重新生成了新的例項。 證明了:在 Scope 註解標記 Component 的作用域內生產的例項是單例的。

4. 總結

至此,對 Dagger2 中的 Scope 的理解就如上文所示了,我認為內容比較容易內容理解,而且比較淺顯,不會增添你的疑惑,希望自己的這篇學習記錄可以幫助到看到的所有同學。

以上程式碼你可以在這裡看到 GitHub--Dagger2Demo


參考資料

Dependency injection with Dagger 2 - the API

Dependency injection with Dagger 2 - Custom scopes


相關文章