Dagger2——(1)入门详解

Dagger2 确实比较难学,我想每个开发者学习的时候总是经历了一番痛苦的挣扎过程,于是就有了所谓的从入门到放弃之类的玩笑,当然不排除基础好的同学能够一眼看穿。本文的目的尝试用比较容易理解的角度去解释 Dagger2 这样东西。

Dagger2 是有门槛的,这样不同水平能力的开发者去学习这一块的时候,感受到的压力是不一样的。

我个人总结了大家在学习 Dagger2 时,为什么感觉难于理解的一些原因。

  1. 对于 Java 注解内容不熟悉。
  2. 对于依赖注入手段不熟悉。
  3. 对于 Java 反射不熟悉。
  4. 对于 Dagger2 与其它开源库的使用方法的不同之处,没有一个感性的认知。
  5. 对于 Dagger2 中极个别的概念理解不够。
  6. 对于 Dagger2 的用途与意义心生迷惑。

其实以上几点,都可以归类到基础技能不扎实这个范畴内,但正如我所说的,学习 Dagger2 时开发者的水平是不一样的,所以困扰他们的原因就不一样。下面,我针对这些情况,一一给出自己的建议。

对于 Java 注解不熟悉

这一部分的开发者基础知识确实薄弱,那么怎么办呢?当然是学习了。就算不为 Dagger2,注解的知识内容也应该好好值得学习,虽然在平常开发中,我们自己编写注解的机会很少,但是我们运用第三方开源库的时候,应该会经常看见注解的身影,所以熟悉注解不是为了自己编写注解代码,而是为了开发过程中更加高效从容而已。

如果,对 Java 注解一无所知,我可以给大家一个感性的认知。

一般,我们评价某人会说,这是一个好人、坏人、男神、女神、大神、单身狗等等,这是我们人为贴得标签,这些标签有助于我们自己或者其他人去获取被评价的人的基本信息。

而在 Java 软件开发中,我们也可以给某些类,某些字段贴上作用类似的标签,这种标签的名字就叫做注解,只不过这种标签是给代码看的。

这里写图片描述

标签只对特定的人起作用,比如小张被人贴了一个小气鬼的标签,所以小红认为小张是一个小气鬼,但是小张本人不会因为这个标签而改变自己变得不是小张,也许本质上小张是个大方的人。

所以,注解本身也不会影响代码本身的运行,它只会针对特定的代码起到一定的用处,用来处理注解的代码被称作 APT(Annotation Processing Tool)。

更详细的内容请阅读这篇文章《秒懂,Java 注解 (Annotation)你可以这样学》

对依赖注入手段不熟悉

这一块而言,如果让很多人慌张的原因,我觉得可能是依赖注入这个词过于学术化了。而从小到大,10 多年的应试教育让绝大部分的同学对于这些枯燥无味的概念产生了恐惧与绝望。其实,没有那么夸张的,不要被这些东西吓倒。

因为,Java 学习的时候,我们一直写这样的代码。

class B{}


class A {
    B b;

    public A() {
        b = new B();
    }
}

 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

这样的代码,一点问题都没有,类 A 中有一个成员变量 b,b 的类型是类 B。所以,在软件开发中,可以称 A 依赖 B,B 是 A 的依赖,显然,A 可以依赖很多东西,B 也可以依赖很多东西。

通俗地讲,依赖这个概念也没有什么神奇的,只是描述了一种需求关系。

我们再来看一种情况,现在,业务需要,代码越来越复杂。

class B{}

class C{
    int d;
    public C (int value) {
        this.d = value;
    }
}


class A {
    B b;
    C c;

    public A() {
        b = new B();
        c = new C(3);
    }
}

 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20

现在,A 有了一个新的依赖 C。不过,由于业务的演进,C 这个类经常发生变化,最明显的变化就是它的构造方法经常变动。

class C{
    int d;
    String e;
    public C (String value) {
        this.e = value;
    }
}


class A {
    B b;
    C c;

    public A() {
        b = new B();
        //c = new C(3);
        c = new C("hello");
    }
}

 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20

C 变动的时候,由于 A 依赖于它,A 不得不修改自己的代码。但是,事情还没有完。C 还会变动,C 把 B 也带坏了节奏。

class B{
    int value;

    public B(int value) {
        this.value = value;
    }

}

class C{
    int d;
    String e;
    public C (int index,String value) {
        this.d = index;
        this.e = value;
    }
}


class A {
    B b;
    C c;

    public A() {
        b = new B(110);
//      b = new B();
        //c = new C(3);
//      c = new C("hello");
        c = new C(12,"hello");
    }
}

 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32

可以想像的是,只要 B 或者 C 变动一次,A 就可能需要修改自己的代码,用专业术语描绘就是A 与依赖模块太过于耦合,这个可是犯了软件设计的大罪,

我们再可以想像一下,A 是领导,B 和 C 是小兵,如果因为 B 和 C 自身的原因,导致领导 A 一次次地改变自己,那么以现在流行的话来说就是,“你良心不会痛吗?”。所以我们需要的就是进行一些变化来进行解耦,也就是解除这种耦合的关系。让 A 不再关心 B 和 C 的变化,而只要关心自身就好了。

class A {
    B b;
    C c;

    public A(B b, C c) {
        this.b = b;
        this.c = c;
    }

}  

 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

在上面代码中,A 不再直接创建 B 与 C,它把依赖的实例的权力移交到了外部,所以无论 B 和 C 怎么变化,都不再影响 A 了。这种实例化依赖的权力移交模式被称为控制反转(IoC),而这种通过将依赖从构造方法中传入的手段就是被传的神乎其乎的依赖注入(DI)。其实,本质上也没有什么神奇的地方,只是起了一个高大上的名字而已,好比东北的马丽,在国际化上的大舞台,宣称自己是来自神秘东方的 Marry 一样。

依赖注入有 3 种表现形式。
构造方法注入

class A {
    B b;
    C c;

    public A(B b, C c) {
        this.b = b;
        this.c = c;
    }

} 
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

Setter 注入

class A {
    B b;
    C c;


    public void setB(B b) {
        this.b = b;
    }


    public void setC(C c) {
        this.c = c;
    }

}

 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

接口注入

interface Setter {
    void setB(B b);
    void setC(C c);
}


class A implements Setter{
    B b;
    C c;

    public void setB(B b) {
        this.b = b;
    }


    public void setC(C c) {
        this.c = c;
    }

}

 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21

大家肯定会想,依赖注入的引进,使得需求方不需要实例化依赖,但总得有地方去实例化这些依赖啊。确实,依赖注入引进了第三方,你可以称它为 IoC 容器,也可以称它为注射器(injector),为了便于理解,我们之后都有注射器来指代吧,通过注射器可以将依赖以上面 3 种注入方式之一注入到需求方。

这里写图片描述

病人需要的是药水,所以病人是需求者,药水是病人的依赖,注射器把药水注射给病人。

更多细节,请阅读《轻松学,浅析依赖倒置(DIP)、控制反转(IOC)和依赖注入(DI)》

而 Dagger2 就是一个依赖注入框架,你也可以想像它是一位非常智能化的服务员,用来处理大量的顾客的各种订餐需求,然后针对不同的菜单提供给不同的顾客不同类型的餐具。

对于 Java 反射不熟悉

对于这一块不熟悉的同学同样是基础知识太薄弱,需要补强。

相对于正常流程开发,Java 反射是非常规化手段。如果正常流程开发是司机驾驶一辆汽车,那么反射的运用就是采用无人驾驶的手段。

