依赖注入(三)—— Dagger的作用域 Scope

1,836 阅读3分钟

在Dagger中,Scope用来限定依赖容器中依赖项的生命周期。默认情况下,我们每次获取依赖项,Dagger都会通过Provides-methodInject-constructor获取一个新的实例。但是,在某些场景下,我们并不想每次都构造新的实例,而是希望可以在一定范围内复用同一实例对象。此时我们就可以使用Scope

Dagger的Scope可以将我们构造的依赖项与Component实例绑定起来,只要通过同一个Component实例进行依赖注入,注入的依赖项就是同一个,从而实现了限定作用域的效果。

如何使用

首先需要使用@Scope元注解定义一个作用域注解:

@MustBeDocumented
@Retention(AnnotationRetention.RUNTIME)
@Scope
annotation class MyScope

然后使用此作用域注解标记Component接口:

@MyScope
@Component(modules = [CommonModule::class])
interface CommonComponent {
    fun inject(activity: Context)
    fun student(): Student
}

然后再用此作用域注解标记要和Component关联的依赖项的类或者Provides方法:

@MyScope
class Student @Inject constructor()

@MyScope
@Provides
fun provideStudent(): Student { ... }

此时我们再使用同一个Component实例获取依赖项时,就会是同一个了:

val commonComponent = DaggerCommonComponent.create()
commonComponent.student().toString() // Student@751d71c
commonComponent.student().toString() // Student@751d71c

💡 Dagger依赖javax.inject包中定义的注解和接口,其中提供了一个默认的@Singleton作用域,需要注意的是,在Dagger中,@Singleton注解与我们自定义的作用域注解别无二致,Dagger并未对其做任何特殊处理。

实现分析

Dagger会为Inject-constructorProvides-method生成对应的对象工厂:

public final class Student_Factory implements Factory<Student> {
  @Override
  public Student get() { return newInstance(); }

  public static Student newInstance() { return new Student(); }
  public static Student_Factory create() { return InstanceHolder.INSTANCE; }
  private static final class InstanceHolder {
    private static final Student_Factory INSTANCE = new Student_Factory();
  }
}

然后会为Component接口生成实现类,并在其中创建对象工厂,并将获取依赖项的请求转发给对应的对象工厂实例:

private static final class CommonComponentImpl implements CommonComponent {
    private final CommonComponentImpl commonComponentImpl = this;
    private Provider<Student> studentProvider;

    private CommonComponentImpl() { initialize(); }

    private void initialize() {
      this.studentProvider = Student_Factory.create();
    }
    @Override
    public void inject(Context context) { }
    @Override
    public Student student() { return studentProvider.get(); }
  }

当我们使用作用域注解标记依赖项和Component接口后,在Component实现类中,会使用DoubleCheck.provider方法为对象工厂实例生成代理,而这个代理就是用来生成和缓存单例:

private void initialize() {
   this.studentProvider = DoubleCheck.provider(Student_Factory.create());
}

让我们来看一下DoubleCheck的实现:

public final class DoubleCheck<T> implements Provider<T>, Lazy<T> {
  private static final Object UNINITIALIZED = new Object();
  private volatile Provider<T> provider;
  private volatile Object instance = UNINITIALIZED;

  private DoubleCheck(Provider<T> provider) {
    assert provider != null;
    this.provider = provider;
  }

  @Override
  public T get() {
    Object result = instance;
    if (result == UNINITIALIZED) {
      synchronized (this) {
        result = instance;
        if (result == UNINITIALIZED) {
          result = provider.get();
          instance = reentrantCheck(instance, result);
          provider = null;
        }
      }
    }
    return (T) result;
  }

  public static <P extends Provider<T>, T> Provider<T> provider(P delegate) {
    if (delegate instanceof DoubleCheck) return delegate;
    return new DoubleCheck<T>(delegate);
  }
  ...
}

可以看到,对象工厂实例providerDoubleCheck代理后,通过get获取对象实例时,DoubleCheck做了一个双检查的缓存实现。而Component获取对象实例时正是调用的此方法,因此,只要使用的Component实例对象不发生改变,获取的依赖对象也就是唯一的。

Reusable作用域

有时候我们并不需要严格的保障在某个组件中注入的依赖实例完全相同,而只是想尽量减少实例的构造方法或provider方法的调用(因为这些方法的执行可能会比较昂贵),此时可以使用Reusable作用域。

@Reusable是Dagger定义的一个特殊的作用域注解,和其它作用域注解不一样,该注解只需要对依赖提供方法进行标记即可,无需与任何Component相关联。

对于使用@Reusable标记的依赖项,Dagger会为每一个Component中该依赖的对象工厂生成SingleCheck代理对象,该对象中的get方法是一个非线程安全的单检查的缓存实现。因此,当有多个线程同时通过同一个Component实例获取同一个依赖项实例时,它可能会返回不同的实例。SingleCheck的代码实现和DoubleCheck大同小异,详细请自行查看源码。