Dagger2-从放弃到再次入门

穆秋实 2017-11-06 17:31:19

背景

说放弃有些夸张,其实之前简单了解过Dagger2框架,但是并没有深入研究。这次准备从依赖注入概念入手,通过写一个简单的Demo,掌握Dagger2最基本的使用,同时阅读最基础的生成源码,分析其注入过程

简介

Dagger,匕首,Jake Wharton大神三把刀中最著名的一把,是一个Android依赖注入框架,目前已经交由谷歌维护,开发到2.0+版本。其最大的作用,是用于模块间解耦,提高代码的健壮性和可维护性。

控制反转

1、耦合

我们总说解耦,那首先就得先定义一下什么是耦合,不然连解的是什么都不知道怎么行,看下面的代码

class A {
    B b;
    public A() {
        this.b = new B();
    }
}

很简单,我在开发中也经常这么用,在A中通过new关键字创建B的实例对象,调用相关方法。这里的创建对象,其实 这里就存在着耦合,因为当B修改了构造方法,A也要跟着做修改,所以耦合用大白话说,就是模块间产生了联系。当然,这个联系不仅仅局限于使用了new关键字进行被调用方的实例化,也包括共用一个全局变量或者可以直接修其他模块内的数据等等,后续内容不涉及这类耦合,主要讨论数据耦合。

2、解耦

既然耦合是模块间产生联系,那解耦就必然是拆散这种联系,做到一个模块无论怎么修改都不会影响其他模块。

3、怎么做

还是以上面的代码为例,如果不想让B的构造方法修改影响到A,那可以采取传递和注入两种方式解决。

传递

修改一下刚才的代码,在构造A时,添加B的实例作为参数,很好理解。

class A {
    B b;
    public A(B b) {
        this.b = b;
    }
}
依赖注入(Dependency Injection,简称DI)

注入的过程可以参考下图(去掉Dagger的注解部分),分为三个角色:

  • 依赖提供方:准备好需求方要用的数据
  • 依赖需求方:数据的使用者,需要标识哪些数据需要注入
  • 依赖注入容器:将提供方准备好的数据交给需求方

需求方的数据不再是自己创建,而是使用前由依赖注入容器,将提供方准备好的数据交给需求方,具体的实现方式可以是运行时动态赋值,也可以由专门的数据模块统一管理等等,Dagger2就是注入框架,后面我们会看到它是如何做到的。

依赖注入主要流程

上面这两种解决办法的核心思想都是一个,把添加依赖的权利给外部,自己不去主动添加依赖关系,这种解决办法就叫做控制反转(Inversion of Control,简称IOC)。

Dagger2

Dagger2就是一个依赖注入器框架,使用预编译期间生成代码的方式来完成依赖,而不是通过反射,这样做的好处就是注入过程对性能没有损耗。生成代码的方式是使用注解搭配APT生成,常用注解有:@Inject、@Module 、@Component 、@Provides,按照上面提到的身份作为维度给这些注解做个分组,便于我们记忆和理解:

  • 依赖提供方:包括Module和Provides,被Module标记的类就是一个依赖提供方,在这个类中,被Provides标记方法的返回值,就是该类能提供的数据。
  • 依赖需求方:使用Inject标记需要被注入的数据
  • 依赖注入容器:被Component标记的类是容器类,负责将Provides的数据赋值给被Inject标记的对象。

这么说可能比较空洞,搭配一个基础使用的Demo来看就明白了。

先提供一个数据的实体类,简单的药物基类

public abstract class BaseMedicine {
    //药品名称
    public String name;
    //计量
    public int metering;
    public BaseMedicine(String name, int metering) {
        this.name = name;
        this.metering = metering;
    }
    public abstract void take();

    //省略set和get方法
}

和他的两个子类,一个退烧药和一个止痛药吧

public class Medicine1 extends BaseMedicine {
    public Medicine1(int metering) {
        super("退烧药", metering);
    }
    @Override
    public void take() {
        takeBeforeMeal();
    }
    private void takeBeforeMeal() {
        //doSomeThing
    }
}

public class Medicine2 extends BaseMedicine {
    public Medicine2(int metering) {
        super("止痛药", metering);
    }
    @Override
    public void take() {
        takeAfterMeal();
    }
    private void takeAfterMeal() {
        //doSomeThing
    }
}

有了数据,我们开始构建依赖注入的三个角色,首先是依赖提供方,刚才提到,被Module标记的类,就是一个依赖提供方,类中使用Provides标记提供数据的方法,这里提供了一个药品的数组,很简单


@Module public class MedicineModule { @Provides BaseMedicine[] provideMedicines() { return new BaseMedicine[]{new Medicine1(5), new Medicine2(3)}; } }

然后是依赖需求方,一个病人类,medicines被Inject标注,表明它是需要被注入的对象。Patient是android中的Activity的子类,onCreate是入口函数,注入时机一般会选择在这里,具体注入代码我们一会再添加,继续构建三个角色

public class Patient extends AppCompatActivity {
    @Inject
    BaseMedicine[] medicines;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        //执行注入
    }
}

提供方和需求方构建完成,万事俱备,就差一个~~写代码的~~依赖注入器了

@Component(modules = MedicineModule.class)
public interface DoctorComponent {
    void inject(Patient patient);
    //如果有多个需求方,将会有多个inject方法
    //void inject(Patient2 patient2);
}

是不是非常简单,通过Component标注中的modules参数,设置该注入器具备使用哪些提供方的能力,需要注入的需求方,被作为inject方法中的参数传入,如果有多个需求方,可以添加多个inject方法。

