开源代码分析之Dagger2
1. 在项目中引入Dagger2
Dagger2 搞Android开发以及Java开发的同学估计即使没有使用过也应该听说过这个开源库吧。它是一个依赖注入库.源码地址如下所示:
Dagger2 github地址
在项目中引入Dagger2
project的build.gradle添加
dependencies { |
module的build.gradle添加
// 添加其他插件 |
2. Dagger2简介
在介绍如何使用Dagger2之前我们必须先搞清楚一件事–为甚么需要使用Dagger2,也就是Dagger2的功能,刚刚提到了Dagger2 是一个依赖注入框架,那么什么是依赖注入呢?为什么要使用依赖注入呢?
我们传统的开发过程中如果某个类中需要一个依赖可以通过在这个类中创建需要的依赖,这样的缺点显而易见,比方我们需要在一个已经开发成熟的项目中更换某个类的实现,再具体点,比如我们在
现有项目中使用的是Picasso作为图像处理框架,但是某天项目中需要显示gif,这时候我们可能考虑到Glide库能够支持Gif播放,所以我们想要将图像处理框架更换为Glide
如果原先没有设计好的话可能改动的代码就很庞大。而且极为容易出错。但是如果使用了依赖注入框架,那么这个问题就变得很简单了,只需要修改对应的Module就可以了,对代码不需要大幅的改动。(这里的前提条件是这两个库接口上是相似的)
那么什么是依赖注入呢?这里我就谈谈自己对依赖注入的理解:
依赖注入就是将使用依赖的代码部分与依赖生成的部分分开,这样做的好处就是:因为对象是在一个独立、不耦合的地方初始化,所以当注入抽象方法的时候,我们只需要修改对象的实现方法,而不用大改代码库。并且可以通过注入这些依赖的Mock对象来进行模拟测试。从而使得对项目的测试更加方便。
有Dagger2就说明有Dagger的存在,关于这段历史大家如果感兴趣的话可以去网上了解下,这里就不展开介绍了,Dagger2的最大改进就是,它使用了Java注解处理器,完全去除了反射机制,在编译的时候检查并分析依赖关系。使得在效率上得到很大的提升。
3. Dagger2结构
下面是Dagger2的大致结构:
整个依赖注入体系分成三个部分:
- 依赖提供方: 用于生成并提供依赖对象的一方。
- 依赖需求方: 需要使用注入依赖的一方
- 依赖注入器: 连接依赖提供方和依赖需求方的注入器。它负责将依赖提供方生成的依赖对象注入到依赖需求方。换个角度来说就是依赖需求方需要注入依赖的时候可以顺着这个注入器找到依赖提供方。
4. Dagger2 重要的注解
了解了大体的结构后我们就需要了解下Dagger2中常用的一些注解,其实Dagger2的注解并不太多,但是需要注意的是这些注解的理解。
@Inject:
我们看到上图中依赖注入器左右两端各有一个@Inject注解也就是说@Inject既可以用在提供方也可以用在依赖需求方,用在依赖提供方的时候一般用来注解待注入对象的构造函数,用在依赖需求方的时候一般用来注解需要Dagger2进行依赖注入的成员变量。
@Module:
我们看到依赖提供方还有个@Module注解,它的作用是什么呢?我们知道提供方已经有了一个@Inject为什么还需要@Module呢?我们考虑一个情景我们现有项目中使用了第三方的类库,在不采用导入第三方类库源码或者源代码非开源的情况,如果用Inject要怎么处理,根本不可能使用Inject注解加入这些类中是吧,那这还怎么办呢?这时候就需要@Module出场了。Modules类是由一系列专门提供依赖的方法组成,所以我们定义一个类,用@Module注解,这样Dagger在构造类的实例的时候,就知道从哪里去找到需要的依赖。
那么这两种方式有没优先级区分呢?
有的,它的查找规则如下:
- 首先会先从Module的@Provides方法集合中查找
- 如果查找不到,则查找成员变量类型是否有@Inject构造方法。
也就是说@Module中的优先级会比@Inject注解的构造方法优先级更高,还有个需要注意的是@Modules不仅仅只用于那些第三方项目中不可见源码的对象注入,可以使用@Inject注入的对象,使用@Module一样可以注入。
@Provide
提到@Module就不得不提到 @Provide,我们用这个注解来告诉Dagger2被这个注解的方法是被用来提供依赖的,具体提供哪种依赖对象是由返回值决定的,一般这类方法规定以provide作为开头,后面的可以随意。Module中@Provides方法可以是带输入参数的方法,其参数由Module集合中的其他@Provides方法提供,或者自动调用构造方法,也就是说如果找不到@Provides方法提供对应参数的对象,Dagger2就会自动调用带@Inject参数的构造方法生成相应对象。
下面是一个最基本的@Module的写法
|
@Named
如果待注入方需要依赖同个类的两种不同的对象的时候,那要怎么办,我们可能会想就写两个@Provides方法,而且这两个@Provides方法都是返回需要的类型,但是我们前面提到过Dagger2是靠返回值的类型来判断具体选择哪个@Provide方法来提供依赖的,现在有两个provide方法返回同一个类型,那就比较尴尬了,这种现象也有专门的叫法叫做注入迷失,为了解决这个问题这就需要使用@Named来进行区分了:
|
在待注入方也要使用@Named来标记到底使用的是哪个依赖,具体的待注入方以及注入会在下面进行介绍。
@Named("typeA") //添加标记@Name("typeA"),只获取对应的@Name("typeA")的依赖 @Inject |
@Qualifier
上面的方式只能使用字符串作为区分标签,一般来说是够用的,但是如果你需要其他的方式作为区分标签可以使用Qualifier进行定义了:
@Qualifier //必须,表示IntNamed是用来做区分用途 |
用法和@Named注解类似就不展开介绍了。
@Component:
Components从根本上来说就是一个注入器,也可以说是@Inject和@Module的桥梁,它的主要作用就是连接这两个部分。它注释的类必须是接口或抽象类。
既然它是注入器,必定由两个部分构成,一个是提供方,一个是需求方,提供方是由module引入,需求方是由inject方法引入。Component的职责就是在inject目标中有使用@Inject
注解的成员变量的时候顺着 Component 所管理的 Module中进行查找需要的依赖,但是如果不需要@ Module那么就不需要定义Component了,也就是说Component 是用于管理 @Module的,可以通过Component中的modules属性把Module加入Component,modules可以加入多个Module。
这样Component获取依赖时候会自动从多个Module中查找获取,需要注意的是Module间不能有重复方法,不然也会照成上面所提到的依赖迷失。
添加多个module有两种方法
1. @Component(modules={××××,×××}) |
假设ComponentA依赖ComponentB,B必须定义带返回值的方法来提供A缺少的依赖
ComponentA依赖ComponentB的代码如下
//定义ComponentB |
//定义ComponentA |
这样,当使用ComponentA注入Container时,如果找不到对应的依赖,就会到ComponentB中查找。但是,ComponentB必须显式把这些A找不到的依赖提供给A。怎么提供呢,只需要在ComponentB中添加方法即可,如下
@Component(modules={××××××××}) |
Component 当中定义的方法可以分成两类:
- 注入的目标对象以injectXXX作为方法名开始, 同一个Component可以有多个inject方法,也即是说可以注入到多个目标对象。注意inject的参数不能是父类,必须是你注入的那个类,因为这里写啥,Dagger就回去对应的类中寻找@Inject注解进行注入
- 需要暴露给依赖components的方法,如果不在这里列出那么使用dependencies方式的时候就不会暴露出来。
下面是一个最基本的Component 定义方法
//指明Component在哪些Module中查找依赖 |
@Component的注入
Component注入有两种方式:
- 基本方式
public Container{ |
上面简单例子中,当调用DaggerFruitComponent.create()实际上等价于DaggerFruitComponent.builder().build()。在构建的过程中,默认使用Module无参构造器产生实例。
如果需要传入特定的Module实例,可以使用
DaggerFruitComponent.builder() |
如果Module只有有参构造器,则必须显式传入Module实例。
这里还留有一个问题等到讲 Component 依赖以及子Component的时候讲,那就是在Component依赖以及子Component的情况下怎么进行依赖注入。
@Scope && @Singleton
在学Dagger2的时候最难理解的部分就是@Scope 以及 Component依赖,子Component.还有就是如何在项目中组织Component。
我们接下来先来看下@Scope的作用,在不使用@Scope 的时候我们的例子如下,我们注入到MainActivity后将这两个对象打印出来,
public class Apple {
}
|
|
@Inject |
打印出来的结果如下:
07-18 20:24:15.355 3059-3059/com.idealist.tbfungeek.mvpframework I/MainActivity: class -> MainActivity method -> onCreate() line -> 29 [ Message ] com.idealist.tbfungeek.mvpframework.Apple@3dd96071 |
接着我们再做个对比实验:
@Scope |
@Module |
@FruidScope |
@FruidScope |
结果如下:
07-18 20:27:36.364 5818-5818/? V/ActivityLifeCycleManager$1: class -> ActivityLifeCycleManager$1 method -> onActivityCreated() line -> 61 [ Message ] onCreate --> MainActivity |
发现了什么没?我们在没有用scope注解的时候两个对象实际是不同的两个对象,但是如果用scope注解标记后两个返回的是同一个对象。
最早看到Singleton注解的时候我第一反应就是只要用上这个注解就可以实现单例模式了,但是它并非我们通常以为的单例,Java中,单例通常保存在一个静态域中,这样的单例往往要等到虚拟机关闭时候,该单例所占用的资源才释放。但是,Dagger通过Singleton创建出来的单例并不保持在静态域上,而是保留在Component实例中。也就是这种单例只是针对对应的Component。如果要实现传统意义上的单例模式,那么就需要通过一定的方法保证对应的Component是全局单例的。
下面是来自网络上的一个很经典的例子,估计看过后大家一定会豁然开朗:
在实际开发中我们可能还需要一种局部单例的控件(这个应该是更常用),比如说我们有三个Activity,MainActivity,BActivity和CActivity,我们想让MainActivity和BActivity共享同一个实例,而让CActivity获取另外一个实例,这又该怎么实现呢?在Dagger2中,我们可以通过自定义Scope来实现局部单例。那就动手吧:
首先让我们先来定义一个局部作用域:
@Scope |
然后在我们的UserModule和ActivityComponent中应用该局部作用域:
@Module |
@UserScope |
大家注意,我的ActivityComponent作为一个注入器只可以向MainActivity和BActivity两个Activity中注入依赖,不可以向CActivity中注入依赖。最后,要让该局部作用域产生单例效果,需要我们在自定义的Appliation类中来初始化这个Component,如下:
public class MyApp extends Application { |
接下来我们在MainActivity和BActivity中注入依赖,MainActivity如下:
@Inject |
BActivity如下:
@Inject |
那么如果我还想在CActivity中使用User对象该怎么办呢?再来一个CUserModule和CActivityComponent呗!
CUserModule如下:
|
这里我没有再注明单例了哦!
CActivityComponent如下:
|
在CActivity中注入依赖:
@Inject |
大家看到,MainActivity和BActivity是同一个实例,而CActivity则是另外一个实例。
同时还需要注意一点就是:一个@Module和component中可以有多个scope对象。这些scope将一个component划分成多个不同的区域:
|
@FruidScope1 |
@FruidScope |
输出结果:
07-18 21:09:07.245 5676-5676/? I/MainActivity: class -> MainActivity method -> onCreate() line -> 36 [ Message ] com.idealist.tbfungeek.mvpframework.Apple@3dd96071 |
在使用scope的时候我们还需要注意如下两点:
编译器会检查 Component管理的Modules,若发现标注Component的自定义Scope注解与Modules中的标注创建类实例方法的注解不一样,就会报错。所以Component和Modules中的scope必须匹配。
如果两个Component间有依赖关系,那么它们不能使用相同的Scope。
@Subcomponent && dependencies
如果一个Component的功能不能满足你的需求,我们需要对它进行拓展,这时候有两种方法
- 使用Component(dependencies=××.classs)
- 使用@Subcomponent,Subcomponent用于拓展原有component。这时候注意子component同时具备两种不同生命周期的scope。子Component具备了父Component拥有的Scope,也具备了自己的Scope。
那么它们的不同之处在哪里呢?@Component 只能获取到依赖的 Component 所暴露出来的对象,而 @Subcomponent 则可以获取到父类所有的对象。
Subcomponent其功能效果优点类似component的dependencies。但是使用@Subcomponent不需要在父component中显式添加子component需要用到的对象,只需要添加返回子Component的方法即可,子Component能自动在父Component中查找缺失的依赖。
|
通过Subcomponent,子Component就好像同时拥有两种Scope,当注入的元素来自父Component的Module,则这些元素会缓存在父Component,当注入的元素来自子Component的Module,则这些元素会缓存在子Component中。
5. 参考文章
下面是较好的文章,如果看了该博客还是不大明白可以通过下面的文章来进一步阅读