Dagger2 中也应用了反射,不过开发者本身不需要运用反射,Dagger2 是自身框架通过反射处理注解。

学习反射内容可以阅读这篇文章《细说反射,Java 和 Android 开发者必须跨越的坎》

Dagger2 与其它开源库略有不同

开源软件的出现,大大造福了程序员,所以,大家都说不要重复创造轮子

但是,我个人一直认为,不重复创造轮子,不代表可以不去深入了解这些轮子。

我把 Android 开发中所应用到的开源库当作武装

武装与两部分构成,武器装备

那么,在 Android 中什么样的库可以当作是武器呢?什么样的库可以当作是装备呢?

大家想一下,武器什么用途?战斗进行中,用来杀敌的

装备呢?战斗开始时,就要穿上或者安装好的物件。
这里写图片描述

刀、枪、棍、棒是武器,盔甲是装备。
武器拿来就用,盔甲等却要在开始战斗前就装备上。

Java 软件代码是在虚拟机中运行的,所以在这里可以把 jvm 当作战场。

Piccso、Logger、sweet-alert-dialog 等等,这些开源库都是在程序运行过程中拿来就用的。

而 GreenDao、Butterknife、Dagger2 这些因为涉及到了反射处理,而反射处理相对于正常开发速度很慢,所以它们通常在编译时产生一些新的代码,然后才能在程序运行过程中使用,也就是说它们都把反射处理移动到编译器编译代码时的阶段,而程序运行时并不涉及到反射,这就是这些框架运用了反射技术,但是仍然高效的秘诀所在。

所以,Dagger2 会产生中间代码,不少同学应该会有迷惑,为什么引进了 Dagger2 时,要先编译一次代码,不然就会报错。现在,可以解释了,编译代码是为了生成中间代码,然后在中间代码的基础上按照正常的流程开发。

Dagger2 并非横空出世

都说要站在巨人的肩膀上,Dagger2 其实也算站在巨人的肩膀上。

Dagger2 是一款依赖注入的框架,但依赖注入的框架有 ,所以 Dagger2 也并不算是一款新鲜事物,大家觉得新奇不过是因为对于依赖注入框架本身了解过少罢了。

Dagger2 是在 Dagger 的基础上来的,Dagger 是由 Square 公司开发的,Dagger2 基于 Dagger 由 Google 公司开发并维护。
Square 是一家伟大的公司,Android 大神 JakeWoton 之前就在它任职,不久前才离职。而我们熟悉的 RxJava、Butterknife、Retrofit、OKHttp 等等都是 Square 提供的,外号 Square 全家桶。
这里写图片描述

当然,Google 公司更是一家伟大的公司,这个无需多言。

最后,有个重要的地方就是 Dagger2 是基于注解开发的,而 Dagger2 中所涉及到的注解其实是基于 javax.inject 上开发的,它出自 JSR330
这里写图片描述

JSR330 是规范,建议大家怎么做,而 Dagger2 则实现了这个规范。

因此,对于普通开发者而言,学习 Dagger2 其实就是学习相关的注解的意义与用途。

Dagger2 的引进

Dagger2 是适应于 Java 和 Android 开发的依赖注入框架,记住得是它不仅仅对 Android 开发有效。

Dagger2 官网地址是 https://google.github.io/dagger//

对于 Eclipse 开发而言,需要下载相应的 jar 包。

对于 AndroidStudio 开发而言,只需要在相应的 build.gradle 引入对应的依赖就好了。

如果你 AndroidStudio 的 gradle build tool 版本在 2.2 以上,直接在引进就好了

dependencies {
  compile 'com.google.dagger:dagger:2.4'
  annotationProcessor 'com.google.dagger:dagger-compiler:2.4'
}
 
 
  • 1
  • 2
  • 3
  • 4

如果你的 gradle build tool 版本在 2.2 以下,则需要引进 apt 插件。
首先需要在 Project 层级的 build.gradle 文件中引入依赖

buildscript {
    repositories {
      mavenCentral()
    }
    dependencies {
        // replace with the current version of the Android plugin
        classpath 'com.android.tools.build:gradle:2.1.0'
        // the latest version of the android-apt plugin
        classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8'
    }
}

 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

然后在 Module 层级的 build.gradle 引入相应的插件和依赖

apply plugin: 'com.android.application'
apply plugin: 'com.neenbedankt.android-apt'


dependencies {
     apt 'com.squareup.dagger:dagger-compiler:2.4'
     compile 'com.squareup.dagger:dagger:2.4'
     //java注解
     compile 'org.glassfish:javax.annotation:10.0-b28'
}

 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

Dagger2 的基本概念

前面讲到过 Dagger2 基于 JSR330 注解,在普通开发者视角中,就是这些注解构成了 Dagger2 的全部。

前面文章我提到过,注解如同标签,给一个人贴标签有助于自己去理解这个人,而给代码贴标签,有助于 APT 程序去处理相应的代码,Dagger2 有自己的注解,而这些注解也有特定的意义,它们大体上都是为了实现依赖注入。

我们说依赖注入有 3 种手段:
- 构造方法注入
- Setter 注入
- 接口注入

但是,如果不借助于框架的话,我们就必须自己编写相应的代码,这些代码充当了注射器的角色。

B b = new B(5);
C c = new C(110,"110");

A a = new A();
a.setB(b);
a.setC(c);

 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

A 将内部的依赖 B 和 C 的实例化的权力移交到了外部,通过外部的注入。

Dagger2 这类依赖注入框架的出现进一步解放了我们的双手,Dagger2 有一套自己的依赖注入机制,我们不再手动编写注射器,而只要按照规则配置好相应的代码就好了,Dagger2 会自动帮我们生成注射器,然后在适当的时候进行依赖注入。

什么意思呢?意思就是我们不需要调用 a.setB() 和 a.setC() 方法,只需对代码添加一些注解就好了。

class B{
    int value;
    @Inject
    public B(int value) {
        this.value = value;
    }

}

class C{
    int d;
    String e;
    @Inject
    public C (int index,String value) {
        this.d = index;
        this.e = value;
    }
}

class A {
    @Inject
    B b;
    @Inject
    C c;
}

 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26

看起来,不可思议,不是吗?@Inject 是一个注解,只要按照 Dagger2 的配置,就能颠覆我们之前的编码习惯。

但不管看起来怎么神奇,任何事都有一个本质。

因此这个本质就是,Dagger2 是一个依赖注入框架,依赖注入的目的就是为了给需求方在合适的时候注入依赖。

对 Dagger2 学习过程如果感到不适与难以理解,回过头来想想它的本质好了。

这里写图片描述
Dagger2 的使命就是为了给需求者注射依赖。

@Inject 注解就如同一个标签,或者说它是一个记号,它是给 Dagger2 看的。它运用的地方有两处。

  1. @Inject 给一个类的相应的属性做标记时,说明了它是一个依赖需求方,需要一些依赖。

  2. @Inject 给一个类的构造方法进行注解时,表明了它能提供依赖的能力。

就这样,通过 @Inject 注解符号,就很容易标记依赖和它的需求方。但是,单单一个 @Inject 是不能让 Dagger2 正常运行的。还需要另外一个注解配合。这个注解就是 @Component。

而 @Component 相当于联系纽带,将 @inject 标记的需求方和依赖绑定起来,并建立了联系,而 Dagger2 在编译代码时会依靠这种关系来进行对应的依赖注入。

@Inject 和 @Component

我们来编写代码,验证一下。

假设有这么一个场景:

一个宅男,他喜欢在家玩游戏,所以饿了的时候,他不想自己煮饭吃,也不愿意下楼去餐厅,他选择了外卖。

