我所了解的 Dagger2(一)

自从 Dagger2 出现,个人对 Dagger2 的学习过程也是断断续续的,一直没有系统的总结过,所以也谈不上掌握,还有就是没有投入到项目的使用。

本文的目的:希望能尽量详细的整理总结一下 Dagger2 的相关知识,尽量避免有所遗漏,造成大家需要查找大量的文章进行查漏补缺。以及用由简到繁的几个例子来介绍一下如何使用,以方便日后翻阅。

简介

Dagger2 是什么?为什么要使用?这也是我接触 Dagger2 以来一直有思考的事,在没有一个大概的认识之前,也疑惑过是不是有必要去使用,但是这里我不会说有没有必要,我希望大家能在看完本文之后对 Dagger2 有一个大概的认识。

A fast dependency injector for Android and Java.

Dagger2 是一个 Android 和 Java 的依赖注入框架。而「依赖注入」(Dependency Injection)则是实现「控制反转」(Inversion of Control)的一种方式,是一种面向对象的编程模式,主要目的就是为了降低耦合。

Dagger2 和其他依赖注入框架的区别是:Dagger2 是在编译阶段生成注入代码,不再使用反射,所以不会影响到性能。在应用运行的时候其实运行的是真正的 Java 代码,可以直接断点调试。

使用前需要了解的原则
依赖倒置原则
  1. 高层次的模块不应该依赖于低层次的模块,他们都应该依赖于抽象。
  2. 抽象不应该依赖于具体实现,具体实现应该依赖于抽象。
依赖注入的方式
  1. 构造函数注入
  2. setter注入
  3. 接口注入

如果对这些原则和「控制反转」有所疑问的话强烈推荐大家看下下面几篇文章,文章不长但是比较益于我们理解。这里也不再举其他的例子了,感觉也举不出更好的例子,但是不建议直接跳过。

使用 Dagger2 前你必须了解的一些设计原则
依赖注入原理
IoC 模式

配置

在 project 的 build.gradle 中添加

    dependencies {
        //...其他
        classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8'//Android gradle plugin 版本是2.2以上可以省略
    }

在 module 的 build.gradle 中添加

    apply plugin: 'com.neenbedankt.android-apt'//Android gradle plugin 版本是2.2以上可以省略

    dependencies {
        compile 'com.google.dagger:dagger:2.10'
        apt 'com.google.dagger:dagger-compiler:2.10'//Android gradle plugin 版本是2.2以上,apt 替换成 annotationProcessor
        provided 'org.glassfish:javax.annotation:10.0-b28'
    }

基本使用

@Inject

该注解是在「需要依赖的类的成员变量」上和「被依赖的成员变量的类的构造方法」上标注的。让 Dagger2 知道这个字段需要依赖注入,这样它就会去构造一个类的实例来满足依赖。注意:标注的成员变量不能为 private;如果有多个构造函数只能标注一个。

    public class A {
        B mB;
        public A(){
            mB = new B();
        }
    }

原来上面的代码就可以改写成,下面这样。

    public class A {
        @Inject
        B mB;
        public A(){
            //「1」...这里省略了注入的过程,先跳过
        }
    }
    public class B {
        @Inject
        public B() {

        }
    }
}

单单上边的改写其实是不够的,因为我们省略了注入的过程,所以要介绍 @Component

@Component

该注解是在接口或者抽象类上标注的。它的作用就是把「需要被注入的类的实例」注入到「需要依赖注入的目标类」中。

开始编写Component,来实现「1」中省略的注入过程。

    @Component
    public interface AComponent {
        void inject(A a);
    }
    //----------分割线-------
    //修改 A
    public class A {
        @Inject
        B mB;
        public A() {
            DaggerAComponent.create().inject(this);
        }
    }

到这里其实我们已经成功的初始化了 A 中的成员变量 mB 了,有的同学可能疑问:我们仅仅是创建了一个带 @Component 注解的 interface,这个 DaggerAComponent 是什么?这里的代码和最初的代码看上去麻烦了不少,不是用 new B() 显得更简单嘛,而且也并没有去除 A 类对 B 类的依赖,说好的「依赖倒置原则」呢?

注意: inject(A a) 方法只能传入「需要注入的目标类」,而不能传入「注入目标类的基类」,因为最终 Dagger2 会在 inject 方法传入的类中查找 @inject 标注的成员变量进行初始化。

这里我先回答第一个问题,DaggerAComponent 是在代码编译时期 apt 给我们生成的代码,所以我们 rebuild 一下项目就会生成以 Dagger 为前缀的 Component;至于第二个问题就先带着疑问往下看吧!

@Module

