最近在项目中使用了 Dagger2 这个依赖注入的框架,在这里记录一下。第一次写技术文章,不足之处请多指教。不过真的是写出来才发现还是有很多不懂的地方。
介绍
- 什么是 Dagger2
下面这段是官网的简介:
Dagger is a fully static, compile-time dependency injection framework for both Java and Android.
翻译过来就是Dagger2是Java 和 Android 的一个编译时生成代码的依赖注入框架,如果你没有了解过依赖注入,可能会感到不理解,什么是依赖注入,为什么要使用依赖注入,使用依赖注入有什么优点,下面来简单的说一下。
前置知识
- 什么是依赖注入 Dependency Injection
我们在实现一个功能的时候,往往需要创建很多对象,举个例子,如果要实现一个Computer,需要几个类:MotherBoard, Cpu, Computer。
在没有依赖注入的时候,首先按顺序构建每个对象,先 new 一个 Cpu,然后 new 一个 MotherBoard,再用这两个对象 new 一个 Computer,当逻辑简单的时候,这不会有什么问题,但是实际开发中要构建的对象要远远比这要复杂的多,如果某个人修改了其中一个类的 Constructor,就要修改每个使用 Constructor 的地方,如果你的创建所有对象的代码有成百上千行,那就很麻烦了。
使用依赖注入后,通过依赖注入的框架创建这些对象,不需要自己去维护依赖关系,你只需要去维护你一个依赖的配置,这就要简单的多了。依赖注入的另一个好处,是对单元测试很友好,因为每个对象的创建并不依赖于特定的逻辑,在写单元测试的时候,某些类的功能并不需要,或者无法在测试场景使用,那么只需要替换掉就可以了。在开发过程中,使用依赖注入也对多人并行开发有一定的好处,如上面的例子,可以一个人开发 MotherBoard,一个人开发 Cpu,一个人开发 Computer,每个人都不需要关心其他人的对象要如何创建,因为每个对象的创建都是由依赖注入框架去维护的。
说到依赖注入的同时,一定会提起控制反转(IOC),控制反转是一种设计思路,意思就是说,原本我要用什么,是我自己创建,自己用,现在是我要用什么,我就管别人要,别人给什么用什么。这就又不得不提到一个软件设计中的重要思想,依赖倒置原则。
那么什么是依赖倒置原则?
举个例子,我们设计一台电脑,先设计 cpu,再设计主板,最后设计机箱,这就存在一个依赖关系,主板依赖于 cpu的尺寸,机箱依赖于主板的尺寸,看起来是没什么问题,现在我们要改一下设计,把 cpu 的尺寸做小一圈,这就蛋疼了,因为主板依赖于 cpu 的尺寸,主板需要重新设计,机箱依赖主板的尺寸,机箱也要重新设计,整个的设计都因此而发生了变化。
现在我们换一种思路,首先设计电脑整体的模型,据此设计机箱的结构,再根据机箱的结构设计主板,根据主板的结构去设计 cpu,现在要对 cpu 的进行修改,就只需要修改 cpu 自己就行了,并不会出现上面那种全部推倒重来的现象。
这就是依赖倒置原则,将原本由下至上的依赖关系翻转过来,变为有上至下,上层决定下层功能,下层的修改对上层无任何影响,避免出现牵一发而动全身的现象。此时,上层与下层的依赖是依赖抽象,而非依赖实现,这种方式能对上下层之间进行解耦,也能达到最大程度的复用。控制反转即是依赖倒置原则的一种设计思路,而依赖注入则是这种思路的实现方式。
关于如何设计抽象关系,通常应当遵循面向软件设计的5大原则(SOLID),依赖倒置原则也是其中之一,这里就不多说了,贴一下 wiki 上的描述把,有兴趣的话可以多了解一些。
S
Single responsibility principle
a class should have only a single responsibility (i.e. changes to only one part of the software's specification should be able to affect the specification of the class)
O
Open/closed principle
“software entities … should be open for extension, but closed for modification.”
L
Liskov substitution principle
“objects in a program should be replaceable with instances of their subtypes without altering the correctness of that program.” See also design by contract.
I
Interface segregation principle
“many client-specific interfaces are better than one general-purpose interface.”
D
Dependency inversion principle
one should “depend upon abstractions, [not] concretions.”
这里简单的总结一下,个人理解,软件设计中的各种设计模式,原则,归根结底,目的都是为了让你的代码更容易阅读,扩展和维护,人不是机器,无法从数不清的代码中快速找到自己需要的内容,也无法在快速滚动的代码中定位问题,而且一切都依赖于抽象,各种设计模式,原则,本质上都是各种形式抽象,unix 中有一句话经常提到,keep it simple,stupid,也是同样的道理。依赖注入便是其中的一种实现方式。
- 常见注入方式
Java有很多依赖注入的框架,常见的有 Spring, Guice, Dagger1, Dagger2等。早期的注入方式是通过 xml 进行配置(Spring),xml注入的方式有很多缺点,比如说无法进行代码检查。在 Java5 出了之后,注解提供了另一个更便捷的方式,Spring 和 Guice 等框架支持了注解的方式,在JSR 330标准出现之后,更是有了统一的官方标准,使用注解进行注入的好处是配置不容易出错,因为代码能编译过,就说明写的没问题,但是此类注入都是通过修改编译后生成的字节码,或通过 JDK 提供的动态代理(Proxy),反射等方式实现,修改后的字节码无法进行 debug,运行时出错很难排查。
Dagger2提供了另一种选择,与其他依赖注入框架不同的是,Dagger2的注入方式是在编译时生成代码,通过 JDK 提供的 Annotation Processor Tool功能,在移动终端这种性能有限的环境下,生成代码的方式更合适,性能也更好。但是也存在一些局限性,例如不支持任何动态注入,因为代码在编译后就已经固定了,会增加编译时间等。
以上内容参考了这个视频,有兴趣的话可以看一下)
- Annotation Processor
上面提到了注解处理器,这里简单的说一下,注解处理器是 Java5之后提供的一种在编译时动态生成代码的功能,通过读取源码中使用注解标记的位置,使用自己的逻辑动态生成代码。需要注意的是,生成代码的时机是在编译之前,在 Android的基于 Gradle 的编译环境下,每个 Module 都需要指定自己的注解处理器。
Dagger2基本使用
现在开始说具体的使用,Dagger2使用JSR 330标准的API,和自己定义的一些注解,通过几个简单的注解和接口,就能进行注入操作。
首先要说明的是整体的一个思路,要记住的就是 No Magic,依赖注入从根本上来说,就是创建一个对象并给某个变量赋值,Dagger2的本质就注解处理器在编译时生成代码,实际上就是对你所需要的变量进行赋值,而想要正确的给这个变量进行赋值,需要两点:
- 要创建哪个对象
- 如何创建这个对象
第一点,首先要找到要创建哪个对象,Dagger2在匹配的时候通过类型和 Qualifier 注解来定位一个对象,所以不要在写了两个相同类型的注入并且编译不通过的时候感到奇怪。
第二点,所有的注入对象,框架都要知道如何去创建它,以及它所依赖的其他所有对象,编译不通过的时候,第一件事就是看报错信息, Dagger2的报错信息在大部分情况下都很详细,明确的指出了到底是缺少配置,还是配置错误,或者其他原因,框架也不能把一个对象变出来,如果你都不知道这个对象是怎么创建的,框架又怎么能知道呢?
看到这里可能比较疑惑,不着急,先往下看。
这里有几个关键性的注解:
- @Inject
- @Provides
- @Module
- @Component
先来看一个例子,最简单的注入:
还记得上面的两点么,第一点,告诉框架如何创建对象
public class MotherBoard {
private Cpu mCpu;
@Inject
public MotherBoard(Cpu cpu) {
mCpu = cpu;
}
...
}
第二点,告诉框架我要什么对象
public class Computer {
@Inject
MotherBoard motherBoard;
...
}
这样在 Computer 对象中,就能获取到 MotherBoard 了。
如果你仔细看懂了上面的内容,就会发现这段代码并不能正确编译,因为框架不知道如何创建 Cpu 对象,这里来说明另一种告诉框架如何创建某个对象的方式:
@Provides
public static Cpu provideCpu() {
return new Cpu();
}
那么这段代码要在哪里执行呢?下面来介绍另一个重要注解@Module
在Dagger2中,所有用@Provides
注解的方法,都属于一个 Module
,其实就是一个类上面写@Module
@Module
class ComputerModule {
@Provides
static Cpu provideCpu() {
return new Cpu();
}
...
}
OK,现在完事齐全,只差注入了,没有这最后一步,前面的一切都是没用的。那么到底要怎么注入呢?现在需要另一个注解@Component
在 Dagger2 中,有一个 graph 的概念,其实每个依赖注入框架都有类似的概念,要注入的对象,及其所有依赖的对象之间,必然存在一个完整的依赖关系图,在注入之前,必须要能完整的构建出这个依赖关系图。Component 注解标记的类代表了一个完整的依赖关系的终点。当然这一切都是框架去做的,你所需要的就是正确的配置出这个依赖关系。
@Component(modules = {
ComputerModule.class
})
public interface ComputerComponent {
void inject(Computer computer);
}
编译后,dagger2会生成一个ComputerComponent 接口的实现类,类名为 Dagger + Component 名字,调用其中的方法就可以对 Computer 进行注入了。
Computer computer = new Computer();
DaggerComputerComponent.builder().build()
.inject(computer);
如果你连 Computer 都不想自己创造,也可以这么写:
// Computer
public class Computer {
MotherBoard motherBoard;
@Inject
Computer(MotherBoard motherBoard) {
this.motherBoard = motherBoard;
}
...
}
// Component
public interface ComputerComponent {
Computer make();
}
看到这里可能会有疑问,这写 Component,Module,Provide 的类和方法,为什么都叫这样的名字呢?在上面注入相关的方法,类名,及后面会说到的相关注入功能,方法名类名均与注入过程无关,起什么名字只是为了方便阅读,方便阅读也是很重要的。
最简单的说完了,下面来说一下各种不同场景的使用。
你可能已经注意到了上面的DaggerComputerComponent是带 Builder 的,那这个 Builder 是做什么的呢?
Provides 方法可以是static
的,也可以不是,如果不是,那么就需要一个 Module 对象来调用这个方法,这时候Component 中的 Builder 就有用了。
修改一下就会变成这样:
@Module
class ComputerModule {
@Provides
Cpu provideCpu() {
return new Cpu();
}
}
// Inject
DaggerComputerComponent.builder()
.computerModule(new ComputerModule())
.build()
.inject(computer);
有的时候你可能想用接口来注入
public interface Computer {
}
public class MyComputer implements Computer {
private MotherBoard mMotherBoard;
@Inject
MyComputer(MotherBoard motherBoard) {
mMotherBoard = motherBoard;
}
}
// Module
@Provides
Computer provideComputer(MyComputer myComputer) {
return myComputer;
}
这种情况下,如果你的 Module 里都是上面那样,也可以写成这样
@Module
abstract class ComputerModule {
@Provides
static Cpu provideCpu() {
return new Cpu();
}
@Binds
abstract Computer provideComputer(MyComputer myComputer);
}
这里使用一个新的注解@Binds
,这个注解和@Provides
的功能是相同的,区别在于,如果你的 Provide 方法入参与返回值相同,仅仅是做了类型转换,那么可以省略这个方法调用,以一个 abstract 方法代替,在注入时会直接使用传入的参数,如果你去看生成的代码,就会发现实际并没有调用你写的这个 abstract 方法,而是直接使用了入参的变量,减少了一个方法调用也是可以节约一些性能的,在Android这种性能有限的平台上,还是很有意义的。
Scope
只要是依赖注入,一定会有一个 Scope 的概念,通常代表了注入对象的作用域。在 Dagger2,也就代表了这个注入的对象的生命周期。
一个常见的 Scope 是 @Singleton
,在可注入的类的类名上,或者 @Provides 标记的方法上使用这个注解,表示改对象是单例的。使用的时候需要注意,如果使用Scope,对应的 Component 也要使用此注解进行标记。仅仅一个@Singleton 注解实际上并不能实现单例,单例的实现是依赖于 Component 的,从同一个 Component 对象进行注入,才是单例,如果你创建了两个 Component用来注入一个单例,仍然会产生两个不同的对象,如果打开生成的代码进行查看,会有更深刻的印象。
同样也可以自己定义 Scope
@Documented
@Retention(RUNTIME)
@CanReleaseReferences
@Scope
public @interface MyScope {}
事实上,自己定义的 scope,与@Singleton 并没有什么区别,因为Scope 的实现都是基于Component,在同一个 Component 对象中,标记了 Scope 的注入对象,都只会注入同一个对象,相当于局部的单例,主要的目的是实现清晰的结构。
实际使用中可能还会有这样的场景,某些情况下不需要这个Component 中再保存单例对象,这时候可以将其释放掉,仔细看上面的自定义 Scope,有一个@CanReleaseReferences
注解,使用这个注解就可以额外注入一个ReleasableReferenceManager
对象来对此进行操作。h'n
// Module
@MyScope
@Provides
static Computer provideMyComputer(MotherBoard motherBoard) {
return new MyComputer(motherBoard);
}
// Component
@Inject @ForReleasableReferences(MyScope.class)
ReleasableReferenceManager releasableReferenceManager;
ReleasableReferenceManager
有两个方法,releaseStrongReferences的作用是把保存的对象转移到一个 WeakReference 中,众所周知WeakReference中的对象可以被回收,另一个方法restoreStrongReferences则可以把前面的 WeakReference 中的还没有被回收的对象变回到强引用状态。
关于ForReleasableReferences官方的例子是在内存不足时释放对象,但是实际应用中并未发现实际的实用场景,待补充。
关于 Scope,还有一个@Reusable 注解,官方的解释是针对某些特殊的可以随便重复使用的对象,实际操作没有发现什么区别,也没有想到什么使用场景,待补充。
延迟注入
有的时候注入的对象并不想在注入的时候创建,而是在需要的时候自己去创建
@Inject
Lazy motherBoard;
// Inject
motherBoard.get();
多次注入
如果需要多次获取不同的对象,如下代码可以获取10次主板对象,每次的对象都是一个新的主板。
Provider motherBoard;
// Inject
for (int i = 0; i < 10; i++)
motherBoard.get();
Qualifier
前面说到Dagger2如何定位需要注入的对象,首先是通过类型,但是如果需要多个相同类型的要注入怎么办呢?这时候就需要Qualifier 注解了。
在@Provides 或@Inject 的地方使用@Named 注解,可以指定一个名字用来匹配要注入的对象。
@Provides
@Named("MyComputer")
static Computer provideComputer() {
return new MyConputer();
}
@Provides
@Named("HisComputer")
static Computer provideComputer() {
return new HisConputer();
}
// Component
public interface ComputerComponent {
@Named("MyComputer")
Computer make();
@Named("HisComputer")
Computer make();
}
@Named注解使用一个字符串来给每个注入对象增加一个名字,如果在很多地方使用,比如说多个 Module 中注入不同的对象,就不是很好定位,可以自己定义一个用@Qualifier
标记的注解,代替上面的@Named注解
@Qualifier
@Documented
@Retention(SOURCE)
public @interface MyComputer {
}
可选绑定
如果某个依赖对象允许不存在,可以在 Module 里使用@BindsOptionalOf
注解,这样其他的需要依赖此对象的地方,都可以写一个 Optional
例如我现在设计了一种神奇的电脑可以没有主板:
public class MotherBoard {
private Cpu mCpu;
MotherBoard(Cpu cpu) {
mCpu = cpu;
}
}
public class MyComputer implements Computer {
private MotherBoard mMotherBoard;
@Inject
MyComputer(Optional motherBoard) {
...
}
}
// Module
@BindsOptionalOf
abstract MotherBoard optionalMotherBoard();
现在即使不提供 MotherBoard 对象的注入,也不会报错。
在一个 Module 中使用@BindsOptionalOf后,同样的 Module 不允许使用@Provides 提供相同的注入对象,并且不能有使用@Inject 的 Constructor。当有多个 Module 提供不同的依赖关系时,@BindsOptionalOf才有意义。
现在我修改了设计,又想要主板了,于是单独写一个MotherBoardModule用来提供主板:
@Module
class MotherBoardModule {
@Provides
static MotherBoard provideMotherBoard(Cpu cpu) {
return new MotherBoard(cpu);
}
}
又可以把主板注入到 Computer 中了。
BindsInstance
有时,Component 中需要注入的对象可能运行时才创建,这是可以使用@BindsInstance注解,给 Component 传入一个可注入的对象。比如说现在我的电脑里的 Cpu 改为使用其他厂家的,不自己生产了,那么我只需要在构建 Component 的时候作为一个参数传进去就行了。
public class Cpu {
public Cpu() {
}
}
// Component
@Component.Builder
interface Builder {
@BindsInstance
Builder useCpu(Cpu cpu);
ComputerComponent build();
}
// Inject
ComputerComponent computerComponent = DaggerComputerComponent.builder()
.useCpu(new Cpu())
.build();
需要注意这里传入的参数不能为null,如果可能为 null,需要使用@Nullable 注解。
Dagger2进阶功能
- @Subcomponent
- Set, Map
前面的注入例子都是单个的对象,Dagger2还支持把多个单独的对象注入到集合(Set,Map)中,这块很简单,大家自己去看文档把。
这里主要说一下另一个比较有用的功能,@Subcomponent
从名字就看出来,Subcomponent 有着和 Component 类似的功能,事实上,他们的功能基本相同。Subcomponent 的主要用法是对某些子模块的注入功能进行封装,隐藏内部实现细节。
Subcomponent 目前还没怎么用过,具体例子待补充。
Dagger2在 Android 中的使用
在网上搜到的大部分关于 Dagger2的教程,都没有写到 Dagger2针对 Android 的特殊功能,所以特别的说明一下。前面举了很多例子,通过 Component 可以将所需要的对象注入到某个对象中,这就导致了一个问题,针对每个我们被注入的对象,都需要写一个 Component 接口来实现注入,在 Android 中,可能最常见的被注入的对象就是 Activity 和 Fragment 了,通常的写法是每个 Activity 和 Fragment都写一个 Component。这里直接用一下官方文档的例子:
public class FrombulationActivity extends Activity {
@Inject Frombulator frombulator;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// DO THIS FIRST. Otherwise frombulator might be null!
((SomeApplicationBaseType) getContext().getApplicationContext())
.getApplicationComponent()
.newActivityComponentBuilder()
.activity(this)
.build()
.inject(this);
// ... now you can write the exciting code
}
}
看到上面的一大串代码了么,是不是觉得好像没什么问题?
现在想象一下,你有10个 Activity,10个 Fragment,每个 Activity 和 Fragment 里面都要写这么一长串,是不是很麻烦,最后的结果就是,每个人都照着前面一个人写的复制粘贴,久而久之,大部分人都不记得这段代码是做什么的,只是记得新做一个界面就要复制一份过去。显而易见,这样复制粘贴的代码并不是一个易于维护和修改的代码。
其次,在进行如上注入的时候,你需要首先知道这个 Activity 需要哪些依赖,并进行不同的设置,尤其是在使用接口的时候,需要知道要用的实现类,才能进行正确的注入,这就打破了依赖注入一个很重要的原则,使用注入对象的类不应该知道如何创建所需要的对象。
为了解决上述问题,Dagger2特别提供了一个针对 Android 使用的简化流程。
- 给你的Activity 写一个 Subcomponent,应引用所需的 Module
@Subcomponent(modules = ComputerModule.class)
public interface MainActivitySubcomponent extends AndroidInjector {
@Subcomponent.Builder
public abstract class Builder extends AndroidInjector.Builder {}
}
- 写一个 Module 来使用这个 Subcomponent
@Module(subcomponents = MainActivitySubcomponent.class)
abstract class MainActivityModule {
@Binds
@IntoMap
@ActivityKey(MainActivity.class)
abstract AndroidInjector.Factory extends Activity>
bindMainActivityInjectorFactory(MainActivitySubcomponent.Builder builder);
}
- 现在需要一个 Top-Level Component 来进行注入,注意,一定要写
AndroidSupportInjectionModule
,以及上面写的 ActivityModule
@Component(modules = {
AndroidSupportInjectionModule.class,
MainActivityModule.class
})
@Singleton
interface AppComponent {
void inject(MyApp app);
- 最后,修改你的 Application,实现HasActivityInjector接口,并在其中调用 AppComponent 进行注入
public class MyApp extends Application implements HasActivityInjector {
@Inject
DispatchingAndroidInjector mActivityInjector;
@Override
public AndroidInjector activityInjector() {
return mActivityInjector;
}
@Override
public void onCreate() {
super.onCreate();
DaggerAppComponent.create().inject(this);
}
}
- 最后一步,在你的 Activity 生命周期中调用注入方法
public class MainActivity extends AppCompatActivity {
@Inject
Computer mComputer;
@Override
protected void onCreate(Bundle savedInstanceState) {
AndroidInjection.inject(this);
super.onCreate(savedInstanceState);
}
}
之后添加新的 Activity 只需要重复1,2步,并在AppComponent中加入新建的 Module 即可。
是不是觉得很复杂,没关系,还可以写的更简单:
@Module
abstract class MainActivityModule {
@MyScope
@ContributesAndroidInjector(modules = {ComputerModule.class})
abstract MainActivity contributeYourActivityInjector();
}
用上面的代码替换1,2步的内容即可。这种写法适用于 Subcomponent 中不需要任何其他内容的情况。
写了这么多,是不是觉得很奇怪,为什么这么写就能注入了?查看AndroidSupportInjectionModule的代码可以看到,里面定义了Android 中常用的组件的 Map 类型的注入,key 是对应的Class 类,而 value 这是上面第一步定义的MainActivitySubcomponent.Builder类,这个 Builder 类我们在第二步已经注入到 这个Map 中了,然后在 AndroidInjection 的 inject 方法中,根据 Activity 的类型去获取对应的 Builder 类进行注入。
Android中其他组件的注入方式与 Activity 类似,就不多说了,大家可以自己去查看文档。
这里还存在一些疑问,在 Subcomponent 的 Builder 类中,是可以添加@BindsInstance 注解传入参数的,但是如果使用 AndroidInjectiion的 inject 方法进行注入,是无法传入参数的,官方也没有实际的例子可供参考,可能是我对 Subcomponent的理解还不够,初步猜测可能需要自己重写AndroidInejction,在里面针对某个特定的需要参数的 Builder 进行单独的初始化操作。如果你有想法,欢迎一起讨论。