Dagger 2 进阶使用
[Dagger 2 完全解析(一),基本使用与原理
Dagger 2 完全解析(二), 进阶使用
Dagger 2 完全解析(三), Component 与 SubComponent
Dagger 2 完全解析(四),在Android中的使用
本系列文章是基于 Google Dagger 2.23.2 版本, Kotlin 1.3.21版本
在Dagger 2 完全解析(一),基本使用与原理中介绍了 Dagger 2
基本使用,但是在实战中基本使用是远远不够的,我们还需要掌握一些其它的知识,下面由简入繁地讲解这几个概念,同时结合 Dagger 2 的编译时生成代码分析背后的原理(示例代码沿用第一篇的)。
下面开始对Lazy
、Provider
、Qualifier
和 Scope
进行分析。
Lazy (延迟注入)
有时我们想注入的依赖在使用时再完成初始化,提高加载速度,就可以使用注入Lazy<T>
。只有在调用Lazy
的 get()
方法时才会初始化依赖实例注入依赖。
public interface Lazy<T> {
T get();
}
在A
中使用:
class A {
...
@Inject
lateinit var lazyD: Lazy<D>
fun doWork(){
lazyD.get() // 返回D的实例
}
}
Make app
后,在DaggerAComponent
中:
public final class DaggerAComponent implements AComponent {
private final AModule aModule;
...
@Override
public void injectA(A a) {
injectA2(a);}
private A injectA2(A instance) {
A_MembersInjector.injectB(instance, AModule_ProvideBFactory.provideB(aModule));
A_MembersInjector.injectC(instance, new C());
// DoubleCheck 是线程安全的Lazy实例初始化类
A_MembersInjector.injectLazyD(instance, DoubleCheck.lazy(D_Factory.create()));
return instance;
}
...
DoubleCheck
是线程安全的Lazy
实例初始化工具类
public static <P extends Provider<T>, T> Lazy<T> lazy(P provider) {
if (provider instanceof Lazy) {
@SuppressWarnings("unchecked")
final Lazy<T> lazy = (Lazy<T>) provider;
return lazy;
}
return new DoubleCheck<T>(checkNotNull(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;
}
Provider 注入
有时候不仅仅是注入单个实例,我们需要多个实例,这时可以使用注入Provider<T>
,每次调用它的 get()
方法都会调用到 @Inject 构造函数
创建新实例或者Module 的 provide 方法
返回实例。
class A {
@Inject
lateinit var providerD: Provider<D>
fun createDFactory(): MutableList<D> {
val ds = mutableListOf<D>()
for (i in 0..10) {
ds.add(providerD.get())
}
return ds
}
}
Make app
后,在DaggerAComponent
中:
public final class DaggerAComponent implements AComponent {
private final AModule aModule;
...
@Override
public void injectA(A a) {
injectA2(a);}
private A injectA2(A instance) {
A_MembersInjector.injectB(instance, AModule_ProvideBFactory.provideB(aModule));
A_MembersInjector.injectC(instance, new C());
A_MembersInjector.injectLazyD(instance, DoubleCheck.lazy(D_Factory.create()));
// 注入provider ,将`Factory`传入
A_MembersInjector.injectProviderD(instance, D_Factory.create());
return instance;
}
...
再来看看D_Factory
:
public final class D_Factory implements Factory<D> {
private static final D_Factory INSTANCE = new D_Factory();
// 每次调用get方法都会重新创建实例
@Override
public D get() {
return new D();
}
public static D_Factory create() {
return INSTANCE;
}
public static D newInstance() {
return new D();
}
}
通过上面可以看到,每次调用get
方法都会重新创建实例, D_Factory
实现了Factory<D>
而Factory<D>
继承自Provider<D>
。
public final class D_Factory implements Factory<D> {}
public interface Factory<T> extends Provider<T> {}
Qualifier(限定符)
试想这样一种情况:在 AModule
提供了两个生成B
实例的provide
方法,如果使用 Dagger 2
在 A
中注入B
实例时应该选择哪一个方法呢?
@Module
class AModule {
@Provides
fun provideB(): B = B()
@Provides
fun provideOther(): B = B()
}
这时 Dagger 2 不知道使用provideB
还是provideOther
提供的实例,在编译时就会出现:
[Dagger/DuplicateBindings] *.data.B is bound multiple times:
而@Qualifier
注解就是用来解决这个问题,使用注解来确定使用哪种 provide 方法。
@Target(ANNOTATION_TYPE)
@Retention(RUNTIME)
@Documented
public @interface Qualifier {}
我们可以发现@Qualifier
是用来修饰注解的,所以它是不能直接作用到provide
方法上。
下面是自定义的@Named
注解,你也可以用自定义的其他 Qualifier 注解:
@Qualifier
@Documented
@Retention(RUNTIME)
public @interface Named {
String value() default "";
}
在 provide 方法上加上@Named
注解,用来区分
@Module
class AModule {
@Named("b")
@Provides
fun provideB(): B = B()
@Named("other")
@Provides
fun provideOther(): B = B()
}
还需要在Inject
注入的地方加上@field:Named
注解:
class A {
@Inject
@field:Named("other")
lateinit var b: B
...
}
**tips: ** 在kotlin
中使用@Name
给属性注入时需要添加@field
,不然注解不生效。
这样在依赖注入时,Dagger 2 就会使用provideB
方法提供的实例,所以Qualifier(限定符)的作用相当于起了个区分的别名。
public final class DaggerAComponent implements AComponent {
private final AModule aModule;
...
@Override
public void injectA(A a) {
injectA2(a);}
private A injectA2(A instance) {
// 使用的是 provide方法
A_MembersInjector.injectB(instance, AModule_ProvideBFactory.provideB(aModule));
...
return instance;
}
...
}
当然啦,我们也可以自定义注解进行区分,比如:
@Qualifier
@Retention(AnnotationRetention.RUNTIME)
@Target(AnnotationTarget.FUNCTION, AnnotationTarget.FIELD)
annotation class Other
然后在provideOther
和lateinit var b: B
上加上@Other
即可。
Scope(作用域)
Scope 是用来确定注入的实例的生命周期的,如果没有使用 Scope 注解,Component 每次调用 Module 中的 provide 方法或 Inject 构造函数
生成的工厂时都会创建一个新的实例,而使用 Scope 后可以复用之前的依赖实例。下面先介绍 Scope 的基本概念与原理,再分析 Singleton、Reusable 等作用域。
Scope 基本概念
先介绍 Scope 的用法,@Scope
是元注解,是用来标注自定义注解的,如下:
@Target(ANNOTATION_TYPE)
@Retention(RUNTIME)
@Documented
public @interface Scope {}
**Scope 注解只能标注目标类、@provide 方法和 Component。**Scope 注解要生效的话,需要同时标注在 Component
和提供依赖实例的Module 或目标类
上。Module 中 provide 方法中的 Scope 注解必须和 与之绑定的 Component 的 Scope 注解一样,否则作用域不同会导致编译时会报错。
那么 Scope 注解又是如何产生作用的呢,怎么保证生成的依赖实例的生命周期呢?
在 Dagger 2 官方文档中我找到一句话,非常清楚地描述了@Scope
的原理:
When a binding uses a scope annotation, that means that the component object holds a reference to the bound object until the component object itself is garbage-collected.
当 Component 与 Module、目标类(需要被注入依赖)使用 Scope 注解绑定时,意味着 Component 对象持有绑定的依赖实例的一个引用直到 Component 对象本身被回收。也就是作用域的原理,其实是让生成的依赖实例的生命周期与 Component 绑定,Scope 注解并不能保证生命周期,要想保证赖实例的生命周期,需要确保 Component 的生命周期。
下面以@AScope
为例,看 Scope 注解背后的代码:
先定义AScope
@Scope
@Retention(RUNTIME)
@Target(FIELD, FUNCTION, CLASS)
annotation class AScope
使用Module方式
在AModule
中:
@Module
class AModule {
@AScope
@Provides
fun provideB(): B = B()
}
在AComponent
中
@Component(modules = [AModule::class])
@AScope
interface AComponent {
fun injectA(a: A)
}
这样生成的 B
实例就与 AComponent
绑定了。下面看编译时生成的代码:
public final class DaggerAComponent implements AComponent {
private Provider<B> provideBProvider;
private DaggerAComponent(AModule aModuleParam) {
initialize(aModuleParam);
}
...
@SuppressWarnings("unchecked")
private void initialize(final AModule aModuleParam) {
this.provideOtherProvider = DoubleCheck.provider(AModule_ProvideBFactory.create(aModuleParam));
}
@Override
public void injectA(A a) {
injectA2(a);}
private A injectA2(A instance) {
// provideOtherProvider 不再由Factory创建,而是DobuleCheck
A_MembersInjector.injectB(instance, provideBProvider.get());
return instance;
}
...
}
从上面 DaggerAComponent
的代码可以看出使用了 AScope
作用域后,provideBProvider
由AModule_ProvideCarFactory.create()
变为了DoubleCheck.provider(AModule_ProvideBFactory.create())
。而 DoubleCheck 包装的意义在于持有了 B
的实例,而且只会生成一次实例,也就是说:没有用 MyScope 作用域之前,DaggerAComponent
每次注入依赖都会新建一个 B
实例,而用 AScope
作用之后,每次注入依赖都只会返回第一次生成的实例。通过生成的代码可以发现这和Lazy
的方式很相似。
注解到目标类方式
class A {
@AScope
@Inject
lateinit var b: B
}
@AScope
class B @Inject constructor()
@AScope
@Component(modules = [AModule::class])
interface AComponent {
fun injectA(a: A)
}
@Module
class AModule {
}
使用这种方式生成的代码:
public final class DaggerAComponent implements AComponent {
private Provider<B> bProvider;
private DaggerAComponent() {
initialize();
}
...
@SuppressWarnings("unchecked")
private void initialize() {
// 此处使用的是Factory,不是AModule_ProvideBFactory
this.bProvider = DoubleCheck.provider(B_Factory.create());
}
@Override
public void injectA(A a) {
injectA2(a);}
private A injectA2(A instance) {
A_MembersInjector.injectB(instance, bProvider.get());
return instance;
}
...
}
Scope 作用域的本质:Component 间接持有依赖实例的引用,把实例的作用域与 Component 绑定
Singleton
在了解作用域的原理后,再来理解 Dagger 2 提供的自带作用域就容易了。@Singleton
顾名思义保证单例,那么它又是如何实现的呢,实现了单例模式那样只返回一个实例吗?
把上面例子中@AScope
换成@Singleton
,发现生成的 DaggerAComponent 和其他类没有变化。也只是用DoubleCheck
包装了工厂而已,并没有什么特殊实现。所以 Singleton 作用域可以保证一个 Component 中的单例,但是如果产生多个 Component 实例,那么实例的单例就无法保证了。
所以在网上一些例子中,有看到AppComponent
使用 Singleton 作用域,保证绑定的依赖实例的单例。它生效的原因是AppComponent
只会在 Application 中创建一次,由AppComponent
的单例来保证绑定的依赖实例的单例。
**注意:Component 可以同时被多个 Scope 标记。**即 Component 可以和多个 Scope 的 Moudle 或目标类绑定。
Reusable
上文中的自定义的@AScope
和@Singleton
都可以使得绑定的 Component 缓存依赖的实例,但是与之绑定 Component 必须有相同的 Scope 标记。假如我只想单纯缓存依赖的实例,可以复用之前的实例,不想关心与之绑定是什么 Component,应该怎么办呢?。
这时就可以使用@Reusable
作用域,**Reusable 作用域不关心绑定的 Component,Reusable 作用域只需要标记目标类或 provide 方法,不用标记 Component。**下面先看看使用 Reusable 作用域后,生成的 DaggerAComponent
的变化:
public final class DaggerAComponent implements AComponent {
private Provider<B> provideBProvider;
private DaggerAComponent(AModule aModuleParam) {
initialize(aModuleParam);
}
@SuppressWarnings("unchecked")
private void initialize(final AModule aModuleParam) {
this.provideBProvider = SingleCheck.provider(AModule_ProvideBFactory.create(aModuleParam));
}
@Override
public void injectA(A a) {
injectA2(a);
}
private A injectA2(A instance) {
A_MembersInjector.injectB(instance, provideBProvider.get());
return instance;
}
...
}
从上面代码可以看出使用@Reusable
作用域后,利用到 Reusable 实例的 Component 会间接持有实例的引用。但是这里是用SingleCheck
而不是DoubleCheck
,在多线程情况下可能会生成多个实例。因为@Reusable
作用域目的只是可以复用之前的实例,并不需要严格地保证实例的唯一,所以使用 SingleCheck
就足够了。
Releasable references(可释放引用)
相关内容在新版Dagger2
已经废除。
Binding Instances
通过前面作用域的讲解,可以清楚 Component 可以间接持有 Module 或 Inject 目标类构造函数提供的依赖实例,除了这两种方式,Component 还可以在创建 Component 的时候绑定依赖实例,用以注入。这就是@BindsInstance
注解的作用,只能在 Component.Builder 中使用。
在 Android 中使用 Dagger 2 时,activity 实例经常也需要作为依赖实例用以注入,在之前只能使用 Module:
@Module
class MainActivityModule {
private val mainActivity: MainActivity
constructor(mainActivity: MainActivity) {
this.mainActivity = mainActivity
}
@Provides
fun provideMainActivity(): MainActivity {
return this.mainActivity
}
}
而使用@BindsInstance
的话会更加简单:
@Component
interface MainActivityComponent {
fun injectMainActivity(activity: MainActivity)
@Component.Builder
interface Builder {
@BindsInstance
fun activity(activity: Activity): Builder
fun build(): MainActivityComponent
}
}
注意在调用build()
创建 Component 之前,所有@BindsInstance
方法必须先调用。上面例子中 MainActivityComponent
还可以注入 Activity 类型的依赖,但是不能注入 MainActivity
,因为 Dagger 2 是使用具体类型作为依据的(也就是只能使用@Inject Activity activity
而不是@Inject MainActivity activity
)。
如果@BindsInstance
方法的参数可能为 null,需要再用@Nullable
标记,同时标注 Inject 的地方也需要用@Nullable
标记。这时 Builder 也可以不调用@BindsInstance
方法,这样 Component 会默认设置 instance 为 null。
总结
- Lazy 可以延时注入,Provider 可以创建多个实例
- Qualifier 限定符用来解决同一个实例不同方法提供冲突的问题,可以依赖实例起个别名用来区分,或者自定义注解
- Scope 作用域的本质是 Component 会持有与之绑定的依赖实例的引用,要想确保实例的生命周期,关键在于控制 Component 的生命周期。
- 推荐优先使用
@BindsInstance
方法,相对于写一个带有构造函数带有参数的 Module。