被该注解标注的类主要用于提供依赖,Module 类其实就是一个简单工厂,里面的方法基本上就是用于创建类的实例的方法,Component 可以在指定的 Module 中查找依赖。

单单用 @Inject 配合 @Component 是不够满足我们的开发需求的,因为:

  1. 我们没有办法给三方类库添加 @Inject
  2. @Inject 所标注的构造方法需要我们自己传入参数的情况;
  3. 需要被注入的类有多个构造方法的情况。

所以需要用 @Module

在举例使用 @Module 之前,我们需要了解下 @Provides

@Provides

该注解在 Module 中创建类的实例的方法上标注。Component 在注入的目标类中找到用 @Inject 标注的成员变量后,Component 就会去 Module 中查找用 @Provides 标注的对应的创建类的实例方法来实现注入。

现在就来举例讲下 @Module 的使用了:

举例1、对应「注入第三方类」的情况

现在有一个第三方类 C ,我们同样需要把 C 注入到 A 中,现在我们继续修改代码。首先我们需要添加一个 AModule 类;

    @Module
    public class AModule {
        @Provides
        C provideC(){
            return new C();
        }
    }

然后修改 A 类和之前已经定义好的 AComponent;

    @Component(modules={AModule.class})//在指定的 AModule 中查找依赖,可以有多个 Module
    public interface AComponent {
        void inject(A a);
    }
    //----------分割线-------
    //修改 A
    public class A {
        @Inject
        B mB;
        @Inject
        C mC;
        public A() {
            DaggerAComponent.create().inject(this);
            //DaggerAComponent.builder().aModule(new AModule()).build().inject(this);
            //两种写法都是可以的,只不过 AModule() 本身不需要参数,所以 create() 中直接 builder().build() 的操作
        }
    }
举例2、对应「被注入的类的构造方法需要传参的」的情况

这里又分两种情况:

  1. 需要被依赖注入的类的构造函数已经用 @Inject 标注;

添加带参构造的类 D,修改类 AModule 与类 A,AComponent 保持不变;

    public class D {
        private String name;
        @Inject
        public D(String name) {
            this.name = name;
        }
    }

    @Component(modules={AModule.class})
    public interface AComponent {
        void inject(A a);
    }

    @Module
    public class AModule {
        private String mDName;
        public AModule(String dName){
            mDName = dName;
        }

        @Provides
        String provideName(){
            return mDName;
        }

        @Provides
        C provideC(){
            return new C();
        }
    }
    //----------分割线-------

    public class A {
        @Inject
        B mB;
        @Inject
        C mC;
        @Inject
        D mD;
        public A() {
            DaggerAComponent.builder().aModule(new AModule("I am D")).build().inject(this);
        }
    }

简单分析一下,因为类 A 中的成员变量 mD 被 @Inject 标注,所以初始化 mD 的时候会去找同样被 @Inject 标注的 D 的构造方法,但是它需要一个 String 类型的 name 来初始化,这个 name 从哪里找呢?

其实就是通过我们调用 DaggerAComponent 的 inject(A a) 的时候传入的 new AModule("I am D"),然后通过 provideName() 方法暴露出去,这样 mD 的 name 属性就被赋值成 "I am D" 了。

因为 AModule 的构造也带参数,所以生成的 DaggerAComponent 也不再有 create() 方法,只能通过 builder().aModule(new AModule("I am D")).build() 构造。

  1. 需要被依赖注入的类的构造函数没有用 @Inject 标注;其实这种情况和「举例1」的情况一致,只是刚好「举例1」中的 C 的构造函数不带参数;这里就简单带过一下。

稍微在上边代码的基础上修改一下。

    public class D {
        private String name;
        public D(String name) {
            this.name = name;
        }
    }
//------------------分割线--------------
    @Module
    public class AModule {
        private String mDName;
        public AModule(String dName){
            mDName = dName;
        }

        @Provides
        D provideD(){
            return new D(mDName);
        }

        @Provides
        C provideC(){
            return new C();
        }
    }

去掉了类 D 构造方法上的注解( 不去掉的话也没事 ),在 provideD() 中 new 出实例 return 出去。

举例3、对应「被注入的类有多个构造方法的情况」

这里我们不再讨论普通的情况,所谓普通的情况就是「举例2.2」的情况,针对不同的构造函数不外乎 new 出不同的实例。现在我们要讨论的是同时存在 @Inject 和在 provideXX() 方法中 new 对象的情况。我们重新修改一下上面的代码。

    public class D {
        private String name;
        public D(String name) {
            this.name = name;
        }
        @Inject
        public D(){
            this.name = "I am default D";
        }

        public String getName() {
            return name;
        }
    }