三方齐备,这下可以看看具体的注入操作了。在Patient类的入口函数中,添加如下代码

//进行依赖注入
DaggerDoctorComponent.builder()
                     .medicineModule(new MedicineModule())
                     .build()
                     .inject(this);

简简单单的几行代码,就可以将MedicineModule中provideMedicines方法中生成的数组对象,赋值给Patient类中的medicines,这样病人可以不用关心药是怎么来的,计量应该是多少,只需要吃掉就好。

依赖注入有个很形象的比喻,就是把Dagger框架想象成一个注射器,component是针管,module是药瓶,里面的依赖对象是注入的药水,medicineModule是把药吸进针管,build把针头扎进患者,inject方法是推动活塞,把药水打进去。

生成代码分析

刚才的过程实现很简单,不过有个疑问,不知道大家注意到没有,就是我们用到了DaggerDoctorComponent类,但是根本就没有创建这个类啊,它到底是哪来的,做了什么,这一节将会分析一下这个神奇的类。

先回答一下类是哪里来的,这是在编译过程中,由Dagger自动生成的,生成的依据就是程序中的注解,被标注的类或者方法,都会被生成一些供Dagger使用的代码,这些代码都是可以阅读的,这节就去追踪一下DaggerDoctorComponent的具体内容。

DaggerDoctorComponent内部声明了一个MedicineModule的私有对象,回忆一下DoctorComponent类的标注,@Component(modules = MedicineModule.class),根据modules的内容我们可以猜测,Component设置的Module都会被声明。 DaggerDoctorComponent的构造方法被声明成了私有,传入一个Builder类,这样的话想要初始化,只能使用构造器,从代码中也可以看出,通过调用builder()方法,创建并返回了一个构造类。

public final class DaggerDoctorComponent implements DoctorComponent {
  private MedicineModule medicineModule;

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

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

  @SuppressWarnings("unchecked")
  private void initialize(final Builder builder) {
    //将构造器中的对象赋值给自己
    this.medicineModule = builder.medicineModule;
  }
  //省略部分代码
}

然后看一下Builder,它是DaggerDoctorComponent的一个静态内部类,很简单,DaggerDoctorComponent有什么属性,它就有什么属性,并且提供诸如medicineModule配置方法,最后调用build方法,初始化一个DaggerDoctorComponent的实例并返回,至此构造器的作用就结束了。

public static final class Builder {
    private MedicineModule medicineModule;

    private Builder() {}

    public DoctorComponent build() {
      if (medicineModule == null) {
        this.medicineModule = new MedicineModule();
      }
      return new DaggerDoctorComponent(this);
    }

    public Builder medicineModule(MedicineModule medicineModule) {
      this.medicineModule = Preconditions.checkNotNull(medicineModule);
      return this;
    }
  }

最后看一下DaggerDoctorComponent中被省略的部分,inject方法的实现

 @Override
  public void inject(Patient patient) {
    injectPatient(patient);
  }

  private Patient injectPatient(Patient instance) {
    Patient_MembersInjector.injectMedicines(
        instance,
        Preconditions.checkNotNull(
            MedicineModule_ProvideMedicinesFactory.proxyProvideMedicines(medicineModule),
            "Cannot return null from a non-@Nullable @Provides method"));
    return instance;
  }

这里出现了两个新的类,Patient_MembersInjector和MedicineModule_ProvideMedicinesFactory,inject方法分别调用了两个类的静态方法,而不是对象方法,这样我们可以方便的直接追踪到方法内部,而不用通读源码。先从参数入手,猜一下这里想要做什么。injectMedicines方法需要两个参数,一个是Patient的实例对象,是依赖需求方;另一个是proxyProvideMedicines这个静态方法返回的,类型未知,但是方法参数是medicineModule,依赖提供方的实例对象,很容易联想到是为了获取数据。那injectMedicines方法的意图也非常明显,拿到需求方和提供方对象,将数据放到需要的地方去。验证一下,我们的猜想是否正确。 首先是proxyProvideMedicines方法的返回值

  public static BaseMedicine[] proxyProvideMedicines(MedicineModule instance) {
    return instance.provideMedicines();
  }

出乎意料的简单,直接返回MedicineModule中的provideMedicines方法,就是我们之前定义好的,被Provides标记的方法,不知你还有没有印象,没有的话,也不用往回翻啦,就是下面这些代码。可以看出,确实和我们想的一样,就是拿数据。

@Provides
    BaseMedicine[] provideMedicines() {
        return new BaseMedicine[]{new Medicine1(5), new Medicine2(3)};
    }

再来是injectMedicines方法,简单到不需要我多说,只有一行代码,对被Inject标注的对象进行赋值。

  public static void injectMedicines(Patient instance, BaseMedicine[] medicines) {
    instance.medicines = medicines;
  }

以上就是Dagger2中一次依赖注入的完整流程,看完有没有种‘要什么框架,不就是XXX.XXX = YYY.YYY吗,这种破代码一天我能写好几百行’的感觉。一开始我也是这么想的,不过手写依赖关系管理,势必要面对变量名、方法名的变更,新增、删减依赖关系的维护,随便想一想都要头大,Dagger2的作用也正是体现在这里,制定好一定的使用规则,在这个范围内,免去你的维护工作,只需专注于逻辑,美滋滋。

这些就是我再次入门的一些经验,目前只是简单的使用方法,如何在工程中用好IOC的思想,还需要在实际工作中慢慢摸索和总结。