public class ZhaiNan {

    @Inject
    Baozi baozi;

    @Inject
    Noodle noodle;

    @Inject
    public ZhaiNan() {

    }

    public String eat() {
        StringBuilder sb = new StringBuilder();
        sb.append("我吃的是 ");
        if ( baozi != null ) {
            sb.append(baozi.toString());
        }

        if (noodle != null) {
            sb.append("  ");
            sb.append(noodle.toString());
        }
        return sb.toString();
    }
}

public class Baozi {

    @Inject
    public Baozi() {
    }

    @Override
    public String toString() {
        return "小笼包";
    }
}

public class Noodle {

    @Inject
    public Noodle() {
    }

    @Override
    public String toString() {
        return "面条";
    }
}

 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52

上面代码可以看到,@Inject 注解的身影,需求方是 ZhaiNan 这个类,而 Baozi 和 Noodle 是它的依赖。前面说过,光有 @Inject 的话还不行,需要 @Component 配合。

@Component 怎么使用呢?

很简单,它只需要注解在一个接口上就好了。

@Component()
public interface Platform {
    ZhaiNan waimai();
}
 
 
  • 1
  • 2
  • 3
  • 4

Platform 是一个接口,它代表着外卖平台,它内部有一个 waimai() 的方法,返回 ZhaiNan 的类型。

这个接口特别的地方就是它的方法中的返回类型。如果一个方法返回了一个类型,那么其实也算是一种依赖的提供,我们可以在后续的代码中感受。

既然是接口,那么它就需要实现类,但是 Dagger2 会自动帮我们生成一个实现类,前提是使用这个类的时候,要先对工程进行编译。前面用装备解释过 Dagger2 这种类型的库,它会在编译阶段产生中间代码,这些中间代码就包括自动实现了被 @Component 注解过的接口实现类。

所以,我们如果要使用 Dagger2 为了我们自动生成的类时,我们就应该先 Build->Make Project 编译一次代码。生成的代码位置在 app 模块 build 文件夹中,在 AndroidStudio 切换 Project 视角就可以看到。
这里写图片描述

这个目录下都是 Dagger2 产生的中间产物,DaggerPlatform 就是 Dagger2 为我们自动实现的 Platform 这个接口的实现类,注意它的名字都是 Dagger+接口名称

有了 DaggerPlatform,我们就能够使用 Dagger2 进行代码的依赖注入了。

public class MainActivity extends AppCompatActivity {

    Button mButton;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        mButton = (Button) findViewById(R.id.btn_test);

        final ZhaiNan zainan = DaggerPlatform.builder()
                .build()
                .waimai();

        mButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Toast.makeText(MainActivity.this,zainan.eat(),Toast.LENGTH_LONG).show();
            }
        });

    }
}

 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23

然后,测试效果是:
这里写图片描述

需要注意的地方是,Component 的实现类是由 Dagger2 自动生成的,它的名字前面说了是 Dagger+接口名称。但这是通常情况,因为 @Component 注解的都是顶级类。但还有一种情况是。

class Foo {
  static class Bar {
    @Component
    interface BazComponent {}
  }
}

 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

它只是一个内部类的接口,Dagger2 针对这种情况需要把外部的类的名字加下划线的形式拼接起来,所以上例中 Dagger2 生成的 Component 实现类类名是 DaggerFoo_Bar_BazComponent。

我们并没有在任何地方用 new 关键字亲自创建 ZhaiNan 这个类的实例,但是它确实有效,而且它的内部依赖 Baozi 和 Noodle 都被实例化了,也就是说依赖被正确地注入到了 ZhaiNan 的实例对象当中。

所以,@Component 和 @Inject 的配合就能够使用 Dagger2 了,但这里面存在一个局限,@Inject 只能标记在我们自己编写的类的构造方法中,如果我们使用第三方的库或者标准库的话,是不是代表我们对于这些就无能为力了呢?

答案显然是否定的,Dagger2 作为一款优秀的框架必须考虑到开发过程中的方方面面,不然谈何优秀呢?

Dagger2 为了能够对第三方库中的类进行依赖注入,提供了 @Provides 和 @Module 两个注解。

@Provides 和 @Module

Provide 本身的字面意思就是提供,显然在 Dagger2 中它的作用就是提供依赖。
Module 是模块的意思,Dagger2 中规定,用 @Provides 注解的依赖必须存在一个用 @Module 注解的类中。

@Module
public class ShangjiaAModule {
    @Provides
     public Baozi provideBaozi() {
        return new Baozi("豆沙包");
    }
    @Provides
     public Noodle provideNoodle() {
        return new Noodle();
    }
}

public class Baozi {

    String name;

    @Inject
    public Baozi() {
        name = "小笼包";
    }

    public Baozi(String name) {
        this.name = name;
    }

    @Override
    public String toString() {
        return name;
    }
}

 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31

值得注意的地方有
- @Provides 修饰的方法一般用 provide 作用方法名前缀。
- @Module 修饰的类一般用 Module 作为后缀。

前面有讲过,@Component 是依赖双方的联系纽带,现在多了一个 @Module 注解,怎么配合使用呢?方法,很简单。只要在 @component 注解后面的括号中取值就是。

@Component(modules = ShangjiaAModule.class)
public interface WaimaiPingTai {
    ZhaiNan waimai();
}

 
 
  • 1
  • 2
  • 3
  • 4
  • 5

然后编写测试代码

mBtnTestModule = (Button) findViewById(R.id.btn_test_module);

final ZhaiNan zainan1 = DaggerWaimaiPingTai.builder()
        .build()
        .waimai();

mBtnTestModule.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        Toast.makeText(MainActivity.this,zainan1.eat(),Toast.LENGTH_LONG).show();
    }
});

 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

演示效果如下:
这里写图片描述

我们再看看 @Provides 用法。

@Module
public class ShangjiaAModule {
    @Provides
     public Baozi provideBaozi() {
        return new Baozi("豆沙包");
    }
    @Provides
     public Noodle provideNoodle() {
        return new Noodle();
    }
}

 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

@Provides 注解的方法中直接用 new 创建了依赖,其实还有另外一种方式。我们先对 Noodle 进行重构,让它作为面条的基类,然后编写它一个继承类。

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

public class Tongyi extends Noodle{

    @Inject
    public Tongyi() {
    }

    @Override
    public String toString() {
        return "统一方便面";
    }
}

 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18

ZhanNan 这个类不用改变,然后,用另外一种方式编写 @Provides 注解的方法。

@Module
public class ShangjiaAModule {
    @Provides
     public Baozi provideBaozi() {
        return new Baozi("豆沙包");
    }
    @Provides
     public Noodle provideNoodle(Tongyi noodle) {
        return noodle;
    }
}

 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

测试代码也不需要更改,演示效果如下:
这里写图片描述
那么,两种方式有什么区别呢?

@Provides
public Noodle provideNoodle(Tongyi noodle) {
    return noodle;
}

@Provides
public Noodle provideNoodle(Tongyi noodle) {
    return noodle;
}

 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

什么时候用 new 关键字?什么时候直接返回传入进来的参数?
我们不妨再创建一个类 Kangshifu,同样继承自 Noodle 这个基类。

public class Kangshifu extends Noodle{

    public Kangshifu() {
    }

    @Override
    public String toString() {
        return "康师傅方便面";
    }
}

 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

与 Tongyi 这个类不同的地方是,它并没有用 @Inject 注解构造方法。
我们再尝试更改 @Provides 注解的相应方法。

