Dagger2略深入浅出的梳理

****(说在最前:阅读本篇之前,希望大家对Dagger2已经有了一个初步的了解。从而帮助感觉似是而非的同学进一步的理清一些问题)****

 谈到Dagger2 第一印象就是难于上手,事实上也的确如此。关于Dagger2,之前,需要一些很多知识作为铺垫。

从最基本的概念开始:
依赖注入是怎么一回事儿;注解又是怎么一回事儿;
(其中对注解的运用又涉及到源码级框架
关于Java注解
注解处理器
注解处理器能够在编译源码期间扫描Java代码中的注解,并且根据相关注解动态生成相关的文件。之后在程序运行时就可以使用这些动态生成的代码。值得注意的是,注解处理器运行在跟最终程序不同的虚拟机,也就是说,编译器为注解处理器开启了另外一台虚拟机来运行注解处理器。
所以我们看到的那些@,是在帮助我们在编译期间辅助创建一些依赖注入的代码:性能上Dagger2是优于Spring,但带来的是编译阶段时间的延长。这样的话,每当我们修改或者添加这些注解代码的时候,就需要我们重新Build一下(即由apt插件来生成我们所需要使用的代码)。对此也带来一问题,我们需要正确的配置以及写出正确的Dagger2代码,这也是为什么说上手难的原因之一。

举个例子:
一个容器里面装的是苹果,不用Dagger2的情况下我们可以这么写:

    Fruit f = new Apple(color,size);
}```
上面的代码有个问题,Container依赖了Apple实现,如果某天需要修改Apple为Banana,那么我们一定得改Container里面的代码。有没有一种方法可以不改Container呢? 

那便是我们所提到的**依赖注入----Dagger2。
通常依赖注入有很多类别,例如setter注入;构造函数注入;依赖获取;反射依赖等等,这里不展开讨论。Dagger2就是通过编译期间辅助创建代码来帮助我们进行依赖获取的依赖注入。**

在这里,我们就可以使用Dagger2,我们可以把代码改成
```public class Container{ 
    @Inject
      Fruit f;
     ...
}```

这样,Container的成员变量就自动初始化成Apple实例了,Container不用关心具体用哪个Fruit的实现。假如某一天要把Apple替换Banana,Container的代码是完全不需要改动的。从某种意义上说,Dagger2就是一个帮你写工厂代码的工具。当然Dagger2的功能比工厂模式更加强大。

接下来,要再弄懂一个关键词就是清楚的大前提便是**JSR-330**!
简而言之就是:
Java的依赖注入为注入类定义了一组标准注解(和一个接口),进而最大限度地提高java代码的可重用性,可测试性和可维护性。

-----------------------------------更深层理论内容的分割线-------------------------------------------

为了更好的弄懂Dagger2,就得弄懂依赖注入的基本原理和下面每个玩意儿的概念,这很重要!(也是为什么开头入手难的原因之二)
- @Inject: 总的来说,用这个注释表示我们要请求依赖了。 换句话说,你使用它告诉Dagger注释的类或字段想要参与依赖注入。从而,Dagger将构造这个注释类的实例并满足它们的依赖性。

- @Module: Modules是其方法提供依赖性的类,因此我们定义一个类并使用@Module注释它,因此,Dagger将知道在哪里找到依赖,以便在构造类实例时满足它们。**Modules的一个重要特性是它们被设计为可以被分割以及组合在一起使用(例如,过会儿我们将看到,在下面的代码中,可以有多个组合Modules)。**

- @Provide: 在Modules内部,我们定义了包含这个注释的方法,告诉Dagger我们如何构造和提供那些提到的依赖。

- @Component: Components总的来说就是注入器,称为@Inject 和@Module之间的桥梁,它的主要职责是将这两部分融合到一起。**Components是你所定义的所有类型的实例(如果理解不了换个想法Component 用于连接module 作为接口 暴露需要操作的方法 将依赖对象自动注入到Container中
)**。例如,我们必须使用@Component注释一个接口,并列出将组成该组件的所有@Modules,如果它们中的任何一个丢失了,我们在编译时会遇到报错。 所有Components都得知道它通过其Modules提供的依赖的范围。

- @Scope:  Scopes是非常有用的**(也是很有迷惑性的,最下面有梳理)**,Dagger2有一种更具体的方式——自定义注释来做范围。随后下面会有例子可以从中看到,这是一个非常强大的功能,就如前面指出的,没有必要让每个对象都知道如何管理自己的实例。Scope示例是具有自定义@PerActivity注释的类,因此只要我们的Activity活着,此对象就会存在。换句话说,我们可以定义范围的程度(@PerFragment,@PerUser等)。

- @Qualifier: **当类的类型不足以识别依赖性时,我们使用此注释。** 例如,在Android的情况下,我们需要不同类型的context,所以我们可以定义一个限定符(qualifier)注释“@ForApplication”和“@ForActivity”,因此当注入context时,我们可以使用这些限定符来告诉Dagger哪种类型是我们想要提供的context。

----------------------------------------------实践为王-----------------------------------------------------
**结构**

Dagger2要实现一个完整的依赖注入,必不可少的元素有三种,Module,Component,Container。 
![image.png](http://upload-images.jianshu.io/upload_images/95044-1225a2c9e4f1d356.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)

Container就是可以被注入的容器,具体对应上文例子中的Container,Container拥有需要被初始化的元素。需要被初始化的元素必须标上@Inject,只有被标上@Inject的元素才会被自动初始化。@Inject在Dagger2中一般标记构造方法与成员变量。
Module 可以说就是依赖的原材料的制造工厂,所有需要被注入的元素的实现都是从Module生产的。
有了可以被注入的容器Container,也有了提供依赖对象的Module。我们必须将依赖对象注入到容器中,这个过程由Component来执行。Component将Module中产生的依赖对象自动注入到Container中。


**配置**

// Add Dagger dependencies

dependencies {
compile 'com.google.dagger:dagger:2.x'
annotationProcessor 'com.google.dagger:dagger-compiler:2.x'
}

如果你使用的Android gradle plugin版本低于2.2参考 [https://bitbucket.org/hvisser/android-apt](https://bitbucket.org/hvisser/android-apt).或者试试如下配置

project的build.gradle添加

dependencies {
... // 其他classpath
classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8' //添加apt命令
}


module的build.gradle添加

// 添加其他插件
apply plugin: 'com.neenbedankt.android-apt'//添加apt命令

dependencies {
apt 'com.google.dagger:dagger-compiler:2.0.2' //指定注解处理器
compile 'com.google.dagger:dagger:2.0.2' //dagger公用api
provided 'org.glassfish:javax.annotation:10.0-b28' //添加android缺失的部分javax注解
}

**实现**

实现Module 
Module其实就是一个依赖的制造工厂。我们只需要为其添加制造依赖的方法即可,继续上文实现苹果容器的例子。

@Module //1 注明本类属于Module
public class FruitModule{
@Provides //2 注明该方法是用来提供依赖对象的特殊方法
public Fruit provideFruit(){
return new Apple(Color.RED,Size.BIG);
}
}

(1)中添加注解@Module注明本类属于Module 
(2)中添加注解@Provides注明该方法是用来提供依赖对象的特殊方法 
一个完整的Module必须拥有@Module与@Provides注解 

实现Component 
Component就是一个将Module生成的实例注入Container中的注入器。我们来写一个水果注入器:

@Component(modules={FruitModule.class}) //3 指明Component在哪些Module中查找依赖
public interface FruitComponent{ //4 接口,自动生成实现
void inject(Container container); //5 注入方法,在Container中调用
}

(3)中@Component使用modules指向使用的Module的集合。 
(4)所有的Component都必须以接口形式定义。Dagger2框架将自动生成Component的实现类,对应的类名是Dagger×××××,这个例子中对应的实现类是DaggerFruitComponent 
(5)中添加注入方法,一般使用inject做为方法名,方法参数为对应的Container

实现Container 
Container就是可以被注入依赖关系的容器。具体实现如下

public Container{
@Inject //6 添加@Inject,标记f可以被注入
Fruit f;
public void init(){
DaggerFruitComponent.create().inject(this); //7 使用FruitComponent的实现类注入
}
}

Container除了代码中(6)标记f需要被注入外,还需要代码中(7)调用Component的实现类将Module的生成的对象注入到f中。

到此,当调用Container的init()方法时,Contianer中的f将会自动初始化成实现类Apple的对象。 
以后如果想更改Fruit的实现类,只需要在@Component中的modules指向不同的Module即可。而Container的代码完全不需要改动。因为Container已经不再依赖Apple实现了。

---------------------------------------------------巨大的例子---------------------------------------------
如此简单就肯定不值得我们说了。。所以。。下面
我们用Google官方的android-architecture-todo-mvp-dagger项目来深入实践探讨。
首先从Application类入手

public class ToDoApplication extends Application {

private TasksRepositoryComponent mRepositoryComponent;

@Override
public void onCreate() {
    super.onCreate();

    mRepositoryComponent = DaggerTasksRepositoryComponent.builder()
            .applicationModule(new ApplicationModule((getApplicationContext())))
            .tasksRepositoryModule(new TasksRepositoryModule())
            .build();

}

public TasksRepositoryComponent getTasksRepositoryComponent() {
    return mRepositoryComponent;
}

}

DaggerTasksRepositoryComponent就是由Dagger2帮我们生成的代码。
对比上面的代码用DaggerXXXXComponent.create()实际上等价于DaggerXXXXComponent.builder().build()。
在构建的过程中,默认使用Module无参构造器产生实例。如果需要传入特定的Module实例,可以像上面一样用。Module只有有参构造器,显式传入Module实例。

1、为什么要写这样的application
Dagger编写Android应用程序的困难中心点是许多Android框架类都是由操作系统本身实例化的,像 Activity和Fragment,只有让Dagger可以创建所有注入的对象才能完美的运作。为此必须在生命周期方法中执行成员注入。

Component中包含了TasksRepositoryModule, ApplicationModule两个module.我们可以详细的看一下

/**

  • This is used by Dagger to inject the required arguments into the {@link TasksRepository}.
    */
    @Module
    public class TasksRepositoryModule {

    @Singleton
    @Provides
    @Local
    TasksDataSource provideTasksLocalDataSource(Context context) {
    return new TasksLocalDataSource(context);
    }

    @Singleton
    @Provides
    @Remote
    TasksDataSource provideTasksRemoteDataSource() {
    return new FakeTasksRemoteDataSource();
    }

}

这个里面用到@Singleton@Provides@Local@Remote
@Singleton:是用来限制创建单实例对象的注解
@Provides:注明该方法是用来提供依赖对象的特殊方法
@Local@Remote是自定义的注解用@Qualifier元注解实现

@Qualifier
@Documented
@Retention(RetentionPolicy.RUNTIME)
public @interface Local {

}


@Module
public final class ApplicationModule {

private final Context mContext;

ApplicationModule(Context context) {
    mContext = context;
}

@Provides
Context provideContext() {
    return mContext;
}

}

在ApplicationModule中很明显的一点,并没有直接返回new xxxx这样的实例。而是在@Provides下用了一个有返回值的方法。这就是要说到的
***如果Module只有有参构造器,则必须显式传入Module实例。***

这里创建好了之后,我们就能在activity、fragment里拿到component并且inject()注入

TasksActivity.java
DaggerTasksComponent.builder()
.tasksRepositoryComponent(((ToDoApplication) getApplication()).getTasksRepositoryComponent())
.tasksPresenterModule(new TasksPresenterModule(tasksFragment)).build()
.inject(this);

---------------------------------------------解析

2、依次往下是神马结构以及完整的一套流程:
在TasksActivity.java进行了控件的初始化,fragment和presenter的创建。
可以看到在变量声明处,声明了 @Inject TasksPresenter mTasksPresenter;
然后通过上面的代码段,在onCreate周期里进行了实例化的创建。

然后具体再看,顺着inject(this),我们进入到TasksComponent。

@FragmentScoped
@Component(dependencies = TasksRepositoryComponent.class, modules = TasksPresenterModule.class)
public interface TasksComponent {

void inject(TasksActivity activity);

}

我们知道,Module里是提供依赖对象的特殊方法,在此指明的
modules = TasksPresenterModule我们进入可以看到如下关键方法:

@Provides
TasksContract.View provideTasksContractView() {
return mView;
}

然而在TasksPresenterModule里
并没有显示的提供实例,所以我们需要去TaskPresenter里的构造函数里去看。
为什么呢?
我们需要知道的是,Dagger给我们一些注入依赖的选项:
**构造函数注入:通过用@Inject注释我们类的构造函数。
字段注入:通过用@Inject注释我们类的(非私有)字段。
方法注入:通过使用@Inject注释一个方法。**

这也是Dagger在绑定依赖项时使用的顺序,它很重要,因为它可能在你有一些奇怪的行为后发生NullPointerExceptions错误,或者更甚,这意味着你的依赖可能没有在对象创建的时刻初始化。 这在Android上在Activities或Fragments中使用字段注入时很常见,因为我们没有访问它们的构造函数。

所以,我们再依照如此的顺序,可以在TasksPresenter里看到它的构造函数是添加了@Inject注释的,也就是说,我们是通过这里来提供实例化对象的。也就是上面代码段所return的TaskContract.View类型的mView。

与此之外构造函数里还需要另外一个参数TasksRepository tasksRepository,在哪里能得到这个参数呢,往回倒倒,我们发现在TasksComponent里@Component注释处,**除了通常的module之外,还有一个dependencies,这个就是说我们的这个Component还依赖着另一个TasksRepositoryComponent。这个Component就会必须定义带返回值的方法来提供我们所缺少的依赖 。**

好了,到这里我们就知道@Inject TasksPresenter mTasksPresenter是怎样得到实例化的了。然而并没有完,我们接着进入到的TasksPresenter里继续看。

发现,除了构造函数之外,紧挨着下面有一个方法也被标注了@Inject注释

/***
* Method injection is used here to safely reference {@code this} after the object is created.
* For more information, see Java Concurrency in Practice.
*/
@Inject
void setupListeners() {
mTasksView.setPresenter(this);
}```
这就是上面提到的第二种方式——字段注入:通过用@Inject注释我们类的(非私有)字段。然后这个方法会在什么时候调用呢。我们可以通过在build构建了Dagger2的代码之后,观察到在

