Dagger是一个依赖注入框架,这个框架往往给人一种“高端,但是却很难用“的感觉。我们在使用或者阅读别人的项目时,往往会陷入dagger搭建的”迷宫里”绕不出来。导致Dagger被认为是android史上”最受冷落的优质库”。
这里主要一方面是由于,Dagger本身功能的复杂和太灵活导致使用起来比较困难。还有一方面原因是对Dagger的定位和依赖注入的概念比较模糊。
本文先通过理清依赖注入的概念,然后详细介绍Dagger一些常见的使用场景和注意事项,最后介绍一下Dagger主要功能实现的原理。帮助大家进行理解。
一、依赖注入
我们知道在一个类中,通常会定义其他类型的变量,这个变量就是我们所说的“依赖“。
对一个类的变量进行初始化,有两种方式。第一种,这个类自己进行初始化;第二种,其他外部的类帮你进行初始化。
其中第二种方式,由外部类进行初始化的方式就是我们所说的”依赖注入“。由于他是由外部来控制,因此又叫做”控制反转“。依赖注入和非依赖注入的区别就是,变量初始化工作是由谁来做的。我们常用的工厂、建造者、带参数的构造函数,都是依赖注入。
因此,实际上我们一直都在使用“依赖依赖“,对它其实并不陌生。而Dagger的定位就是提供了用注解的方式来配置依赖的方法。因此Dagger并不是提供依赖注入的能力,而是为依赖注入提供一种更简单的方法。
二、Dagger2的使用
1、耦合
先看一个没有使用Dagger的一个简单的示例。
上面是一个非常简单框架场景,RequestManager中持有RemoteService和LocalService可以分别进行本地和远端的操作,我们假设提供了保存数据的方法。假设此时要将本地数据保存到SharedPreferences中,我们知道使用SP则需要用到上下文的Context信息。
我们往往把Context从最顶层的调用者Activity,通过RequestManager传到LocalService。上面代码可能会变成下面这样。一次性要改动三个地方。可谓牵一发而动全身。此时,我们说它产生了较高的耦合。
当然,我们可以通过在RequestManager接收LocaService和RemoteService类型的参数,并且在MainAcitivty创建好之后传给它。这样来减少耦合。
我们来看一下使用Dagger2的方案。简单介绍一下下面的代码,SaveDataModule负责提供组件,SavaDataComponet负责连接组件提供者和使用,调用它的inject的方法,将组件注入到RequestManager中。以此来达到组件使用者和组件之间的解耦。
2、Dagger2主要结构
上面的例子中,我们看到Dagger2主要用法,这里涉及到三个关键的组件Inject、Module和Component。它们分别表示,组件接收(注入)的位置、组件提供者和连接它们的桥梁。
简单来说,就是Component将Module提供的组件注入到接收的位置。它是基于注解处理器实现的,通过注解处理器对我们注解的部分,生成中间代码。文章的最后我们看一下注解处理器帮我们生成的代码,了解一下Dagger2实现的原理。
其他还有@Provider注解,表示这是一个提供组件的方法。通过对比它的返回值类型和接受处参数的类型,来进行匹配。如果出现提供多个相同返回类型的方法,通过@Name注解进行区分,这个注解后面进行讲解。
3、组件依赖参数的方式
上面我们的示例,我们看到向组件传递参数的第一种方式,在Module中进行提供。另外还有一种方式,用@Inject注解修饰需要进行依赖的参数。
现在对RemoteService提供一个Url信息,用于进行网络访问。增加一个提供Url的组件—UriModule,提供Url的方法用@Provider注解。将这个组件一起关联到我们的Component。用@Inject在需要用到这个参数的方法上进行注解。这个信息就会被注入到方法的参数位置。
4、多模块间引入
在上面的示例中我们在SavaDataModule中使用了UriModule,既是模块间的引入。除了上面给出的在Component中指定多个模块可以相互引入之外,还有两种模块间引入的方式:在自己模块中直接引入和通过其他Component引入。分别来看一下。
直接引入
将上面UriModule改为在SaveDataModule中直接引入。
通过Component引入
将目标Module引入一个新的Component,并在我们的Component中依赖该Component。改动如下,其他不变。
5、返回相同类型不同实例
在2小结中提到,如果多个Provider提供相同的返回值如何进行区分?这里使用@Named进行注解。
比如,我们需要根据开发版本或者发布版本返回不同实例。只需要在Provider的位置用@Named注解提供不同名称进行区分。在使用时(注入的位置)也用同样的@Named就可以准确的进行注入。
受@Named注解启发,我们可以参考@Namede 注解,自定义自己的注解,进行更优雅的实现。
@Named注解,有三个元注解,其中@Qualifier用来区分不同对象实例。(关于注解有不太了解的,可以看一下我的博客“Java高级语言特性—注解”Java高级语言特性 ---- 注解_源码级注解对ide检查_风行水上_ZH的博客-CSDN博客)。
仿照它我们自定义一个Release注解和一个Dev注解。
在提供组件的位置和注入组件的位置,分别用我们定义的注解进行修饰,即可完成准确的注入。
6、单例
Dagger2中使用@Singleton来实现提供者每次返回的都是同一个对象,就是我们说的单例。写法下面这样,需要注意的是,在@Providers和@Component注解位置都需要写上@Singleton注解,才能生效。
我们知道java中的单例,是一种“全局的“,在整个jvm虚拟机中只有一个实例。需要注意的是,Dagger2中的单例不同,它是一种“有范围的单例”。上面用Singleton修饰的单例。我们增加一个SaveDataComponent2同样关联SaveDataMoule,将它注入到RequestManager2对象中。此时在RequestManager2中注入的LocalService和RequestManager中注入的LocalService将是不同的对象。
Dagger2中实现全局单例的做法通常是,提供一个全局的单例Component,在Application进行初始化,再提供给其他的Component使用。
定义一个用Singleton修饰的组件,关联一个提供单例对象的模块。
在Application中进行初始化,并提供返回的方法。
在其他组件中进行依赖,并在对组件进行初始化时,通过Appliction获取该单例组件对依赖组件进行初始化。此时就是全局单例的。
7、Scope
上面的小结中,我们看到在SaveDataComponent的注解上我们使用了@ActivityScope。这个就是Scope,它表示组件生效的范围。比如,这个组件负责一个Activity的注入,当这个Activity销毁时,它所关联的Module以及Molude中提供的实例,都会等待被销毁或者被销毁。
这里的@ActivityScope是我们自己定义的Scope。Dagger2只定义了一种Scope,就是@Singleton。Dagger2的约束中,要求“Component的dependices与Component自身的Scope不能相同”,并且“没有Scope的Component不能依赖有Scope的Component”。
上面的示例,因为AppComponent使用了Singleton,因此在SaveDataComponent我们需要使用一个不同的Scope,因此自定义了一个ActivityScope。
这里定义成其他的Scope的名称也是可以的。这个地方命名为ActivityScope只是一种推荐的实践。
Scope推荐实践主要用三种Singleton、ActivityScope 、UserScope。实现三种范围Scope的定义。Singleton用于Application,ActivityScope 用于Activity,UserScope用于其他,如class等。
8、Lazy与Provider
这两个工具是Dagger2两种特定的提供示例的方式。Lazy和Provider都是“懒加载”的实现方式,即在第一次获取实例时,才进行实例的创建。不同的是,Lazy后续获取对象都是同一个,而Provider每次调用都会调用Provider方法,是否返回同一个实例,根据Provider的具体实现来。
这个是Dagger2中比较好理解的内容。我们直接看一下下面的示例。
9、Dagger2注意事项总结
通过上面的介绍,我们看到Dagger2的使用相当的灵活,这也是导致它比较难用的原因之一。针对上面的案例,给出一些Dagger2使用中的注意事项,帮助大家更好的使用。有的在上面示例中没有体现的,请读者自行理解。
1)Inject注入没有“子类是父类”的继承关系。如果Component的Inject方法接收父类型参数,调用时传递的是子类类型,则无法注入。(编译可以通过)。
2)Component所关联的所有Module中不能有提供相同示例的Provide,需要用Named属性进行区分。
3)Module的Provide使用了Scope,则使用它的Component必须使用相同的注解。
4)Module的Provide没有使用Scope,与它相关的Component、Module可以加也可以不加Scope。
5)Component的dependencies与Component自身的Scope不能相同。
6)Singleton Scope修饰的组件不能依赖其他Scope组件,只能其他Scope组件依赖Singleton的组件。
7)没有Scope修饰的Component不能依赖有Scope修饰的Component。
8)一个Component不能有多个Scope。
三、Dagger主要实现原理
前面说到Dagger2是用注解的方式,简化了依赖注入的实现。它是基注解处理器生成的代码。
这一章节我们看一些简单场景下,注解处理器为我们生成的代码。理解一下Dagger2的实现原理。
看一下注解处理器生成的代码。这段代码主要在工程下的build/generated/ap_generated_sources目录下。
针对这个示例,它主要会生成DaggerUserComponent、MainActivity_MembersInjector、UserModule_ProviderUserFactory这三个类。看下他们的代码,看上去比较多,实际也不复杂。
DaggerUserComponent是核心的类,通过他的build方法,创建Module的实例。一手拿着MainActivity_MembersInjector,一手拿着UserModule_ProviderUserFactory。UserModule_ProviderUserFactory是提供实例的地方,MainActivity_MembersInjector是注入实例的位置。将从提供实例处获取的实例,注入到目标位置。
提供实例的地方,可以看到,就是调用了我们在Module中写的Provider方法返回一个实例。
注入的方式也很简单,直接调用”instance.user = user”,进行赋值。
单例注入,增加Singleton Scope修饰之后,DaggerUserComponent增加了initiallize方法,在这个方法中,会将Module实例放到Provider中。注入时候也从Provider获取进行注入,由此每次得到的实例都是同一个,即是单例。