Android 开源库分析——Dagger2简介

本文链接:https://rainmonth.github.io/posts/A200701.html

摘要

Dagger是一个编译时依赖注入框架。实现原理是在编译的时候根据注解生成相应的Java源代码,然后在利用这些源代码进行程序的处理,所以并不会降低程序运行时的性能。

Dagger解决了什么问题呢?就是对象之间的耦合。将直接的组合方式造成的耦合,改为间接的依赖关系,从而降低耦合。

既然是实现原理是基于Java编译时注解,所以需要先掌握一点Java编译时注解的知识,可以参考Java注解。Dagger用到的注解主要有以下几个:

  • @Inject,具体使用的地方,添加该注解的就可以通过Dagger来获取到相应的对象,不用自己实例化;
  • @Provides,在@Module中使用,用来表示该方法可以向外部提供一个其注解方法返回的对象;
  • @Module,用来定义一个模块,这个模块中可以使用@Providers注解来想外部提供相应的对象供外部调用,主要是为了解决依赖第三方库时不能修改其源码导致无法直接注入的问题;
  • @Component,连接依赖需求方和依赖对象提供方的桥梁,依赖需求方指的就是实例对象最终要被注入的地方(在Android开发场景下一般有Activity、Fragment等,无特殊要求);依赖提供方可以是采用@Module注解的类或采用@Inject注解的实例类的构造函数,可以提供需要注入的对象的实例。@Component注解的modules参数代表的就是依赖提供方,而其注解的类里面定义的inject方法的参数代表的就是依赖需求方

知识准备

  1. Java注解的基础知识;
  2. javax.inject.jar中定义的注解和接口;

由于Dagger2是一个编译时依赖注意框架,在介绍它之前,先看看Java EE提供本身都提供了哪些依赖注入的东西。

在javax.inject.jar包下,定义了一下几个注解和接口:

  • @Inject,标识可注入的构造函数,字段和方法,也可以是静态成员变量,注意问题:
    1. 可以是任意修饰符;
    2. 注入顺序依次是:构造函数,父类字段(如果有),子类字段,父类方法(如果有),子类方法;
    3. 每个类最多可有一个构造函数可添加@Inject;
    4. 修饰构造函数时,当只有一个public无参数构造函数时,@Inject可以省略不写;
    5. 修饰成员变量时,必须是一个有合法名字的非final 变量;
    6. 修饰方法时:
      1. 不能是抽象方法;
      2. 可以有返回类型;
      3. 可以有方法参数,但方法参数不能是自身参数类型(比如 class A中的methodA(A a)就不能用@Inject);
    7. 要避免循环依赖;
    8. 需要利用javax.inject.Provider来监测循环依赖;
  • @Named,类似于@Qualifier,只不过他是通过一个简单的String参数来指定到底来使用哪个依赖提供方,一个接口多个实现,匹配引入想要的实现类;
  • @Qualifier,依赖需求方 创建数据的时候使用哪个依赖提供方
  • @Scope,标记一个注解实例的使用范围,是一个元注解,利用他可以定义@ActivityScope、@FragmentScope等;
  • @Singleton,标记该类只能被创建一次,不能被继承;
  • Provider,用来提供某个类型实例的接口,每次get方法调用都会生成一个新的实例化对象;
  • Lazy,用来提供某个实例的接口,但采用懒加载实现,在单一范围内注解的对象实例化后,再次调用,采用的仍是第一次实例化后的对象;

基本使用方式

先引入官方库

在项目主module的build.gradle的dependencies中,采用以下代码引入Dagger2,具体的版本自行选择(我选择的是2.14.1):

1
2
compile 'com.google.dagger:dagger:2.14.1'
annotationProcessor 'com.google.dagger:dagger-compiler:2.14.1'

再随便定义一个对象A:

1
2
3
4
5
public class A {
public void methodA() {
System.out.println("I am methodA");
}
}

然后创建Module

使用@Module来注解用来获取对象实例的类,Dagger通过这个类就可以知道自己要去按个对应的类中获取实例了,具体代码如下:

1
2
3
4
5
6
7
@Module
public class TestModule {
@Provides
A provideA() {
return new A();
}
}

上面@Provides用来注解获取对象实例的方法的(为方便描述称之为获取实例方案1),Dagger2根据该注解及方法的返回类型将对象实例注入到对应的引用中。

经过上面两部后,依赖提供方(提供对象实例的那一方)相关工作就完成了。接下来看看依赖需求方要做哪些工作。

定义一个MainActivity,以来于A对象,在点击的时候调用A对象的methodA方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class MainActivity extends AppCompatActivity {

@Inject
A a;
TextView tvTest1;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);

tvTest1 = findViewById(R.id.tv_test1);

tvTest1.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
a.methodA();
}
});
}

依赖需求方通过@Inject注解,告诉外界(目前还不知道到底是哪个会给他提供A对象),A对象需要注入,很简单!

这个时候如果运行,会报空指针异常,因为A对象没有真正的实例化。