@Module
public class ShangjiaAModule {
    @Provides
     public Baozi provideBaozi() {
        return new Baozi("豆沙包");
    }
    @Provides
     public Noodle provideNoodle(Kangshifu noodle) {
        return noodle;
    }
//    @Provides
//     public Noodle provideNoodle(Tongyi noodle) {
//        return noodle;
//    }
}

 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

再进行编译的时候,会发现 IDE 报错了。

Error:(10, 13) 错误: com.frank.dagger2demo.Kangshifu cannot be provided without an @Inject constructor or from an @Provides-annotated method.
com.frank.dagger2demo.Kangshifu is injected at
com.frank.dagger2demo.ShangjiaAModule.provideNoodle(noodle)
com.frank.dagger2demo.Noodle is injected at
com.frank.dagger2demo.ZhaiNan.noodle
com.frank.dagger2demo.ZhaiNan is provided at
com.frank.dagger2demo.WaimaiPingTai.waimai()

 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

Log 提示的错误信息是 Kangshifu 这个类代码中没有被 @Inject 注解过的构造方法,也没有办法从一个被 @Provides 注解过的方法中获取。

所以,什么时候用 new 创建对象,什么时候可以直接返回传入的参数就很明显了。对于被 @Inject 注解过构造方法或者在一个 Module 中的被 @Provides 注解的方法提供了依赖时,就可以直接返回传入的参数,而第三方的库或者 SDK 自带的类就必须手动创建了。

@Module
public class ShangjiaAModule {
    @Provides
     public Baozi provideBaozi() {
        return new Baozi("豆沙包");
    }


    @Provides
     public Kangshifu provideKangshifu() {
        return new Kangshifu();
    }
//    @Provides
//     public Noodle provideNoodle(Tongyi noodle) {
//        return noodle;
//    }
}

 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18

这段代码,是可以正常运行的。
这里写图片描述

现在,我有一个新的需求更改。我在 ZhaiNan 类中 eat() 方法中要把餐厅名字打印出来。

public class ZhaiNan {

    @Inject
    Baozi baozi;

    @Inject
    Noodle noodle;

    @Inject
    public ZhaiNan() {

    }

    @Inject
    String resturant;

    public String eat() {
        StringBuilder sb = new StringBuilder();
        sb.append("我从 ");
        sb.append(resturant.toString());
        sb.append("订的外卖,");
        sb.append("我吃的是 ");
        if ( baozi != null ) {
            sb.append(baozi.toString());
        }

        if (noodle != null) {
            sb.append("  ");
            sb.append(noodle.toString());
        }
        return sb.toString();
    }
}

 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34

注意的是,我要新增了一个 String 类型的字段 resturant 来代表餐厅,并且用 @Inject 注解它。
于是,相应的 Module 也要更改。

@Module
public class ShangjiaAModule {
    @Provides
     public Baozi provideBaozi() {
        return new Baozi("豆沙包");
    }
    @Provides
     public Noodle provideNoodle(Kangshifu noodle) {
        return noodle;
    }

    @Provides
     public Kangshifu provideKangshifu() {
        return new Kangshifu();
    }

    @Provides
     public String provideResturant() {
        return "王小二包子店";
    }
//    @Provides
//     public Noodle provideNoodle(Tongyi noodle) {
//        return noodle;
//    }
}

 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26

然后,再编译测试。
这次的编译报错了。

Error:(10, 13) 错误: java.lang.String cannot be provided without an @Inject constructor or from an @Provides- or @Produces-annotated method.
java.lang.String is injected at
com.frank.dagger2demo.ZhaiNan.resturant
com.frank.dagger2demo.ZhaiNan is provided at
com.frank.dagger2demo.Platform.waimai()

 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

报错的原因是之前编写的接口 Platform 没有办法获取 ZhanNan 中 resturant 的依赖。因为之前并没有为 Platform 指定 Module。
那么现在我们就为它指定 ShangjiaAModule 吧。

编译后,进行测试,程序正常运行。
这里写图片描述

现在,我把代码再重构。在 ShangjiaAModule 中提供餐厅名字的时候,直接返回了“王小二包子店”,这个过于直接,缺少变动,现在针对这个进行变化。

@Module
public class ShangjiaAModule {

    String restaurant;

    public ShangjiaAModule(String restaurant) {
        this.restaurant = restaurant;
    }

    ......

    @Provides
     public String provideResturant() {
        return restaurant;
    }

}

 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18

restaurant 在 ShangjiaAModule 创建的时候被赋值,但我们之前的代码好像并没有处理 ShangjiaAModule 的创建,那么它如何创建呢?
我们先尝试重新编译代码并运行。

编译没有出错,但运行的时候出错了。

Caused by: java.lang.IllegalStateException: com.frank.dagger2demo.ShangjiaAModule must be set

at com.frank.dagger2demo.DaggerPlatform$Builder.build(DaggerPlatform.java:64)
at com.frank.dagger2demo.MainActivity.onCreate(MainActivity.java:21)

 
 
  • 1
  • 2
  • 3
  • 4
  • 5

Log 提示的是调用 DaggerPlatform中的Builder.build() 方法时出错了,因为 ShangjiaAModule 并没有出错。我们定位代码。

final ZhaiNan zainan = DaggerPlatform.builder()
                .build()
                .waimai();

 
 
  • 1
  • 2
  • 3
  • 4

我之前没有讲的是,如果一个 Module 没有实现任何构造方法,那么在 Component 中 Dagger2 会自动创建,如果这个 Module 实现了有参的构造方法,那么它需要在 Component 构建的时候手动传递进去。怎么传呢?Component 中生成的 Builder 构造器有与 Module 名字相同的方法,并且参数类型就是 Module 类型。大家细细体会下面代码就明白了。

final ZhaiNan zainan = DaggerPlatform.builder()
                .shangjiaAModule(new ShangjiaAModule("王小二包子店"))
                .build()
                .waimai();


final ZhaiNan zainan1 = DaggerWaimaiPingTai.builder()
                .shangjiaAModule(new ShangjiaAModule("衡阳鱼粉店"))
                .build()
                .waimai();

 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

再编译运行。
这里写图片描述

另外,还有一种特殊情况就是,像在 Android 中,MainActivity 这样的代码是我们自己编写的,所以我们可以给相应的属性添加 @Inject 注解,但是 MainActivity 对象的创建却是由 Android Framework 框架决定的,那么,Dagger2 有没有针对这种内部拥有 @Inject 标注的属性,但还没有进行依赖绑定的类的对象进行依赖注入呢?答案是肯定的。

我们知道,Component 是一个接口,它里面可以定义很多方法。方法的返回值可以提供一种类型的对象,前提是这个类的对象被 @Inject 注解过构造方法或者在 Module 中被 @Provides 注解过的方法提供。

@Component(modules = ShangjiaAModule.class)
public interface WaimaiPingTai {
    ZhaiNan waimai();
}

 
 
  • 1
  • 2
  • 3
  • 4
  • 5

ZhaiNan 能够在一个 Component 中的方法中作为类型返回是因为它符合我上面说的条件,它的构造方法被 @Inject 注解过。

需要注意的是,Component 中方法除了可以返回类型,还可以在方法中传入类型参数。目的是针对这个参数对象进行依赖注入。
比如

@Component(modules = ShangjiaAModule.class)
public interface WaimaiPingTai {
    ZhaiNan waimai();

    void zhuru(ZhaiNan zhaiNan);
}

 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

我新增了一个方法,zhuru() 中的参数就是 ZhaiNan 类型,代表 DaggerWaimaiPingTai 调用这个方法时能够对一个 ZhaiNan 对象进行依赖注入。

