Dagger2 上手指南
在 Java 的开发中,怎么利用各种设计模式和架构设计来解耦、提高拓展性,似乎已经成了一名 Java 开发人员的必修课。在一些大型系统中,还会有各种听起来高大上的名词,如 IOC、OSGI、CGLIB 等。所以这里我们就来讲讲 IOC 的一个实现框架——Dagger2。
IOC/DI
什么是 IOC(inverse of control)?
IOC 是一种面向对象编程中的一种设计原则,主要用来降低代码之间的耦合度。主要手段是通过 第三方工具 实现具有依赖关系代码之间的解耦。
IOC 名称的含义?
考虑两种情况:
- 类 A 需要类 B 的实例,A通过 new 关键字来创建实例,这时创建和使用都是 A 做主。
- 类 A 需要类 B 的实例,但是并没有主动使用 new 关键字,这时 A 只能被动接受外部传递给它的实例。
对比两种情况,第二种情况中 A 失去了对 B 的控制权,获取 B 的过程由主动变为被动。
什么是 DI(Dependency Injection)?
DI 就是将某个实例通过某种方式传递给需要这个实例的地方。
IOC 与 DI
- IOC 是一种工程思想
- DI 是一种设计模式,一种手段
Dagger2
latest version 2.13
Dagger2 是一个针对 Java 和 Android 的 静态、编译时运行 的依赖注入框架。因为是编译时运行,避免了在运行时使用反射所带来的性能开销,非常适合一些性能敏感的场景。
场景分析
首先看一下初学 Java 时会写出的代码:
1 | public class A{ |
1 | public class B{ |
这里有几个问题:
- A 对 B 的依赖程度非常高,当出现一个 NewB 的时候,需要修改 A 代码,不符合开闭原则
- 不利于单元测试,无法使用 mock B 对象
考虑上述两个问题,可以改造成这样:
1 | public class A{ |
1 | public abstract class BaseB{} |
这样,之前的两个问题都解决了。
现在再来思考一种情况:
假如 A 是有状态的,并且很多功能模块使用到了 A,那么会有很多地方存在创建 A 实例的动作,然后因为需求不同还需要不同的 BaseB 子类。如果这时候产经突然改需求,导致我们的的 A 又需要依赖 C ,按照上一步的思想,我们可能需要给 A 构造函数添加一个参数,这时候使用到了 A 的地方就都得修改,虽然不是什么难事,但是效率(投入产出比)十分之低。
Dagger2 的可以比较完美的解决这个问题。
核心注解
先来看几个 Dagger2 最主要的四个注解。
@Inject
这个不是 Dagger2 提供的注解,而是 java 依赖注入规范提供的注解。Inject
可以用在 成员属性、构造方法、成员方法 上,对应于每一种情况都会有不同的作用,这里用表格来归纳下:
使用对象 | 效果 |
---|---|
构造器 | 表示可以使用这个构造器来对外提供实例 |
成员属性 | 表示这个属性需要被注入 |
方法 | 表示这个方法在依赖注入的最后阶段会被调用 |
这里有几点需要注意:
- 注入优先级: 构造器 > 成员属性 > 方法
- 只能有一个构造器被
@Inject
标注,访问权限不能是private
- 成员属性 被注入的顺序不能保证
- 方法 被调用的顺序不能保证
@Module
被 @Module
标注的类表明这个类会参与 依赖图 的构建,最主要的功能是 提供 依赖,提供依赖的方法(可以是静态方法)被 @Provides
标注。这些方法本身可能也需要别的依赖,这时 Dagger2 就会寻找满足这个方法的依赖,找不到就编译失败。
@Provides
配合 @Module
使用在方法上,表明这个方法会参与 依赖树 的构建。
要注意方法的访问权限
@Component
上面提到了两个代表:依赖需求方 和 依赖供给方,但是它们之间没有直接的联系,怎么把依赖交到 依赖需求方 的手上还是个问题。@Component
就是它们之间的桥梁,一般会使用 @Component
来标注一个 接口,然后指定 @Component
的 modules
属性为定义好的 Module
(依赖供给方),最后声明一个包含 依赖需求方 参数类型的函数。
这样,联系就建立好了,构建一下工程,Dagger2 的注解处理器就会帮我们生成一个 DaggerXXXX
的类,通过它我们就能完成依赖注入工作了。
实例练习
刚刚讲解完几个概念,现在来看看在它们的联系在代码中的体现:
为了便于表达,这里使用 MVP 来演示
声明 Inject
首先得确定哪些地方需要依赖注入(即依赖需求方):
1 | public class MainActivity extends AppCompatActivity implements MainContract.MainView { |
注意这里 presenter
的类型声明是 MainContract.BaseMainPresenter
,这是一个抽象类。
再看一下 MainContract.BaseMainPresenter
的实现类:
1 | public class MainPresenter extends MainContract.BaseMainPresenter { |
按照之前的理解,这里可以使用 @Inject
来标注 MainPresenter
的构造器,但是这里没有,而且也不行,因为 Dagger2 是静态解析的,所以我们通过 Module 来提供它的实例。
声明 Module
定义好了 依赖需求方,还差供给方没有定义:
1 |
|
这里有几个点说明一下:
- 被
@Provides
标注的方法一般命名为provideXXX
- 返回值类型一定要和
@Inject
标注的类型一致,如MainContract.BaseMainPresenter
,否则编译失败 - 在方法内部直接构建实例。这也是我个人比较喜欢的方式,因为这样就不需要在
MainPresenter
那边添加啥东西,配置过程都集中到 Module 里面。 再考虑另一种情况,当使用第三方库的时候,一般是没办法在它的构造器上加注解的,也只能在 Module 里面创建实例,保持风格统一总是没错的。
声明 Component
还需要 Component 来建立上面两者之间的联系:
1 |
|
这个方法的名称可以随意,为了便于理解还是建议使用类似 injectXXX
的命名方式。
编译之后,Dagger2 会生成一个 DaggerMainComponent
实现 MainComponent
,通过这个类就能完成注入了
完成注入
直接看代码吧:
1 | public class MainActivity extends AppCompatActivity implements MainContract.MainView { |
在使用到 presenter 之前进行注入,官方说是要在 super.onCreate(savedInstanceState)
之前注入,不过还是觉得在使用之前注入合理点。
这样就完成了一个比较简单的依赖注入实践。
高级功能
除了最基本的注入之外,Dagger2 还提供了一些其他的满足我们需求的功能,比如说单例、懒加载等。
@Scope
这个用在注解上的,因为它本身没有具体定义,只是提供了一个规范。它表明一个依赖的使用规则,可以看做生命周期或者作用域(我更喜欢用生命周期表述),如我在注入 MainPresenter
的时候,是注入同一个实例,还是每执行一次 inject()
就创建一个新的 MainPresenter
,这个需求还是挺常见的。
体现 Scope 的注解有:@Singleton
和 @Resuable
,下面具体说说它们的作用。
@Singleton
向 Dagger2 声明,每次都使用同一个实例进行注入。因为 Dagger2 为 Component 和 依赖
在依赖图中建立联系,所以需要保持它们两个的 Scope 是相同的,比如我在 provideMainPresenter()
上使用了 @Singleton
,那么在 MainComponent
上也要使用 @Singleton
来保持一致。
这样,通过 同一个 DaggerMainComponent
实例获取的 presenter
就都是一样的了。请注意 “同一个” 这个修饰词,如果我们连续执行 DaggerMainComponent.create().inject(this)
两次,那么每次实例还是不一样的,只有这样:
1 | DaggerMainComponnet component = DaggerMainComponent.create(); |
注入的
presenter
才是同一个实例。所以我们不能够指望
@Singleton
能够创建全局单例,而且它也没有处理并发的情况。@Resuable
可重用,表示希望能够重用之前创建的实例,不过也可能还是会创建出新的实例,一般用在类声明上。不过我似乎并没看出使用和不使用有什么区别,所以也就不多嘴了。
Lazy
懒加载,只有第一次使用的时候才会真正的去创建实例。它的适用场景还是比较容易想到,比如有一些开销比较大的对象,但是又不一定会马上用到,甚至不会用上,那么只有真正用上的时候再创建是是再好不过的了。比如要懒加载 presenter
,就可以这样写:
1 |
|
Provider
有时候可能需要很多的实例,而不是一次注入就一直是这么一个实例了。这种需求非常适合 stateful 实例,能够保证每次都能拿到一个全新的实例,不会受之前的操作影响。代码实现是这样子的:
1 | Provider<MainContract.BaseMainPresenter> presenterProvider; |
这个例子不太恰当,不过懂大概意思就好了。
@Qualifier
Dagger2 是静态解析的,直接通过类型来进行辨别依赖关系,但是有时候光凭类型还不能确定一个依赖,那么就需要为 Dagger2 提供额外的信息,Dagger2 使用 @Named
来完成这种额外约束。下面演示一下这种需求场景的代码,还是继续改造我们的 MainPresenter
:
1 | public class MainPresenter extends MainContract.BaseMainPresenter { |
这时候 MainPresenter
多了两个 Repo 依赖,那么 MainModule
也需要做相应的更改:
1 |
|
这样看着没问题,不过 Dagger2 不认,它不知道你方法名字有啥意思,也不知道你方法内部怎么的。
所以我们需要使用 @Named
来帮助 Dagger2 判断:
1 |
|
这样就 OK 了。
这里例子也不是很恰当,Repo 应该有一个良好的封装,Presenter 是不应该知道有 Remote 和 Local 的区别的
@BindsInstance
用在 Component
或 SubComponent
的 Builder
的方法上,它允许我们在构建 DaggerXXXX
的时候主动提供依赖到依赖树中。还是用例子来说明,MainPresenter
能够通过 loadMsg()
来获取一条消息,但是按照常理,我们还得告诉它一个 address
,不能让它 xjb 找,所以还得改改:
1 | public class MainPresenter extends MainContract.BaseMainPresenter { |
MainModule
中再加个参数:
1 |
|
这样就可以了!
这样还可以把 Activity 的实例绑定上去,只要要注意下内存泄漏问题。
也可以通过手动创建
MainModule
实例来完成一些依赖的传递。
到这里,一些基本的、常用的方法就讲得差不多,更多的还是要自己踩坑,经验这东西也不好说。
Dagger2 的使用方式非常像配置文件,把依赖的构建过程从具体的业务逻辑或其它逻辑中抽离出来,使业务代码变得清晰,然后拓展性、可测试性也提高了。
如果是 Android 开发的话,可能带来的问题就是方法数增多,不过也不算大问题,毕竟还有 multidex,如果使用了热修复、插件化等技术的话,可能还需要折腾下。
完整代码在 这里
关于 Dagger2 一些更高级的用法,我们下一篇文章再交流(再不复习怕是要挂了)