我们需要一个中介来将依赖需求方和依赖提供方联系起来,@Component扮演的就是这个角色。

创建Component

  1. 第一步,使用@Component来声明:
  2. 第二步,添加要使用的Module;
  3. 第三部,同目标页面(Activity或Fragment)绑定;

具体代码如下:

1
2
3
4
@Component(modules = TestModule.class)
public interface TestComponent {
void inject(MainActivity activity);
}

@Component注解的modules参数表示的就是依赖的提供方,即TestModule;里面的inject方法的参数MainActivity指代的就是依赖的需求方,告诉实例对象要被注入到哪个地方。

实际上@Component还有一个参数dependencies,表明要想完成这次依赖需求方和依赖提供方的交易,还需要其他机构的协助,后面会给出示例

根据@Component注解支持的参数modules的形式不难推断:一个Component可以依赖多个Module,而实际项目上也是如此。

都定义好了之后,就可以编译项目了。

编译项目

在Module和Component创建完毕后,在AS中Build菜单中点击Rebuild Project,编译成功后,AS中就会自动生成名为DaggerTestComponent的类,自动生成的Component都是Dagger开头的。

开始绑定

编译项目之后,在MainActivity建立依赖需求方和依赖提供方的联系

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class MainActivity extends AppCompatActivity {
@Inject
A a;
TextView tvTest1;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// 途径1:
DaggerTestComponent.builder().build().inject(this);
// 途径2:
// DaggerTestComponent.create().inject(this);

tvTest1 = findViewById(R.id.tv_test1);

tvTest1.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
a.methodA();
}
});
}
}

上面就是Dagger使用的一般流程,十分简单。

上面的示例中,是通过@Provides注解的方法来提供A的对象实例的,还有一种途径就是直接用@Inject注解A类的构造函数(为方便描述称之为获取实例方案2),这样就不用在TestModule中定义相应的provideA方法了。

看完了简单使用,我们接下来分析下Dagger2为我们生成了哪些代码:

获取实例采用方案1时:

  • DaggerTestComponent,即TestComponent接口的实现类;
  • TestModule_ProvideAFactory,即Dagger2的Factory接口的实现类,实际上Factory接口继承自javax.inject.Provider接口,用来提供A对象的;
  • MainActivity_MembersInjector,即Dagger2的MemgersInject接口实现类,负责将被依赖项注入到目标类的成员变量或成员方法的接口中。

获取实例采用方案2时:

  • DaggerTestComponent,即TestComponent接口的实现类;
  • A_Factory,即Dagger2的Factory接口的实现类,实际上Factory接口继承自javax.inject.Provider接口,用来提供A对象的;
  • MainActivity_MembersInjector,即Dagger2的MemgersInject接口实现类,负责将被依赖项注入到目标类的成员变量或成员方法的接口中。

两者不同之处在于A对象的生成方式。

如果实例A的构造依赖于其他类B,即A的构造函数如下:

1
2
3
4
private B b;
public A(B b) {
this.b = b;
}