可以编写代码验证。

mBtnTestZhuru = (Button) findViewById(R.id.btn_test_zhuru);
final ZhaiNan zhaiNan = new ZhaiNan();
WaimaiPingTai daggerWaimaiPingTai = DaggerWaimaiPingTai.builder()
        .shangjiaAModule(new ShangjiaAModule("常德津市牛肉粉"))
        .build();
// 通过调用接口中的方法给 zhaiNan 进行依赖注入
daggerWaimaiPingTai.zhuru(zhaiNan);

mBtnTestZhuru.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        Toast.makeText(MainActivity.this,zhaiNan.eat(),Toast.LENGTH_LONG).show();
    }
});

 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

运行结果如下:
这里写图片描述

所以,我们可以给接口方法参数传值的形式来给 Activity 进行依赖注入。

@Module
public class ActivityModule {

    @Provides
    public int provideActivityTest(){
        return 1234567890;
    }
}


@Component(modules = {ShangjiaAModule.class,ActivityModule.class})
public interface WaimaiPingTai {
    ZhaiNan waimai();

    void zhuru(ZhaiNan zhaiNan);

    void inject(MainActivity mainActivity);
}  


 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20

我们编写了新的 Module,然后把它放时 WaimaiPingTai 这个 Component 中去,再添加了 inject() 方法,为的是能够给 MainActivity 实例进行依赖注入。

现在我们添加测试代码,首先在 MainActivity 中添加一个 int 类型的成员变量。

@Inject
int testvalue;

 
 
  • 1
  • 2
  • 3

然后要调用相关注入方法

mBtnTestActivity = (Button) findViewById(R.id.btn_test_inject_act);
final ZhaiNan zhaiNan = new ZhaiNan();
WaimaiPingTai daggerWaimaiPingTai = DaggerWaimaiPingTai.builder()
        .shangjiaAModule(new ShangjiaAModule("常德津市牛肉粉"))
        .build();

daggerWaimaiPingTai.inject(this);

mBtnTestActivity.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        Toast.makeText(MainActivity.this,"testvalue is "+ testvalue,Toast.LENGTH_LONG).show();
    }
});

 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

运行结果:
这里写图片描述

Component 的创建方式

我们可以看到,创建 Component 都是通过它的 Builder 这个类来进行构建的。其实还有另外一种方式。那就是直接调用 Component 实现类的 create() 方法。

public class Test {}

@Component(modules = TestCreate.class)
public interface TestCreateComponent {
    Test ceshi();
}

TestCreateComponent testCreateComponent = DaggerTestCreateComponent.create();
Test test = testCreateComponent.ceshi();

 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

上面代码中创建 TestCreateComponent 并没有借助于 Builder,而是直接调用了 DaggerTestCreateComponent 的 create() 方法,但是它有一个前提,这个前提就是 Component 中的 module 中被 @Provides 注解的方法都必须是静态方法,也就是它们必须都被 static 修饰。

(经查代码,目前的版本貌似create()调用的就是Builder.build(),不需要要求static修饰)

@Module
public class TestCreate {

    @Provides
    public static int provideTest1() {
        return 1;
    }

    @Provides
    public static String provideTest2() {
        return "test component create()";
    }

    @Provides
    public static Test provideTest(){
        return new Test();
    }
}

 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19

因为不需要创建 Module 对象实例,所以 Builder 自然就可以省去了。

@Inject 和 @Provides 的优先级

可能有心思细腻的同学会问,同样是提供依赖,如果一个类被 @Inject 注解了构造方法,又在某个 Module 中的 @Provides 注解的方法中提供了依赖,那么最终 Dagger2 采用的是哪一个?

public class Baozi {

    String name;

    @Inject
    public Baozi() {
        name = "小笼包";
    }

    public Baozi(String name) {
        this.name = name;
    }

    @Override
    public String toString() {
        return name;
    }
}

@Module
public class ShangjiaAModule {

    String restaurant;

    public ShangjiaAModule(String restaurant) {
        this.restaurant = restaurant;
    }

    @Provides
     public Baozi provideBaozi() {
        return new Baozi("豆沙包");
    }

}

 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35

Baozi 这个类就符合我上面给的情景,一方面它确实拥有被 @Inject 注解过的构造方法,另一方面在 Module 中它又通过 @Provides 提供了依赖。那么,最终,Dagger2 采取了哪一种呢?

答案是 Module,其实现象我们在之前的测试时已经可以观察到了,最终屏幕显示的是豆沙包选项。

Dagger2 依赖查找的顺序是先查找 Module 内所有的 @Provides 提供的依赖,如果查找不到再去查找 @Inject 提供的依赖。

到这里,我们讲解了 Dagger2 中最常见的 4 个注解:@Inject、@Component、@Module、@Provides。

正常情况下,这 4 个注解能够很好的完成一般的代码开发了。但是,这都是基础功能,Dagger2 提供了更多的一些特性。

Dagger2 中的单例 @Singleton

我们在平常开发中经常要涉及到各种单例。比如在 Android 中开发,数据库访问最好要设计一个单例,网络访问控制最好设计一个单例。我们经常编写这样的代码。

public class DBManager {

    private static DBManager instance;

    private DBManager() {
    }

    public static DBManager getInstance() {
        if ( instance == null ) {
            synchronized ( DBManager.class ) {
                if ( instance == null ) {
                    instance = new DBManager();
                }
            }
        }

        return instance;
    }
}

 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20

这种代码手段千遍一律,而 Dagger2 提供了另外一种可能。那就是利用 @Singleton 注解解决它。@Singleton 怎么使用呢?我们用代码来说明。

@Singleton
public class TestSingleton {

    @Inject
    public TestSingleton() {
    }
}

@Singleton
@Component
public interface ActivityComponent {
    void inject(SecondActivity activity);
}

 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

用 @Singleton 标注在目标单例上,然后用 @Singleton 标注在 Component 对象上。

编写测试代码

public class SecondActivity extends AppCompatActivity {
    @Inject
    TestSingleton testSingleton1;
    @Inject
    TestSingleton testSingleton2;

    Button mBtnTestSingleton;

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

        mBtnTestSingleton = (Button) findViewById(R.id.btn_test_singleton);

        DaggerActivityComponent.builder()
                .build()
                .inject(this);

        mBtnTestSingleton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Toast.makeText(SecondActivity.this,"test1 hashcode:"+testSingleton1.toString()
                    +" test2 hashcode:"+testSingleton2.toString(),Toast.LENGTH_LONG).show();
            }
        });

    }
}

 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30

编译后,测试结果往下:
这里写图片描述

可以看到,两个对象的 hashcode 是一样的,说明 TestSingleton 这个类实现了单例。
另外,如果要以 @Provides 方式提供单例的话,需要用 @Singleton 注解依赖提供的方法。如

@Module
public class SecondActivityModule {

    @Provides
    @Singleton
    public TestSingleton provideTestSingleton(){
        return new TestSingleton();
    }
}

 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

@Singleton 引出 @Scope

我们在上一节的内容可以看到,通过 @Singleton 注解就可以实现一个单例了。本节的目标就是深入分析一下 @Singleton。

@Scope
@Documented
@Retention(RUNTIME)
public @interface Singleton {}

 
 
  • 1
  • 2
  • 3
  • 4
  • 5

@Singleton 是一个注解,但是它被一个元注解 @Scope 注解了,所以,可以猜测到的是 @Scope 是真正厉害的角色。而实际上 @Singleton 只是 @Scope 一个默认的实现而已,但是因为它更具可读性,能够让开发者一眼就明白它的作用是为了单例。但是,单例也是有范围限制的。