public final class TasksPresenter_MembersInjector implements MembersInjector<TasksPresenter> {
  public TasksPresenter_MembersInjector() {}

  public static MembersInjector<TasksPresenter> create() {
    return new TasksPresenter_MembersInjector();
  }

  **@Override
  public void injectMembers(TasksPresenter instance) {
    if (instance == null) {
      throw new NullPointerException("Cannot inject members into a null reference");
    }
    instance.setupListeners();
  }

有injectMembers这么一个方法injectMembers()里调用了我们@Inject注释的setupListeners()方法。

关于injectMemers()方法在官方文档里是这样说到的:
injectMembers
void injectMembers(T instance)
Injects dependencies into the fields and methods of instance. Ignores the presence or absence of an injectable constructor.
Whenever the object graph creates an instance, it performs this injection automatically (after first performing constructor injection), so if you're able to let the object graph create all your objects for you, you'll never need to use this method.

Parameters:
instance - into which members are to be injected
Throws:
NullPointerException - if instance is null

里面有说到它会自动执行这个注入(首次执行构造函数注入后)

再继续往上追寻,可以发现,是在DaggerTasksComponent里的inject方法里有调用(具体代码请自行查阅),这个我们Activity里用到的Dagger2框架将自动生成Component的实现类DaggerTasksComponent。也就是说setupListeners()是在调用了.inject(this)之后。跟着我们需要的字段被实例化后,跟着被调用的。

眼尖的同学肯定还注意到了,除了我们分析的这个方法调用点的问题之外,上面还有几行官方的注释,大概是说在方法注入用在这里是创建了安全引用后(this),然后更多信息参考java并发实践。如何并发安全,final类型。创建后状态不能被修改的对象叫做不可变对象。不可变对象永远是线程安全的。

所以~~我们可以看到,TasksPresenter这个类,是被final修饰了的。
流程走到这里,我们就已经清晰的看到了依赖注入给我们创建的实例,以及相关的一些用法。剩下的关于MVP架构的交互方式就不在这里展开了。

3、更多的可能方式和方法
实现单例、Subcomponent、Lazy、Provider、Multibindings等等
未完待续……
---------------------------------------------------接着说-------------------------------------------------

4、需要啰嗦的部分:
关于scope

很多初学者,误以为添加上自定义的Scope后,MainComponent就会获得一种神奇的能力,能够自动实现与MainActivity的“生死与共”。这种想法天真烂漫可爱,但却是错误的。

添加上自定义的Scope后,MainComponent并没有获得什么超能力,要想让它与MainActivity的生命周期同步,还需要我们手动完成,即:

MainActivity的onCreate方法中创建MainComponent并完成依赖注入
MainActivity的onDestroy方法中销毁MainComponent
------------------------------------------------暂时收工-------------------------------------------------

参考以及引用资料:
1、依赖注入相关:
Android 中的依赖注入
依赖注入那些事儿
Java 依赖注入标准(JSR-330)

2、注解相关:
Java注解
源码级框架(注解处理器)

3、工厂模式:

4、Dagger2相关:
Android常用开源工具(1)-Dagger2入门
解析Dagger中的Scope
都是套路——Dagger2没有想象的那么难
聊聊 Android 中的依赖注入
Google官方MVP+Dagger2架构详解【从零开始搭建android框架系列(6)】
Dagger2深入理解
Tasting Dagger 2 on Android
Dependency injection with Dagger 2 - Custom scopes
谷歌官方Dagger2文档

5、demo:
Android Architecture Blueprints [beta] - MVP + Dagger2
GithubClient

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 162,547评论 4 374
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 68,787评论 2 308
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 112,175评论 0 254
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,752评论 0 223
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 53,169评论 3 297
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 41,056评论 1 226
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 32,195评论 2 321
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,980评论 0 214
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,724评论 1 250
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,872评论 2 254
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,344评论 1 265
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,667评论 3 264
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,379评论 3 245
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,202评论 0 9
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,992评论 0 201
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 36,189评论 2 286
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,987评论 2 279

推荐阅读更多精彩内容