这个时候我们只要提供合适的方法来构造B的实例即可(同样可以采用上面说的两种实例构造方案:直接用@Inject注解B的无参数构造函数和在对应的Module中提供相应的provide方法。

进阶使用方式

Dagger2处理上面几个注解外,还有@Scope、@Singleton、@Bind等比较实用的注解

单例对象注入

我要获取一个在Application声明周期内都是单例的对象,该如何操作呢?看下面的例子:

要在TestApplicaion中注入一个DbManager对象,并保证他在Application声明周期内是单例的。

和基本方法一样,页需要经历定义Module、定义Component、关联依赖方,编译和提供方几个步骤

DbManager如下:

1
2
public class DbManager {
}

AppModule如下:

1
2
3
4
5
6
7
8
@Module
public class AppModule {
@Singleton
@Provides
DbManager provideDbManager() {
return new DbManager();
}
}

AppComponent如下:

1
2
3
4
5
@Singleton
@Component(modules = AppModule.class)
public interface AppComponent {
void inject(TestApplication testApplication);
}

注意:由于AppModule中的@Provides注解的方法上采用了@Singleton注解,如果AppComponent中不使用@Singleton注解,就会报如下错误

1
.AppComponent (unscoped) may not reference scoped bindings:

再看TestApplication代码:

1
2
3
4
5
6
7
8
9
10
public class TestApplication extends Application {
@Inject
DbManager mDbManager;

@Override
public void onCreate() {
super.onCreate();
DaggerAppComponent.create().inject(this);
}
}

这样就能保证mDbManager在Application是单例的了(当然这是狭义上的单例)

虽说是单例,但实际上跟@Singleton没有任何关系,可以看看@Singleton这个注解的定义,它是有@Scope元注解定义生成的一个注解,也就是说@Singleton表示的也是一个范围。

与抽象类相关的注入

假设上面的DbManager是一个抽象类,他定一个方法sayHello():

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
public abstract class DbManager {
public static final String TAG = DbManager.class.getSimpleName();
public abstract void sayHello();
}

public class CourseDbManager extends DbManager {
@Override
public void sayHello() {
Log.d(TAG, "I am courseDbManager!");
}
}
public class WorkDbManager extends DbManager {
@Override
public void sayHello() {
Log.d(TAG, "I am workDbManager!");
}
}

// 修改后的AppModule
@Module
public abstract class AppModule {
@Singleton
@Binds
public abstract DbManager bindsManager(CourseDbManager courseDbManager);

@Singleton
@Provides
public static CourseDbManager providesCourseDbManager() {
return new CourseDbManager();
}

@Singleton
@Provides
public static WorkDbManager providesWorkDbManager() {
return new WorkDbManager();
}
}

上面代码需要说明以下两点:

  • @Binds的作用于@Moudle注解的抽象类中的抽象方法,该方法的返回值是抽象类,但方法的参数必须是具体的实现类,@Provides无法作用于抽象方法;
  • 对抽象类使用@Module注解时,获取对象实例的方法必须是static方法(即相应的provides方法必须是static方法)

@Provides方法返回参数相同如何处理

上面的AppModule中如果两个provides方法的返回值都写成DbManager可不可以呢?显然直接改成DbManager是不可以的,无法通过编译,会报如下错误:

1
DbManager is bound multiple times

那么有没有什么方法能让它通过呢?方法是有的,那就是用到另一个注解@Named

@Named,可以注解在参数、成员变量以及方法上,他其实也是@Qualifier注解定义而来的,可以算作@Qualifier比较简单的实现。

利用@Named改写AppModule即可通过编译:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Module
public abstract class AppModule {
@Singleton
@Binds
public abstract DbManager bindsDbManager(@Named("courseDbManager") DbManager dbManager);

@Singleton
@Provides
@Named("courseDbManager")
public static DbManager providesCourseDbManager() {
return new CourseDbManager();
}

@Singleton
@Provides
@Named("workDbManager")
public static DbManager providesWorkDbManager() {
return new WorkDbManager();
}
}

我的理解,@Qualifier或@Named注解就是给一个抽象类的某个具体实现类一个唯一表示,这样Dagger2生成注入时就知道具体使用哪个实现类了。

@Component依赖属性的使用

如果我想把TestApplication中的DbManager对象同样页注入到MainActivity该如何实现呢?我们可以利用@Component注解的另外一个属性dependencies来实现,改写后的TestComponent如下:

1
2
3
4
@Component(modules = TestModule.class, dependencies = AppComponent.class)
public interface TestComponent {
void inject(MainActivity activity);
}

直接按上面的写法,同样不能通过编译,报错如下:

1
(unscoped) may not reference scoped bindings:

很容易理解,就是没有定义scoped的Component引用了定义了作用范围(@Singleton)的Component,要解决这个问题,我们需哟啊给TestComponent定义一个Scope,那么定义成@Singleton可不可以呢?先试试:

1
2
错误: This @Singleton component cannot depend on scoped components:
@Singleton cn.rainmonth.daggerdemo.di.AppComponent

意思就是相同的scope间不能存在依赖关系,那么问题就好解决了,我给TestComponent定义一个不同的Scope就可以了,问题就来了,如何定义一个Scope呢?

看了下javax.inject包下的代码,其中@Scope作为元注解就是用来做这个的,可以参照@Singleton这个注解的写法来定义一个注解,这里姑且叫@ActivityScope吧,代码如下:

1
2
3
4
5
@Scope
@Documented
@Retention(RUNTIME)
public @interface ActivityScope {
}

将定义好的@ActivityScope注解应用与TestComponent上,编译通过。

编译通过后,就是在MainActivity中使用了,在使用之前,我们需要将AppComponent在TestApplication中暴露出来,在TestApplication中添加如下方法:

1
2
3
public AppComponent getAppComponent() {
return DaggerAppComponent.create();
}

为了方便从AppComponent获取DbManager对象,我们需要在AppComponent中添加一个接口方法:

1
DbManager dbManager();

再次编译,编译成功后,看看Dagger2为我们生成的代码,发现dbManger方法已经自动帮我们实现了。

如果我们要在MainActivity中获取DbManager实例,只要如下操作就可以了:

1
final DbManager dbManager = ((TestApplication) getApplication()).getAppComponent().dbManager();

当然可以稍微加上自己的封装,这里就不赘述了。

懒加载机制

在实际使用过程中,有的对象并不是时刻都被使用到的,我们只需要在使用它的时候实例化它就可以了,要实现这种功能,可以使用Lazy,使用Lazy包装后,就会在第一次调用该Lazy包装的对象的get方法后,才去实例化相应对象,实例化后,下次如果还是使用这个Lazy包装对象的get方法,就不再重新创建了。与之对应的Provider接口,虽然也是使用 的时候才实例化,但它每次使用都会重新实例化,而不是像Lazy那样复用第一次实例化出来的实例。

小结

上面通过实例展示了Dagger2的一些基本的使用方法,并逐步深入,讲到了一些进阶的用法,后面还会继续深入Dagger2具体的内部原理。