分析 @Singleton 其实就等同于分析 @Scope 。Scope 的字面意思是作用域,也就是表达一种能力的范围。那么在 Dagger2 中它表达了一种什么样的能力范围呢?

大家有没有想过,为什么要用 @Singleton 同时标注 @Provides 和 @Component ?

文章一开始就讲过,Component 是联系需求与依赖的纽带,所以用 @Singleton 确定的单例作用域应该也是在 Component 的范围内。也就是说 @Scope 的作用范围其实就是单例能力范围,这个范围在单个的 Component 中。

在上面的代码中,MainActivity 和 SecondActivity 运用了不同的 Component 现在我们可以测试一下它们所获取的 TestSingleton 会不会是同一个对象。

public class MainActivity extends AppCompatActivity {

    @Inject
    public  TestSingleton testSingleton;

    onCreate() {

        mBtnJumpToSecond.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Intent intent = new Intent(MainActivity.this,SecondActivity.class);
                startActivity(intent);

                Toast.makeText(MainActivity.this,"testsingleton is "+ testSingleton,Toast.LENGTH_LONG).show();
            }
        });
    }

}

@Module
public class ActivityModule {

    @Provides
    public int provideActivityTest(){
        return 1234567890;
    }

    @Provides
    @Singleton
    public TestSingleton provideSingleton(){
        return new TestSingleton();
    }
}

@Singleton
@Component(modules = {ShangjiaAModule.class,ActivityModule.class})
public interface WaimaiPingTai {
    ZhaiNan waimai();

    void zhuru(ZhaiNan zhaiNan);

    void inject(MainActivity mainActivity);
}

 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45

我们改写了 MainActivity 和它涉及到的 Module、Component。
现在,我们检测 MainActivity 和 SecondActivity 中的 TestSingleton 是不是同一个。
这里写图片描述
可以发现,它们并不是同一个。也就是说 @Singleton 所拥有的单例能力是以 Component 为范围的限定的。

@Singleton 起作用是因为它被 @Scope 注解,所以,如果可能,我们也可以自己定义 Scope。

@Scope
@Documented
@Retention(RUNTIME)
public @interface PageScope {}

 
 
  • 1
  • 2
  • 3
  • 4
  • 5

我自己定义一个 @PageScope 注解,我的想法是一个 Activity 有不同的 Fragment,所以以 @PageScope 标注的依赖对象在这些 Fragment 之间是同一个对象,也就是说在这个 Activity 中实现了单例。而在另外一个 Activity 中因为采取了不同的 Component 对象,所以它们的 Fragment 也共用了同一个依赖对象,但是两个 Activity 中各自的依赖确不是同一个对象。

大家细细体会。

@Qualifiers 和 @Name

Qualifiers 是修饰符的意思,那么它修饰的是什么呢?不知道大家有没有察觉到,前面的演示代码其实很简单,经不起太多推敲。

在一个 Module 中 @Provides 提供的依赖是由返回值决定的。这样就会出现问题,同一种类型不同实例,怎么去区别?比如

public class SecondActivity extends AppCompatActivity {


    @Inject
    String phone;

    @Inject
    String computer;

}
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

phone 和 computer 应该要对应不同的字符串。但是,我们该如何在 Module 中进行编码呢?

@Module
public class SecondActivityModule {

    @Provides
    @Singleton
    public TestSingleton provideTestSingleton(){
        return new TestSingleton();
    }

    @Provides
    public String providePhone() {
        return "手机";
    }

    @Provides
    public String providePhone() {
        return "电脑";
    }

}

 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21

大家可能会想到这样编码,但是这样的代码根本编译不过。因为 Dagger2 是根据返回的类型来进行依赖关系确定的。如果存在两个方法返回一样的类型,那么正常情况下 Dagger2 显然就没有办法处理了。

不过,Dagger2 给出了解决方案。用 @Name 注解就好了,配合 @Inject 和 @Provides 一起使用。例如

 @Inject
@Named("phone")
String phone;

@Inject
@Named("computer")
String computer;

@Module
public class SecondActivityModule {

    @Provides
    @Singleton
    public TestSingleton provideTestSingleton(){
        return new TestSingleton();
    }

    @Provides
    @Named("phone")
    public String providePhone() {
        return "手机";
    }

    @Provides
    @Named("computer")
    public String provideComputer() {
        return "电脑";
    }

}

 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31

当然,如果你嫌每次给 @Name 麻烦,你可以自定义注解。

@Qualifier
@Documented
@Retention(RUNTIME)
public @interface Named {

    /** The name. */
    String value() default "";
}

 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

@Name 只是被 @Qualifier 注解的一个注解。所以,它能够有效完全是因为 @Qualifier。

@Qualifier
@Documented
@Retention(RUNTIME)
public @interface Phone {
}


@Qualifier
@Documented
@Retention(RUNTIME)
public @interface Computer {
}

 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

通过 @Qualifier 建立了 @Phone 和 @Computer 注解。

@Inject
@Phone
String phone;

@Inject
@Computer
String computer;

@Module
public class SecondActivityModule {

    @Provides
    @Singleton
    public TestSingleton provideTestSingleton(){
        return new TestSingleton();
    }

    @Provides
    @Phone
    public String providePhone() {
        return "手机";
    }

    @Provides
    @Computer
    public String provideComputer() {
        return "电脑";
    }

}

 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31

这样的效果是一样的。但是好处在于 @Name() 中要传入字符串,一不小心就容易将单词拼错,容易出错。

Dagger2 中的延迟加载

有些时候,我们希望依赖只有在我们使用的时候再去实例化,这样的机制叫做延迟加载。
比如

public class TestLazy {

    String name;

    public String getName() {
        if ( name == null ) {
            name = "TestLazy";
        }

        return name;
    }
}

 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

只有调用 TestLazy 实例的 getName() 方法时,name 才会被初始化。

Dagger2 提供了延迟加载能力。只需要通过 Lazy 就好了,Lazy 是泛型类,接受任何类型的参数。

public class TestLazy {

    @Inject
    @Named("TestLazy")
    Lazy<String> name;

    public String getName() {
        return name.get();
    }

}

@Module
public class SecondActivityModule {

    @Provides
    @Singleton
    public TestSingleton provideTestSingleton(){
        return new TestSingleton();
    }

    @Provides
    @Phone
    public String providePhone() {
        return "手机";
    }

    @Provides
    @Computer
    public String provideComputer() {
        return "电脑";
    }

    @Provides
    @Named("TestLazy")
    public String provideTestLazy() {
        return "TestLazy";
    }

}

 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41

这样,只有第一次调用 TestLazy 的 getName() 方法时,name 都会被注入。

Provider 强制重新加载

应用 @Singleton 的时候,我们希望每次都是获取同一个对象,但有的时候,我们希望每次都创建一个新的实例,这种情况显然与 @Singleton 完全相反。Dagger2 通过 Provider 就可以实现。它的使用方法和 Lazy 很类似。


public class TestProvider {
    @Inject
    Provider<Integer> randomValue;

    public int getRandomValue () {
        return randomValue.get().intValue();
    }
}


@Module
public class SecondActivityModule {

    ......

    @Provides
    public int provideRandomValue(){
        return (int) Math.random();
    }

}

 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23

但是,需要注意的是 Provider 所表达的重新加载是说每次重新执行 Module 相应的 @Provides 方法,如果这个方法本身每次返回同一个对象,那么每次调用 get() 的时候,对象也会是同一个。

Dagger2 中 Component 之间的依赖。