//-------------分割线------------
    @Module
    public class AModule {
        private String mDName;
        public AModule(String dName){
            mDName = dName;
        }

        @Provides
        D provideD(){
            return new D(mDName);
        }

        @Provides
        C provideC(){
            return new C();
        }
    }
//------------分割线--------------
    public class A {
        @Inject
        B mB;
        @Inject
        C mC;
        @Inject
        D mD;
        public A() {
            DaggerAComponent.builder().aModule(new AModule("I am D")).build().inject(this);
            Log.d("tag", mD.getName());
        }
    }

看下上面的代码,最终我们能打印出来的是 "I am default D" 还是 "I am D" 呢?答案是后者,这里就涉及到 Dagger2 依赖注入的步骤了,在说这个步骤之前我先做一些其他方面的补充。

举例4、一些补充

首先、回答一下上面「@Component」一节中遗留下来的一个疑问:我非要满足「依赖倒置原则」,高层次模块不依赖于低层次模块,而依赖于抽象。其实也可以实现,定义一个接口 ID,让类 D 实现接口 ID,然后把类 AModule 中的 provideD() 返回值和类 A 中的成员变量 mD 的类型修改为 ID。

其次、在「举例1」的代码注释中 Component 可以有多个 Module,这里提一下两种方法,注意: 这多个 Module 之间不能有重复方法。

    //第一种
    @Component(modules={AModule.class, BModule.class})//有多个 Module
    public interface AComponent {
        void inject(A a);
    }
    //第二种 ---------也可以通过下面这种方式构建------------
    @Module(includes={AModule.class, BModule.class})
    public class CModule {
        //...
    }

    @Component(modules={CModule.class})
    public interface AComponent {
        void inject(A a);
    }

Dagger2 依赖注入的步骤

步骤1:查找 Module 中是否存在创建该类的方法。
步骤2:若存在创建类方法,查看该方法是否存在参数

步骤2.1:若存在参数,则按从 步骤1 开始依次初始化每个参数
步骤2.2:若不存在参数,则直接初始化该类实例,一次依赖注入到此结束

步骤3:若不存在创建类方法,则查找 @Inject 注解的构造函数,看构造函数是否存在参数

步骤3.1:若存在参数,则从 步骤1 开始依次初始化每个参数
步骤3.2:若不存在参数,则直接初始化该类实例,一次依赖注入到此结束

不难看到,查找类的实例的过程中 Module 的级别要高于 Inject。
到这里相信大家基本可以使用 @Inject@Component@Module@Provides了,也包括他们在 MVP 中的使用,仅仅需要把类 A 替换成 Activity 或者 Fragment,把类 D 替换成 Presenter,把需要通过 AModule 传递给 D(Presenter) 的 String 替换成 IView 之类的 View 层的接口罢了。

完善使用

上面我们仅仅讲了四个注解的用法,当然这还远远不够,如果累了先休息一下,完了我们再继续介绍。

@Qualifier

该注解是为了实现类似 @Named 这种用于区分需要依赖注入相同类型的不同成员变量的功能的注解。简单的来说 @Named@Qualifier 的一种。

举个例子:现在我们需要在类 A 中注入两个 D 类型的成员变量,一个要求是默认的,另一个要求我们自己传入参数,下面看具体代码。

    public class D {
        private String name;

        public D(String name) {
            this.name = name;
        }
        public D(){
            this.name = "I am default D";
        }

        public String getName() {
            return name;
        }
    }
//-------------分割线------------
    @Module
    public class AModule {
        private String mDName;
        public AModule(String dName){
            mDName = dName;
        }
        @Named("custom")
        @Provides
        D provideD(){
            return new D(mDName);
        }

        @Named("default")
        @Provides
        D provideDefaultD(){
            return new D();
        }
    }
