美文网首页
Dagger2 依赖的接力游戏(六):Bind、BindInst

Dagger2 依赖的接力游戏(六):Bind、BindInst

作者: 散落_a0b3 | 来源:发表于2019-01-04 23:41 被阅读0次

    文接《Dagger2 依赖的接力游戏(五)》

    本篇示例代码收录在项目的chapter6分支

    我们知道Dagger2是帮我们管理依赖关系的工具,而它对依赖关系的匹配是基于Class类型、Qualifier、Scope三者综合判断的。在前面的文章中,我们声明某一个类型的需求或者提供,都是使用注解直接声明的。比如我们使用Provides注解声明一个提供类型:

    @Provides
    Engine getEngine(){
        return new Engine(1);
    }
    

    或者在Component中声明一个需求类型:

    @Component
    public interface EngineComponent{
        Engine getEngine();
    }
    

    Dagger都会将它们的类型当作Engine.class。除此之外,Dagger还提供了一些间接的类型声明方法,这就是本文要讲的三种注解。

    Bind注解

    Bind注解像一个桥梁,它仅作用于提供类型的声明。它可以将某个父类型的需求,嫁接到其子类型的实现上,从而避免子类型的暴露,使调用代码依赖于抽象类型,而不是具体的子类。我们用示例来说明这个作用。

    比如我们有一个CarComponent,它提供两种不同的Car实例,getCarA用于创建Mondeo汽车,getCarB用于创建没有品牌的汽车。

    @Subcomponent(modules = CarModule.class)
    public interface CarComponent {
    
        @QualifierMondeo
        Car getCarA();
    
        Car getCarB();
    
        @Subcomponent.Builder
        interface Builder{
            CarComponent build();
        }
    }
    

    那我们在CarModule中可以这么实现:

    @Module
    public abstract class CarModule {
        @Binds
        @QualifierMondeo
        abstract Car provideCarA(Mondeo mondeo);
    }
    

    我们使用@Bind修饰,将Car类型的依赖,转接到了Mondeo类。当然Mondeo的提供类型也是要声明的,我们用构造方法来实现,并且给Mondeo一个名字和八缸引擎:

    public class Mondeo extends Car {
        @Inject
        public Mondeo(Engine engine){
            super(engine);
            mName = "mondeo";
            engine.mCylinderNumbers = 8;
        }
    }
    

    因为我们使用了Bind注解,CarModule变成了抽象类,不能提供创建普通Car的方法了,所以我们使用Car构造方法来声明P(Car)。

    public class Car {
        String mName;
        Engine mEngine;
        @Inject
        public Car(Engine engine){
            mEngine = engine;
        }
        public String getName(){
            return mName;
        }
        Engine getEngine(){
            return mEngine;
        }
    }
    

    Car依赖的Engine类型提供我们就不在解释了,默认的引擎数是1。我们用下面的代码来测试一下:

        public static void main(String[] args) {
            EngineComponent engineComponent = DaggerEngineComponent.builder()
                    .engineModule(new EngineModule()).tag("tagged success")
                    .build();
            testCar(engineComponent);
        }
        private static void testCar(EngineComponent engineComponent) {
            CarComponent carComponent = engineComponent.carComponent().build();
            Car carA = carComponent.getCarA();
            Car carB = carComponent.getCarB();
            System.out.println("carA : " + carA + "; name : " + carA.getName() + "; cylinders : " + carA.getEngine().getCylinderNumbers());
            System.out.println("carB : " + carB + "; name : " + carB.getName() + "; cylinders : " + carB.getEngine().getCylinderNumbers());
        }
    

    得到结果:

    carA : com.example.dagger2.Mondeo@610455d6; name : mondeo; cylinders : 8
    carB : com.example.dagger2.Car@511d50c0; name : null; cylinders : 1
    

    说明我们的依赖类型嫁接成功了,getCarA最终调用的是Mondeo的构造函数。我们稍微看下源码:

      private final class CarComponentImpl implements CarComponent {
        private CarComponentImpl(CarComponentBuilder builder) {}
    
        private Mondeo getMondeo() {
          return new Mondeo(
              EngineModule_ProvideEngineFactory.proxyProvideEngine(
                  DaggerEngineComponent.this.engineModule));
        }
    
        @Override
        public Car getCarA() {
          return getMondeo();
        }
    
        @Override
        public Car getCarB() {
          return new Car(
              EngineModule_ProvideEngineFactory.proxyProvideEngine(
                  DaggerEngineComponent.this.engineModule));
        }
      }
    

    可以看到依赖嫁接之后,getCarA的方法,调用的Mondeo类的构造方法,而getCarB调用的是Car构造方法。

    有的同学就说了,这么写和我直接使用@Provide创建一个对象有啥区别呢?比如CarModule可以写成这样:

    @Module
    public class CarModule {
        @Binds
        @QualifierMondeo
        Car provideCarA(Engine engine){
            return new Mondeo(engine)
        }
    }
    

    这里我把用这种方式实现的源码也贴出来,通过代码看看两者区别

        @Override
        public Car getCarA() {
          return CarModule_ProvideCarAFactory.proxyProvideCarA(
              carModule,
              EngineModule_ProvideEngineFactory.proxyProvideEngine(
                  DaggerEngineComponent.this.engineModule));
        }
    
        @Override
        public Car getCarB() {
          return new Car(
              EngineModule_ProvideEngineFactory.proxyProvideEngine(
                  DaggerEngineComponent.this.engineModule));
        }
    

    对比前面的getCarA以及这里的getCarB方法,我们发现如果使用绑定的方式来创建Mondeo,我们不需要创建CarModue实例(也不能为CarComponent创建CarModue实例),然后再通过它创建我们真正要的Mondeo,而是直接调用构造方法,这在执行效率上会更加高效一些。

    当然,我觉得最大的好处就是方便,反正我的Mondeo构造方法也是要写的,再加个注解就好了,为啥这里还要再多些一遍创建实例的代码,万一参数或者返回类型有变化,修改起来也麻烦。

    BindInstance注解

    BindInstance注解像是一个管道,它允许我们在Component里,提前为某个类型对象打一个桩,并在构造Component实例的时候,用该类型或子类型的一个对象,替换(赋值)这个桩,以便其他地方获取。通过BindInstance打出来的桩,在构造对象的时候必须进行非空赋值,否则会导致异常。Dagger2将它设计为一个强制绑定关系,仅供必要时使用。

    以下来看示例代码:

    @Component(modules = {EngineModule.class})
    public interface EngineComponent {
    
        String getTag();
    
        CarComponent.Builder carComponent();
    
        @Component.Builder
        interface Builder{
    
            @BindsInstance
            Builder tag(String tag);
    
            Builder engineModule(EngineModule engineModule);
    
            EngineComponent build();
        }
    }
    

    在上面的EngineComponent里,我们定义了String类型的getTag方法,这个方法返回的对象,是在Builder中tag方法传递进去的。如果我们不在Builder中定义tag方法,那么Component就会在Module中进行查找,导致编译失败。所以为了定义这个tag方法,我们需要向上面一样,使用Component.Builder注解,手动实现我们的Builder类,而这本来是可以省略的。同样,如果我们在Module中有相同的提供类型,就会因类型冲突导致报重复绑定错误,所以这也是一个基于类型的强制绑定。

    现在我们来测试一下效果:

        public static void main(String[] args) {
            EngineComponent engineComponent = DaggerEngineComponent.builder()
                    .engineModule(new EngineModule()).tag("tagged success")
                    .build();
            System.out.println("EngineComponent tag : " + engineComponent.getTag());
        }
    

    查看结果:

    EngineComponent tag : tagged success
    Process finished with exit code 0
    

    结果是正确的。这个注解,就是用于扩充我们Component的功能的,相对来说还是比较直观的。

    MultiBinds注解

    在介绍MultiBinds注解之前,我们要先了解这样的一种需求,假如我们需要在Module里提供了很多相同类型的 对象,如果我们不使用Qualifer,就会导致同一类型重复绑定的错误。但是如果我们确实需要在一个Module里包含这些对象的创建,又不想创建N多的Qualifer,我们就可以使用MultiBind机制来达到我们的目的。

    MultiBind机制允许我们为这些对象创建一个集合,这个集合必须是Set或者Map,这样在Component中,我们就可以暴露这个集合,通过集合来获取不同的对象。这个集合的创建有三种方法,我们用简短的代码例子说明一下。

    1. 使用@IntoSet或者@IntoMap
    @Module(subcomponents = CarComponent.class)
    public class EngineModule {
    
        @Provides
        @IntoSet
        Engine provideEngineIntoSet2(){
            return new Engine(2);
        }
    
        @Provides
        @IntoMap
        @StringKey("3")
        Engine provideEngineIntoMap3(){
            return new Engine(3);
        }
    }
    
    

    这种方声明使用集合,我们的提供方法返回的还是原来的类型,但是Dagger2会根据注解,将它转换为Set或者Map类型,并调用这些方法创建对象,将这些对象(与索引绑定之后,如果是Map类型的话)放入集合中。

    1. 直接提供Set或者Map类型
    @Module(subcomponents = CarComponent.class)
    public class EngineModule {
    
        @Provides
        Set<Engine> provideEngineSet(){
            Set<Engine> engines =  new HashSet<>();
            engines .add(new Engine(9));
            return engines;
        }
    
        @Provides
        Map<String, Engine> provideEngineMap(){
            Map<String,Engine> engineMap =  new HashMap<>();
            engineMap.put("engine10", new Engine(10));
            return engineMap;
        }
    }
    

    直接提供Set和Map就比较直观了,但是这种方式和使用前面一种是会冲突的,Dagger2允许使用@ElementIntoSet注解,将自定义的set元素添加到自动生成的Set中,但是Map是不能混用的,放在不同的Module中也不行。

    1. 使用@MultiBinds注解
    @Module(subcomponents = CarComponent.class)
    public abstract class AbsEngineModule {
    
        @Multibinds
        abstract Set<Engine> engineSet();
    
        @Multibinds
        abstract Map<String, Engine> engineMap();
    }
    

    MultiBinds只能用于标注抽象方法,它仅仅是告诉Component我有这么一种提供类型,让我们Component可以在Component中暴露Set或者Map类型的接口,但是不能包含具体的元素。Multibinds注解是可以和第一种集合定义混用的。

    我在学习MultiBinds注解的时候,非常的郁闷。既然MultiBinds用于提供集合,为什么只能是空集?我要往里面添加元素,还得用@IntoSet和@IntoMap标注提供方法,而它们是可以独立工作的,MultiBinds的在这儿的作用不是等于0么?

    是的,如果我们已知有哪些元素可以使用,完全没有必要用MultiBinds标签的,这个标签的作用,就是在不知道有哪些元素可以使用,但是我在Component里要暴露集合集合,以便其他地方可以调用。这在dagger-android框架里体现的就非常明显。它在框架中有这么一个类

    @Module
    public abstract class AndroidInjectionModule {
      @Multibinds
      abstract Map<Class<? extends Activity>, AndroidInjector.Factory<? extends Activity>>
          activityInjectorFactories();
    
      @Multibinds
      abstract Map<Class<? extends Fragment>, AndroidInjector.Factory<? extends Fragment>>
          fragmentInjectorFactories();
    
      @Multibinds
      abstract Map<Class<? extends Service>, AndroidInjector.Factory<? extends Service>>
          serviceInjectorFactories();
    
      @Multibinds
      abstract Map<
              Class<? extends BroadcastReceiver>, AndroidInjector.Factory<? extends BroadcastReceiver>>
          broadcastReceiverInjectorFactories();
    
      @Multibinds
      abstract Map<
              Class<? extends ContentProvider>, AndroidInjector.Factory<? extends ContentProvider>>
          contentProviderInjectorFactories();
    
      private AndroidInjectionModule() {}
    }
    

    这个Module里使用@Multibinds定义了四大组件加Fragment的AndroidInjector.Factory的集合,就是为了创建 DispatchingAndroidInjector<T>这个对象,我们看下它的构造方法:

    DispatchingAndroidInjector(
          Map<Class<? extends T>, Provider<AndroidInjector.Factory<? extends T>>> injectorFactories) {
        this.injectorFactories = injectorFactories;
      }
    

    然后这个对象进行代理注入:

      public boolean maybeInject(T instance) {
        Provider<AndroidInjector.Factory<? extends T>> factoryProvider =
            injectorFactories.get(instance.getClass());
        if (factoryProvider == null) {
          return false;
        }
        //此处省略N行代码
      }
    

    可以看到,它就是使用T的Class作为索引,从集合中取出对应的AndroidInject.Fractory对象进行工作的。而这其中的T,就是我们在App自己定义的各种组件,在dagger-android框架里,是不知道有哪些对象可以使用的。

    总得来说,Multibinds注解的作用,就是为了Component提供依赖类型的完整性,这在编写一些框架给外部使用的时候,会起到关键的作用,其他时候,是可以使用@IntoXX标签替代的

    现在我们还是继续看一下使用集合之后,Component里的写法:

    @Component(modules = {EngineModule.class, AbsEngineModule.class})
    public interface EngineComponent {
    
        String getTag();
    
        Set<Engine> engineSet();
    
        Map<String, Engine> engineMap();
    
        CarComponent.Builder carComponent();
    
        @Component.Builder
        interface Builder{
    
            @BindsInstance
            Builder tag(String tag);
    
            Builder engineModule(EngineModule engineModule);
    
            EngineComponent build();
        }
    }
    

    我们在看看main中的测试写法:

        private static void testEngine(EngineComponent engineComponent) {
            
            System.out.println("EngineComponent tag : " + engineComponent.getTag());
            for (Engine engine : engineComponent.engineSet()) {
                System.out.print("engine in set : " + engine + "; cylinders : " + engine.getCylinderNumbers() + "\n");
            }
            for (Engine engine : engineComponent.engineMap().values()) {
                System.out.print("engine in map : " + engine + "; cylinders : " + engine.getCylinderNumbers() + "\n");
            }
        }
    

    打印的结果是:

    EngineComponent tag : tagged success
    engine in set : com.example.dagger2.Engine@3f99bd52; cylinders : 2
    engine in map : com.example.dagger2.Engine@548c4f57; cylinders : 3
    

    说明结果是正确的。

    我们再看看关键的实现源码:

      @Override
      public Set<Engine> engineSet() {
        return ImmutableSet.<Engine>of(
            EngineModule_ProvideEngineIntoSet2Factory.proxyProvideEngineIntoSet2(engineModule));
      }
    
      @Override
      public Map<String, Engine> engineMap() {
        return ImmutableMap.<String, Engine>of(
            "5", EngineModule_ProvideEngineIntoMap3Factory.proxyProvideEngineIntoMap3(engineModule));
      }
    

    我们在调用engineSet或者engineMap方法,获取对应集合的时候,Dagger直接通过Module实例创建我们要的对象,然后放到ImmutableXX里(这是个对象固定的集合),也就是说通过第一和第三种方式暴露的集合,对象引用是不可以替换的。使用第二种的,和我们正常暴露的普通类型是一样的,大家可以自己试验一下。

    小结

    本篇文章介绍了三种间接定义类型的方法,让我们可以更加灵活地使用类型绑定来构造对象。到此为止,Dagger的主要功能,我们已经介绍完毕了,下一篇,我们讲解dagger-android库,是时候真刀真枪地玩了。

    相关文章

      网友评论

          本文标题:Dagger2 依赖的接力游戏(六):Bind、BindInst

          本文链接:https://www.haomeiwen.com/subject/gbzkrqtx.html