在程序开发中,可以存在多个 Component,而且 Component 之间还可以有依赖关系。比如

public class Guazi {}

public class Huotuichang {}

@Module
public class XiaoChiModule {

    @Provides
    public Guazi provideGuazi() {
        return new Guazi();
    }

    @Provides
    public Huotuichang provideHuotuichang() {
        return new Huotuichang();
    }
}

@Component(modules = XiaoChiModule.class)
public interface XiaoChiComponent {
    Guazi provideGuazi();

    Huotuichang provideHuotuichang();
}


 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26

XiaoChiComponent 这个 Component 主要作用是提供瓜子和火腿肠这些依赖。

现在,我要新建立一个 Component 代表食物类,并且食物类包括小吃,因此,我们得想办法利用 XiaoChiCompoent 这个现成的 Component。

@Module
public class FoodModule {

    @Provides
    public Baozi provideBaozi() {
        return new Baozi();
    }

    @Provides
    public Noodle provideNoodle() {
        return new Kangshifu();
    }
}

@Component(modules = XiaoChiModule.class
        ,dependencies = XiaoChiComponent.class)
public interface FoodComponent {
    void inject(ThirdActivity activity);
}

 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20

只需要在 @Component 中的 dependencies 属性取值为相应的依赖就可以了。这样,FoodComponent 也提供了瓜子和火腿肠的依赖。我们再看如何使用?

public class ThirdActivity extends AppCompatActivity {

    Button mBtnTest;

    @Inject
    Guazi guazi;
    @Inject
    Huotuichang huotuichang;
    @Inject
    Baozi baozi;
    @Inject
    Noodle noodle;

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

        mBtnTest = (Button) findViewById(R.id.test_dependency);

        XiaoChiComponent xiaoChiComponent = DaggerXiaoChiComponent.builder()
                .build();

        DaggerFoodComponent.builder()
                .xiaoChiComponent(xiaoChiComponent)
                .build()
                .inject(this);

        mBtnTest.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Toast.makeText(ThirdActivity.this,
                        baozi+" "+
                        noodle+" "
                        +guazi+""+huotuichang,Toast.LENGTH_LONG).show();
            }
        });

    }
}

 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41

只需要在 DaggerFoodComponent 构建的时候,将所依赖的 Component 传递进去就是了,但前提是要先创建 XiaochiComponent ,然后创建 FoodComponent 的时候将它传递进去。如上面调用了 DaggerFoodComponent.Builder 的 xiaochiComponent() 方法。

继续查看结果。
这里写图片描述

Dagger2 中的 SubComponent

在 Java 软件开发中,我们经常面临的就是“组合”和“继承”的概念。它们都是为了扩展某个类的功能。
前面的 Component 的依赖采用 @Component(dependecies=othercomponent.class) 就相当于组合。
那么在 Dagger2 中,运用 @SubComponent 标记一个 Component 的行为相当于继承。

@Subcomponent(modules = FoodModule.class)
public interface SubComponent {
    void inject(ThirdActivity activity);
}

@Component(modules = XiaoChiModule.class)
public interface ParentComponent {
    SubComponent provideSubComponent();
}


DaggerParentComponent.builder().build()
                .provideSubComponent().inject(this);

 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

使用 Subcomponent 时,还是要先构造 ParentComponent 对象,然后通过它提供的 SubComponent 再去进行依赖注入。

大家可以细细观察下它与 depedency 方法的不同之处。

但是,SubComponent 同时具备了 ParentComponent 和自身的 @Scope 作用域。所以,这经常会造成混乱的地方。大家需要注意。

如果你要我比较,SubComponent 和 dependency 形式哪种更好时,我承认各有优点,但我自己倾向于 dependency,因为它更灵活。

不是说 组合优于继承嘛

到这里的时候,Dagger2 的基础知识都介绍的差不多了,它还有一些知识点,但是应用的场景太复杂,所以没有必要细究。有兴趣的同学可以到官网上自行研究。

Dagger2 在什么地方进入依赖注入?如何注入?

也许会有一部分同学,执着于细节。因为 Dagger2 帮我们进行了依赖注入,但这一切过程是透明的,我们并不知晓。有探索精神的同学总想去获取更多的细节,这种精神值得称赞。

我简单说一下,Dagger2 运用了 APT 插件,这种插件会在编译时根据 @Provide、@Inject、@Moudle、@Component 这些注解生成许多中间代码,但是不管它多么复杂它的目的也只是为了依赖注入。所以,肯定有一个地方进行了。


a.setB(b);

 
 
  • 1
  • 2
  • 3

这样的操作。

在依赖注入概念中,我分了需求者、注射者、依赖三个角色。
这里写图片描述
总是注射者给需求者注入了依赖。

前面说过 Component 是需求与依赖的联系,因此可以在 Component 的实现类代码中找出分别代表需求、注射者、依赖 3 个角色,然后找出依赖注入发生时的代码,这个问题就算解答完成了。

@Component(modules = SecondActivityModule.class)
public interface ActivityComponent {
    void inject(SecondActivity activity);
}

 
 
  • 1
  • 2
  • 3
  • 4
  • 5

我们以 ActivityComponent 为例解释说明,在这里显然 SecondActivity 的实例是需求者。

@Inject
TestSingleton testSingleton1;
@Inject
TestSingleton testSingleton2;

@Inject
@Phone
String phone;

@Inject
@Computer
String computer;

 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

SecondActivity 它需要 2 种依赖,TestSingleton、String。我们去查看最关键的类,也就是 Dagger2 帮助我们生成的 DaggerActivityComponent。

public final class DaggerActivityComponent implements ActivityComponent {
  private Provider<TestSingleton> provideTestSingletonProvider;

  private Provider<String> providePhoneProvider;

  private Provider<String> provideComputerProvider;

  private MembersInjector<SecondActivity> secondActivityMembersInjector;

  private DaggerActivityComponent(Builder builder) {
    assert builder != null;
    initialize(builder);
  }

}
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

我在前面讲解过 Provider 是什么用处,大家一看就懂。而 MembersInjector 以 Injector 为后缀,所以它肯定是一个注射器。我根本不需要去查看它的定义和相关源码。

好了,现在注射者(MembersInjector)找到了,依赖(Provider provideTestSingletonProvider 等等)找到了,需求者我们知道是 SecondActivity,所以我们只要把依赖注入发生的代码找出来,问题就解答完成。

@Override
public void inject(SecondActivity activity) {
    secondActivityMembersInjector.injectMembers(activity);
}

 
 
  • 1
  • 2
  • 3
  • 4
  • 5

inject() 方法显然是依赖注入发生的地方,但它内部调用了 secondActivityMembersInjector.injectMembers() 方法,我们跟踪进去。

public void injectMembers(SecondActivity instance) {
    if (instance == null) {
      throw new NullPointerException("Cannot inject members into a null reference");
    }
    instance.testSingleton1 = testSingleton1AndTestSingleton2Provider.get();
    instance.testSingleton2 = testSingleton1AndTestSingleton2Provider.get();
    instance.phone = phoneProvider.get();
    instance.computer = computerProvider.get();
}

 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

代码交待的一清二楚,SecondActivity 实例 instance 的 testSingleton1、testSingleton2、phone、computer 4 个依赖,全部在这里进行赋值,也就是在这里进行依赖注入。

不过,我们都知道依赖都是由 Module 提供的,回到 DaggerActivityComponent 的源码当中关注它的 initialize 方法。