//------------分割线--------------
    public class A {
        //...省略其他
        @Named("default")
        @Inject
        D mD1;
        @Named("custom")
        @Inject
        D mD2;
        public A() {
            DaggerAComponent.builder().aModule(new AModule("I am D")).build().inject(this);
            Log.d("tag", "mD1:" + mD1.getName() + ”,mD2:" + mD2.getName());
        }
    }

这里在 A 中的成员变量 mD1 和 mD2 上以及 AModule 中添加的两个返回值为 D 类型的 provide 方法上,分别添加了注解 @Named("default")@Named("custom")。不难猜到打印出来的结果为:

    mD1:I am default D,mD2:I am D

我们看下 @Named 的代码:

    @Qualifier
    @Documented
    @Retention(RUNTIME)
    public @interface Named {
        /** The name. */
        String value() default "";
    }

所以 @Named 后面跟着的参数为 String 类型,Component 会在指定的 Module 中寻找同时满足类型和 @Named 的依赖进行注入。

当然我们可以自定义一个 @Num 用 int 类型来做参数:至于使用方法就不再多说了。

    @Qualifier
    @Documented
    @Retention(RUNTIME)
    public @interface Num {
        /** The name. */
        int value() default 0;
    }

@Qualifier 就是针对这种有多个相同类型不同成员变量的注入来进行区分。
注意:@Qualifier 同样可以给 Module 的中的方法的参数标注,以区分一个 Module 中有多个返回值类型相同的方法,又有另一个方法需要其中一个或者多个的返回值作为参数的情况。

@Scope

该注解表示作用域的意思,@Singleton 是它 Dagger2 中已有的一个实现。使用 @Scope就可以更好的管理创建的类实例的生命周期。

既然看到了 @Singleton 那么来实现一个单例吧!

目标:

我要实现在两个 Activity 中实现单例。

使用方法:
  1. 在 Module 对应的 Provides 方法标注 @Singleton
  2. 同时在 Component 类标注 @Singleton
    public class ActivitySingle {
    }
    //--------------分割线------------
    @Module
    public class SingleModule {

    @Singleton
    @Provides
    ActivitySingle provideActivitySingle(){
        return new ActivitySingle();
    }
    //--------------分割线------------
    @Singleton
    @Component(modules = {SingleModule.class})
    public interface SingleComponent {
        void inject(SingleOneActivity activity);
        void inject(SingleTwoActivity activity);
    }
    //--------------分割线------------
    public class SingleOneActivity extends AppCompatActivity {

        private TextView mTv1;
        private TextView mTv2;
        @Inject
        ActivitySingle mActivitySingle1;
        @Inject
        ActivitySingle mActivitySingle2;
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_single);
            mTv1 = (TextView) findViewById(R.id.tv1);
            mTv2 = (TextView) findViewById(R.id.tv2);
            DaggerSingleComponent.create().inject(this);
            mTv1.setText(mActivitySingle1.toString());
            mTv2.setText(mActivitySingle2.toString());
        }
    }
    //SingleTwoActivity 和 SingleOneActivity 除了类名完全一致
运行结果:
singleton1.png
singleton2.png

注意:这和说好的单例不一样!我们可以看到在同一个 Activity 中我们实例化的 ActivitySingle 对象的确是单例的,但是这不是我们要的结果。可以看到并不是我们标注了 @Singleton Dagger2 就给我们实现了单例了的。

Java 中,单例通常保存在静态域中,这样的单例往往要等到虚拟机关闭时候,该单例所占用的资源才释放。但是,Dagger 通过 @Singleton 创建出来的单例并不保持在静态域上,而是保留在Component 实例中。

原理

在讲「Dagger2 依赖注入的步骤」中我们知道 Component 查找类的实例的过程中 Module 的级别要高于 Inject。

真正创建单例的方法
  1. 在 AppModule 中定义创建全局类实例的方法;
  2. 创建 AppComponent 来管理 AppModule;
  3. 保证 AppComponent 只有一个实例;

简单的来说就是把上例的 SingleComponent 提升到 Application 中管理并保证只有一个。现在有没有体会到 @Scope 的作用域的含义?

具体实现,后续我会拿一个贴近实际使用的例子来讲,这次就先到这里了,因为后续还会牵扯到 Component 之间的拓展和依赖关系以及组织 Component 篇幅会显得比较长,但是我会保证在两篇文章把 Dagger2 整理完。当然这里 @Scope 并没有讲完,我会后续结合例子补充完整。

其实主要还是靠大家自己动手体会,如果有写的不对的地方请提醒我,避免误导了后来的同学,如果觉得我整理的还不错请为我点个赞,谢谢!

已经被改的不成样的源码

更新

我所了解的 Dagger2(二)

参考资料

等不及的同学可以看下下面的文章,尽量按顺序来看。

使用 Dagger2 前你必须了解的一些设计原则
依赖注入原理
IoC 模式
Android:dagger2 让你爱不释手-基础依赖注入框架篇
Android:dagger2 让你爱不释手-重点概念讲解、融合篇
Android:dagger2 让你爱不释手-终结篇
从零开始搭建一个项目(rxJava + Retrofit + Dagger2) --完结篇
Android 常用开源工具(1)-Dagger2 入门
Android 常用开源工具(2)-Dagger2 进阶
详解 Dagger2
Android 单排上王者系列之 Dagger2 注入原理解析

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,968评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,601评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,220评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,416评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,425评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,144评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,432评论 3 401
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,088评论 0 261
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,586评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,028评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,137评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,783评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,343评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,333评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,559评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,595评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,901评论 2 345

推荐阅读更多精彩内容