Dagger2简介
Dagger2是由Google维护的开源依赖注入框架,是由Dagger发展而来,完全去除了反射机制;
Dagger是由Square公司开发并开源的,现已停止维护 (Deprecated)。
依赖注入 是什么?
- 依赖注入可以认为是一种设计原则,目的是降低代码间的耦合程度。
- 我们开发的应用程序都是由多个类合作来实现某个业务或功能,这就会发生例如:在Class A中用到了Class B对象b的情况。这时A依赖于B,B是A的依赖。A与B就这样发生了关系——依赖关系。
- 在A类外部创建好B类的实例后再传递给A,这个过程可以称为依赖注入。
举个简单的例子:
public class Engine {
public void start(){
System.out.println("Start the engine");
}
}
public class Car {
private Engine mEngine;
public Car() {
mEngine = new Engine();
}
public void start() {
System.out.println("Going to start the car.");
mEngine.start();
}
}
上面代码段中Car类依赖于Engine类,因为没有引擎汽车就没有了动力,这个例子很简单,没什么好说的。
关键在于这是没有使用依赖注入原则的写法,而是直接在Car的构造函数中显式new了一个Engine对象,并没有在Car类外部创建好Engine的实例再传递给Car类。这样Car类就必须要知道Engine是如何构造的,这就产生了耦合,同时造成代码无法在运行时更换不同的Engine。
那么遵从依赖注入原则该怎么写呢?
public class Car {
private Engine mEngine;
public Car(Engine engine) {
this.mEngine = engine;
}
public void start() {
System.out.println("Going to start the car.");
mEngine.start();
}
}
没错,上面的代码就遵从了依赖注入原则。现在不需要在Car类内部创建Engine的实例了,我们需要在创建Car的实例之前就要创建好Engine,然后把它“注入”到Car里面去。Car不需要再知道Engine是如何创建的了,这也就在一定程度上降低了耦合度。
那既然这样的话还要Dagger2有什么用? Wait, 实现依赖注入的思想很简单,但是想要灵活性、高可扩展性可没那么简单。试想一下,目前的Car还很简单,它还需要轮子、变速箱、传动系统等很多部件才能开动起来,比如我们现在增加一个变速箱:
public class GearBox {
public void shiftGears(int gear) {
System.out.println("Shift gears to " + gear);
}
}
public class Car {
private Engine mEngine;
private GearBox mGearBox;
public Car(Engine engine, GearBox gearBox) {
this.mEngine = engine;
this.mGearBox = gearBox;
}
public void start() {
System.out.println("Going to start the car.");
mEngine.start();
mGearBox.shiftGears(1);
}
}
output:
>> Going to start the car.
>> Start the engine
>> Shift gears to 1
看起来这也没什么嘛,不就是新增了GearBox变速箱类,然后修改了一下Car的构造函数而已嘛。
再想象一下,如果有多处使用了Car类,难道我们要一处一处的去修改吗?如果后来我们又要增加油门怎么办?这也不符合开闭原则,我们应该尽量少的修改既有代码。此时就该Dagger2大显身手了。
Dagger2 闪亮登场
文章开始就介绍了Dagger2相比于Dagger完全去除了反射机制。它为了效率最大化,是在编译时期通过apt自动生成依赖注入相关代码的。
Dagger2有两个基本概念,Module和Component。
Module:负责提供依赖的实例,例如提供Engine的实例;
Component:负责将Module提供的依赖的实例注入到目标类,它是接口,编译时自动生成实现类的代码。
我们再声明一个概念,就是对依赖有需求的类称为目标类,把依赖注入到目标类。例如上面的Car类。
我们先实现一个最简单的Dagger2依赖注入(没有使用Module),代码如下:
@Component
public interface CarComponent {
void inject(Car car);
}
public class Engine {
@Inject
public Engine() {
}
public void start() {
System.out.println("Start the engine");
}
}
public class GearBox {
@Inject
public GearBox() {
}
public void shiftGears(int gear) {
System.out.println("Shift gears to " + gear);
}
}
public class Car {
@Inject
Engine mEngine;
@Inject
GearBox mGearBox;
public Car() {
DaggerCarComponent.builder().build().inject(this);
}
public void start() {
System.out.println("Going to start the car.");
mEngine.start();
mGearBox.shiftGears(1);
}
}
output:
>> Going to start the car.
>> Start the engine
>> Shift gears to 1
看看我们都做了哪些改变:
- 创建了一个 CarComponent 接口,使用了 @Component 注解,有个 inject 方法,参数是目标类对象;
- 依赖类的构造方法都用了 @Inject 注解,用来让Dagger2知道可以提供依赖;
- Car目标类依赖的成员变量都用了 @Inject 注解(不能 private)声明需要哪些依赖;
- Car目标类构造方法不再创建依赖的实例,DaggerCarComponent 是 Dagger2 生成的 CarComponent 实现类,实现了我们上面声明的 inject 方法,调用此方法将目标类传给 CarComponent 实现类
注:编写 CarComponent 接口后,我们需要 bulid 一下工程(Make Project 或 Rebuild Project 都行),随后Dagger2 就会通过 apt 为我们生成实现类 DaggerCarComponent 的代码了。
Dagger2做了什么:
- 接收传入的目标类实例,根据成员变量注解 @Inject 知道目标类需要哪些依赖;
- 查找依赖中被 @Inject 注解的构造方法,初始化依赖实例,然后注入到接收的目标类实例中;
可以看到,上面并没有使用 Module 但可以直接提供依赖实例,是因为我们在依赖的构造方法上使用了 @Inject 注解。这就是第一种依赖注入的方式,也是最简单、有局限性的方式。因为如果我们没有办法在依赖的构造方法上使用 @Inject 注解,比如使用第三方库无法修改源码的情况,我们就无法使用这种方式。值得一提的是,如果依赖类还依赖于其他类,例如 Car->Engine->Gas, 不用担心,Dagger2 一样会帮我们完成全部依赖注入的工作。这里也能够看出来,目标类、依赖 是相对的概念,一个类既可以是另一个类的依赖,也可以是其他类的目标类。
我们再实现一个稍微复杂一点的例子(使用Module):
@Module
public class CarModule {
@Provides
public Engine providesEngine() {
return new Engine("Mercedes");
}
@Provides
public GearBox providesGearBox() {
return new GearBox();
}
}
@Component(modules = CarModule.class)
public interface CarComponent {
void inject(Car car);
}
public class Engine {
private String supplier;
public Engine(String supplier) {
this.supplier = supplier;
}
public void start() {
System.out.println("Start the engine supplied by " + supplier);
}
}
public class GearBox {
public void shiftGears(int gear) {
System.out.println("Shift gears to " + gear);
}
}
public class Car {
@Inject
Engine mEngine;
@Inject
GearBox mGearBox;
public Car() {
DaggerCarComponent.builder().carModule(new CarModule()).build().inject(this);
// DaggerCarComponent.builder().build().inject(this);
}
public void start() {
System.out.println("Going to start the car.");
mEngine.start();
mGearBox.shiftGears(1);
}
}
output:
>> Going to start the car.
>> Start the engine supplied by Mercedes
>> Shift gears to 1
再来看看我们这一次做了哪些改变:
1.新增了 CarModule 类:
- 使用 @Module 注解此类,表明此类是Module类,以此让Dagger2知道它能够提供依赖实例;
- 使用 @Provides 注解了两个方法,表明被注解的方法是用来提供依赖实例的,这里一个提供Engine,一个提供GearBox;
- 为什么增加Module类?是因为Engine和GearBox两个依赖的构造方法我们不再使用@Inject注解,这在使用第三方框架的时候大有所为。
2. CarComponent 接口的注解增加了属性:
- 在@Component注解后面增加了属性 (modules = CarModule.class),modules属性用来指明当前的Component类包含哪些Module类,可以理解为查找提供依赖方法的范围;
- Component 类需要依赖实例时,它会在modules属性指定的Module类中的有@Provides注解的方法中,依据返回值类型来选择使用哪个提供依赖实例的方法。
- inject 方法将目标类实例传入,Component类得到目标类需要哪些依赖,到Module类中获取相应的依赖实例,赋值给目标类的成员变量,完成依赖注入。
3. Engine和GearBox类的构造方法不再使用@Inject注解,Engine类的构造方法增加了供应商名称参数。目标类Car没有修改。
- Car类需要注意的是,我们的CarModule是有默认的无惨构造方法的,所以上面代码中Car的构造方法中创建并依赖注入的两种写法都是可以的。
Dagger2做了什么:
- 相对于第一个例子,本例中Dagger2完成的事情是相同的,即使用Dagger2完成了依赖注入。但是依赖注入的具体实现存在不同,本例中不再使用@Inject注解依赖的构造方法了,所有依赖都是由Module类来提供。
- 例子1与例子2最重要的区别就是依赖的来源不同了,这也就是提供依赖的两大来源:1.使用@Inject注解构造方法;2.使用Module类提供依赖实例。
- 依赖的来源虽然不同,但其实依赖注入的流程并没有不同。那么Dagger2是如何协调不同的依赖来源,查找依赖的流程是怎样的呢?这里引用 依赖注入神器:Dagger2详解系列 文章中的一段话:
我们有两种方式可以提供依赖,一个是注解了@Inject的构造方法,一个是在Module里提供的依赖,那么Dagger2是怎么选择依赖提供的呢,规则是这样的:
- 步骤1:查找Module中是否存在创建该类的方法;
- 步骤2:若存在创建类方法,查看该方法是否存在参数;
- 步骤2.1:若存在参数,则按从步骤1开始依次初始化每个参数;
- 步骤2.2:若不存在参数,则直接初始化该类实例,一次依赖注入到此结束。
- 步骤3:若不存在创建类方法,则查找@Inject注解的构造函数,看构造函数是否存在参数;
- 步骤3.1:若存在参数,则从步骤1开始依次初始化每个参数;
- 步骤3.2:若不存在参数,则直接初始化该类实例,一次依赖注入到此结束。
概括一下就是从注解了@Inject的对象开始,从Module和注解过的构造方法中获得实例,若在获取该实例的过程中需要其他类的实例,则继续获取被需要类的实例对象的依赖,同样是从Module和标注过的构造方法中获取,并不断递归这个过程直到所有被需要的类的实例创建完成,在这个过程中Module的优先级高于注解过的构造方法。明白了这个原理之后老奶奶都会用这几个标签了。
上面这位作者总结已经非常清晰了,Dagger2一直遵循这一依赖查找的规则。可以看出,这个规则覆盖了上面说的提供依赖的两大来源。
小结:
本例子,实际上就是一个Dagger2常用的依赖注入的一个基本实现,使用Dagger2进行依赖注入的基本逻辑就是这样,其他更复杂的用法都是实际项目中各种复杂情况下的解决方案,但依赖注入的基本逻辑不变!
用一张图来总结:(图片引自 依赖注入神器:Dagger2详解系列)