private void initialize(final Builder builder) {

    this.provideTestSingletonProvider =
        DoubleCheck.provider(
            SecondActivityModule_ProvideTestSingletonFactory.create(builder.secondActivityModule));

    this.providePhoneProvider =
        SecondActivityModule_ProvidePhoneFactory.create(builder.secondActivityModule);

    this.provideComputerProvider =
        SecondActivityModule_ProvideComputerFactory.create(builder.secondActivityModule);

    this.secondActivityMembersInjector =
        SecondActivity_MembersInjector.create(
            provideTestSingletonProvider, providePhoneProvider, provideComputerProvider);
  }

 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

它们都是通过工厂方法创建的。大家应该都懂工厂方法都是用来创建对象的。我不嫌麻烦,挑出 SecondActivityModule_ProvidePhoneFactory 这个个例来进行讲解。它代表 Phone 对象的工厂。

public final class SecondActivityModule_ProvidePhoneFactory implements Factory<String> {
  private final SecondActivityModule module;

  public SecondActivityModule_ProvidePhoneFactory(SecondActivityModule module) {
    assert module != null;
    this.module = module;
  }

  @Override
  public String get() {
    return Preconditions.checkNotNull(
        module.providePhone(), "Cannot return null from a non-@Nullable @Provides method");
  }

  public static Factory<String> create(SecondActivityModule module) {
    return new SecondActivityModule_ProvidePhoneFactory(module);
  }
}


public interface Factory<T> extends Provider<T> {
}

 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23

调用它的 create 方法会返回一个 Factory 对象,但是 Fractory 是 Provider 的子类。
前面讲过调用 Provider 方法的 get() 方法能够得到实例,所以最终会调用

public String get() {
    return Preconditions.checkNotNull(
        module.providePhone(), "Cannot return null from a non-@Nullable @Provides method");
}

 
 
  • 1
  • 2
  • 3
  • 4
  • 5

所以最终会调用

@Module
public class SecondActivityModule {

    @Provides
    @Phone
    public String providePhone() {
        return "手机";
    }

}

 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

其它几个流程类似,那就不一一分析了。有兴趣的同学可以去查看源码,我只提示一个信息,你去观察 MembersInjector 怎么创建 @Singleton 注解的依赖时会发现它的实现步骤,跟我前面文章手动生成 DBManager 单例用的手段是很相似的。

对于 Dagger2 的用途与意义心生迷惑

我想,还是会有一大部分的同学看到这里的时候仍然不明白,Dagger2 的妙处。
其实在文章开始的地方我就讲了 Dagger2 的本质,它本质就是一款依赖注入框架,用来解耦的。

掌握上面的 Dagger2 基础知识已经足够让你进行此类代码编写了,另外也足够让你去看懂一些运用了 Dagger2 的优秀开源项目,比如 Google 提供的示例
todo-mvp-dagger

这个项目示例就是为了演示 Dagger2 与 MVP 架构的配合使用。由于文章篇幅所限,我不作过多的讲解,大家自行研究。有机会,我会专门写一篇文章来讲述 Dagger2 在一个完整项目工程中如何进行解耦的。

如果你对 Dagger2 兴趣更浓烈了

Dagger2 的知识内容稍多,所以如果你耐着性子学习完后,兴致依赖不减,我可以给你一些建议。

  1. 自己去阅读官网文档。因为那才是第一手资料,虽然它写的不是很好,但毕竟权威。
  2. 多去观察不同的博文,因为每个人思考方式不一样,所以观察问题的角度可能不一样。
  3. 自己多练,任何没有经过自己实践的行为在软件编程中都不可取。
  4. 阅读优秀的开源代码,并思考。
  5. 在思考的同时,纠正自己的理解,然后再实践,再思考,再总结。我总说主动学习要好过被动学习,任何没有经过自己思考和求索的学习都是被动学习,看文档、看博文、看代码那都是别人的知识,你需要的就是用自己把这些纳入自己的知识体系中,当你也能讲述给其他人听的时候,那时你就可以确定你掌握它了。

如果你仍然意识不到 Dagger2 的美好

这个其实也没有多大关系。不要迷恋武器。

也许你写的代码中类文件不是很多,模块之间的耦合并不是很强,或者是整个系统并不复杂,强行引进 Dagger2 只会让你感受复杂,多了很多类,多了很多编译的步骤,还增加了学习成本,你会觉得不划算。

我们总说优化代码,设计架构,其实对于很多开发人员而言,大量的产品需求就能让自己加好几个晚上的班,并且需求还经常变动,如果你这个时候跟他讲代码规范什么的,肯定不现实。现实的事情是,完成需求永远第一要务。

饱暖才能思淫欲。

我与其劝你去感受 Dagger2 的美好,还不如劝你细细去体会一下依赖注入的美好。

如果,你仍然觉得 Dagger2 麻烦,中肯地讲一句:那么索性放弃它算了,Dagger2 不重要,依赖注入才重要

等到哪一天,你真的有强烈的对于代码解耦需求,也许你会想起 Dagger2 这么一款框架,那时候回过来学习,我保证你的效果会非常明显,你的理解力也会较现在更加的深刻。

所以,我最终的目的仍然是希望你能够好好学习 Dagger2,希望大家不要误解我这一节的真实用意。

demo

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Dagger是网易杭州研究院QA团队开发的一个轻量级、运行稳定的WebUI自动化测试框架,主要基于Selenium及TestNg可以认为是对Selenium进行二次封装的一个框架(俗称 造轮子 )。之所以把这个轮子开源出来,主要在于经过了公司内部多个项目的实践,也取得了不错的成效,因此,希望开源以后可以对大家有所帮助及参考。 设计理念 Dagger首先是一个WebUI自动化框架,提供了赖以操纵浏览器的一些API。API数量不多,少于20个,但从实践上,已经基本涵盖95%的应用场景了(其余5%比较 个性 的自动化操作一般是封装在业务逻辑层面,有时候甚至会须要hack) Dagger其次是一个测试框架,使用TestNg管理和运行用例,TestNg相关断言内嵌于上述API中。因此,在我们的测试用例里面不应该看到单独的TestNg断言的 Dagger还是一种设计风格:简约。无论是Dagger框架本身还是基于Dagger编写的测试用例,都是十分light及straightforward的,以至于会让人感觉有点土。但实践中,这两者确保了低成本、易用性、可维护性 WebUI自动化从业界看,难推进,易烂尾,原因基本在于:维护成本高、运行速度慢、稳定性差 Dagger专注于WebUI自动化,从技术上克服了速度与稳定问题(见下文)。只封装够用的浏览器操作为API,并充分简化/强化这些API,以简约的风格去降低自动化的学习及使用成本。同时,在实践中,我们主要使用Dagger编写冒烟用例、其次是主干用例,少写逻辑复杂功能,不写边边角角功能,让用例也保持清爽(在整个自动化实施过程中,会定期进行用例Review),同样易于后期维护 主要特性 API极少,易于上手,详见这里. 提供比较完备的文档,便于快速入门,详见这里. 支持单机多浏览器并发执行,大大缩短用例执行时间,详见这里 通过修改TestNg源码实现失败用例自动重运行(详见这里)由此几乎消除WebUI自动化中常见的虚假失败 默认使用Chrome浏览器,原因详见这里 失败用例自动截屏 后续工作 加入Flex/Flash自动化支持 如何使用 Dagger十分适合中小型团队从零开始WebUI自动化,这样的话,只须要直接下载整个Dagger代码就行了,Dagger本身都已经配置好了,下载后看一下使用文档就可以直接开始写用例了 也可以把Dagger打成Jar包,导入已有的自动化框架中,详见这里 标签:Dagger  自动化测试

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值