最近在重新搭建android工程听到dagger2这个框架,然后去Github上看了一下,天啊!2w多的star,呃,是应该看一下这玩意是个什么东西了,竟然这么火。
百度了一下大概看了一下其他关于dagger的文章大概知道这玩意是干啥的了。
Dagger2是Dagger的升级版,是一个依赖注入框架,现在由Google接手维护。
我们要搞清楚这个框架怎么用必须先知道什么是依赖注入和依赖注入的好处。
什么是依赖注入
依赖注入是面向对象编程的一种设计模式,其目的是为了降低程序耦合,这个耦合就是类之间的依赖引起的。
举个例子:我们在写面向对象程序时,往往会用到组合,即在一个类中引用另一个类,从而可以调用引用的类的方法完成某些功能,就像下面这样.
1 | public class ClassA { |
这个时候就产生了依赖问题,ClassA依赖于ClassB,必须借助ClassB的方法,才能完成一些功能。这样看好像并没有什么问题,但是我们在ClassA的构造方法里面直接创建了ClassB的实例,问题就出现在这,在ClassA里直接创建ClassB实例,违背了单一职责原则,ClassB实例的创建不应由ClassA来完成;其次耦合度增加,扩展性差,如果我们想在实例化ClassB的时候传入参数,那么不得不改动ClassA的构造方法,不符合开闭原则。
因此我们需要一种注入方式,将依赖注入到宿主类(或者叫目标类)中,从而解决上面所述的问题。依赖注入有以下几种方式:
通过接口注入
1 | interface ClassBInterface { |
通过set方法注入
1 | public class ClassA { |
通过构造方法注入
1 | public class ClassA { |
通过Java注解
1 | public class ClassA { |
在Dagger2中用的就是最后一种注入方式,通过注解的方式,将依赖注入到宿主类中。
这些框架的Java注解是怎么实现的
从JDK5开始,Java增加了Annotation(注解),Annotation是代码里的特殊标记,这些标记可以在编译、类加载、运行时被读取,并执行相应的处理。通过使用Annotation,开发人员可以在不改变原有逻辑的情况下,在源文件中嵌入一些补充的信息。代码分析工具、开发工具和部署工具可以通过这些补充信息进行验证、处理或者进行部署。
Annotation提供了一种为程序元素(包、类、构造器、方法、成员变量、参数、局域变量)设置元数据的方法。Annotation不能运行,它只有成员变量,没有方法。Annotation跟public、final等修饰符的地位一样,都是程序元素的一部分,Annotation不能作为一个程序元素使用。
定义Annotation
定义新的Annotation类型使用@interface关键字(在原有interface关键字前增加@符号)。定义一个新的Annotation类型与定义一个接口很像,例如:
1 | public Test{ |
定义完该Annotation后,就可以在程序中使用该Annotation。使用Annotation,非常类似于public、final这样的修饰符,通常,会把Annotation另放一行,并且放在所有修饰符之前。例如:
1 |
|
成员变量
Annotation只有成员变量,没有方法。Annotation的成员变量在Annotation定义中以“无形参的方法”形式来声明,其方法名定义了该成员变量的名字,其返回值定义了该成员变量的类型。例如:
1 | public MyTag{ |
示例中定义了2个成员变量,这2个成员变量以方法的形式来定义。
一旦在Annotation里定义了成员变量后,使用该Annotation时就应该为该Annotation的成员变量指定值。例如:
1 | public class Test{ |
也可以在定义Annotation的成员变量时,为其指定默认值,指定成员变量默认值使用default关键字。示例:
1 | public MyTag{ |
如果Annotation的成员变量已经指定了默认值,使用该Annotation时可以不为这些成员变量指定值,而是直接使用默认值。例如:
1 | public class Test{ |
根据Annotation是否包含成员变量,可以把Annotation分为如下两类:
标记Annotation:没有成员变量的Annotation被称为标记。这种Annotation仅用自身的存在与否来为我们提供信息,例如@override等。
元数据Annotation:包含成员变量的Annotation。因为它们可以接受更多的元数据,因此被称为元数据Annotation。
元注解
在定义Annotation时,也可以使用JDK提供的元注解来修饰Annotation定义。JDK提供了如下4个元注解(注解的注解,不是上述的”元数据Annotation“):
@Retention
@Target
@Documented
@Inherited
@Retention
@Retention用于指定Annotation可以保留多长时间。
@Retention包含一个名为“value”的成员变量,该value成员变量是RetentionPolicy枚举类型。使用@Retention时,必须为其value指定值。value成员变量的值只能是如下3个:
RetentionPolicy.SOURCE:Annotation只保留在源代码中,编译器编译时,直接丢弃这种Annotation。
RetentionPolicy.CLASS:编译器把Annotation记录在class文件中。当运行Java程序时,JVM中不再保留该Annotation。
RetentionPolicy.RUNTIME:编译器把Annotation记录在class文件中。当运行Java程序时,JVM会保留该Annotation,程序可以通过反射获取该Annotation的信息。
例如:
1 | package com.demo1; |
如果Annotation里有一个名为“value“的成员变量,使用该Annotation时,可以直接使用XXX(val)形式为value成员变量赋值,无须使用name=val形式。
@Target
@Target指定Annotation用于修饰哪些程序元素。@Target也包含一个名为”value“的成员变量,该value成员变量类型为ElementType[ ],ElementType为枚举类型,值有如下几个:
ElementType.TYPE:能修饰类、接口或枚举类型
ElementType.FIELD:能修饰成员变量
ElementType.METHOD:能修饰方法
ElementType.PARAMETER:能修饰参数
ElementType.CONSTRUCTOR:能修饰构造器
ElementType.LOCAL_VARIABLE:能修饰局部变量
ElementType.ANNOTATION_TYPE:能修饰注解
ElementType.PACKAGE:能修饰包
示例1(单个ElementType):
1 | package com.demo1; |
示例2(多个ElementType):
1 | package com.demo1; |
@Documented
如果定义注解A时,使用了@Documented修饰定义,则在用javadoc命令生成API文档后,所有使用注解A修饰的程序元素,将会包含注解A的说明。
示例:
1 |
|
1 | public class Test { |
@Inherited
1 | package com.demo2; |
1 | package com.demo2; |
1 | package com.demo2; |
示例中Base使用@MyTag修饰,SubClass继承Base,而且没有直接使用@MyTag修饰,但是因为MyTag定义时,使用了@Inherited修饰,具有了继承性,所以运行结果为true。
如果MyTag注解没有被@Inherited修饰,则运行结果为:false。
基本Annotation
JDK默认提供了如下几个基本Annotation:
@Override
1 | package java.lang; |
限定重写父类方法。对于子类中被@Override 修饰的方法,如果存在对应的被重写的父类方法,则正确;如果不存在,则报错。@Override 只能作用于方法,不能作用于其他程序元素。
@Deprecated
用于表示某个程序元素(类、方法等)已过时。如果使用被@Deprecated修饰的类或方法等,编译器会发出警告。
@SuppressWarning
抑制编译器警告。指示被@SuppressWarning修饰的程序元素(以及该程序元素中的所有子元素,例如类以及该类中的方法…..)取消显示指定的编译器警告。例如,常见的@SuppressWarning(value=”unchecked”)
@SafeVarargs
@SafeVarargs是JDK 7 专门为抑制“堆污染”警告提供的。
提取Annotation信息(反射)
当开发者使用了Annotation修饰了类、方法、Field等成员之后,这些Annotation不会自己生效,必须由开发者提供相应的代码来提取并处理Annotation信息。这些处理提取和处理Annotation的代码统称为APT(Annotation Processing Tool)。
JDK主要提供了两个类,来完成Annotation的提取:
java.lang.annotation.Annotation接口:这个接口是所有Annotation类型的父接口(后面会分析Annotation的本质,Annotation本质是接口,而java.lang.annotation.Annotation接口是这些接口的父接口)。
java.lang.reflect.AnnotatedElement接口:该接口代表程序中可以被注解的程序元素。
java.lang.annotation.Annotation
java.lang.annotation.Annotation接口源码:
1 | package java.lang.annotation; |
java.lang.annotation.Annotation接口的主要方法是annotationType( ),用于返回该注解的java.lang.Class。
java.lang.reflect.AnnotatedElement
java.lang.reflect.AnnotatedElement接口源码:
1 | package java.lang.reflect; |
主要方法有:
isAnnotationPresent(Class<? extends Annotation> annotationClass):判断该程序元素上是否存在指定类型的注解,如果存在则返回true,否则返回false。
getAnnotation(Class
annotationClass):返回该程序元素上存在的指定类型的注解,如果该类型的注解不存在,则返回null
Annotation[] getAnnotations():返回该程序元素上存在的所有注解。
java.lang.reflect.AnnotatedElement接口是所有程序元素(例如java.lang.Class、java.lang.reflect.Method、java.lang.reflect.Constructor等)的父接口。
所以程序通过反射获取了某个类的AnnotatedElement对象(例如,A类method1()方法的java.lang.reflect.Method对象)后,就可以调用该对象的isAnnotationPresent( )、getAnnotation( )等方法来访问注解信息。
使用反射获取注解信息
给定一个类的全额限定名,加载类,并列出该类中被注解@MyTag修饰的方法和没被修饰的方法。
注解定义:
1 | package com.demo1; |
注解处理:
1 | package com.demo1; |
测试类:
1 | package com.demo1; |
1 | package com.demo1; |
运行结果:
1 | 没被MyTag注解修饰的方法名:m1 |
注解本质
注解实质上会被编译器编译为接口,并且继承java.lang.annotation.Annotation接口。注解的成员变量会被编译器编译为同名的抽象方法。
额!我的天,看了这么老半天注释的原理,接下来我们进入正题来看看我们的Dagger框架。
引入Dagger
配置apt插件(在build.gradle(Project:xxx)中添加如下代码)
1 | dependencies { |
添加依赖(在build.gradle(Module:app)中添加如下代码)
1 | apply plugin: 'com.android.application' |
使用Dagger2
先看一个例子,MainActivity依赖Province,Province依赖City,City依赖Street;
使用前
Street.java
1 | public class Street { |
City.java
1 | public class City { |
Province.java
1 | public class Province { |
MainActivity.java
1 | public class MainActivity extends AppCompatActivity { |
可以看到,为了获取地址信息,在代码中需要实例化各种依赖到的对象,一旦依赖过多就容易影响代码阅读,那么配合使用@Inject和@Component又是怎样的呢?
使用后
Street.java
1 | public class Street { |
City.java
1 | public class City { |
Province.java
1 | public class Province { |
需要额外加一个接口,MainActivityComponent.java
1 |
|
此时的MainActivity.java
1 | public class MainActivity extends AppCompatActivity { |
前后的打印是一致的,可以看到MainActivity的中原本需要实例化对象的那些代码现在可以省略了,有助于我们更好地关注业务实现。回顾下使用注解的步骤:
build.gradle中添加dagger2依赖
使用@Inject标注在构造函数和被引用的成员变量上
新建MainActivityComponent接口,并用@Component标注
在MainActivity中执行DaggerMainActivityComponent.create().inject(this);(第一次需Rebuild Project)
Demo源码下载:https://github.com/lxqxsyu/TestDagger
1 | git clone git@github.com:lxqxsyu/TestDagger.git |
源码分析
我们在MainaActivity中加了DaggerMainActivityComponent.create().inject(this)这句代码替换了之前的一些实例化的操作,那么这句代码具体做了哪些工作?原来Dagger2会在编译过程中生成对应的依赖项,这些依赖项在Android Studio该路径下,如图所示:
DaggerMainActivityComponent.create()
1 | public static MainActivityComponent create() { |
可以看到,不管是通过builder().build()还是create(),最后都会调用DaggerMainActivityComponent构造函数;在通过源码阅读后,可以将整个过程分为两步,分别是initialize()和inject()
initialize()
1 | private DaggerMainActivityComponent(Builder builder) { |
在initialize()方法中对成员赋值,这里的成员分为两类,分别以_MembersInjector和_Factory为后缀;Xx_MembersInjector可以理解为注入器,用来实现Xx与内部成员的依赖:
1 | public interface MembersInjector<T> { |
Xx_Factory是创建Xx的工厂:
1 | public interface Provider<T> { |
这里先记住两者的功能,具体后面会分析
注释1
先看下Street_Factory.java
1 | public final class Street_Factory implements Factory<Street> { |
代码很简单,通过饿汉式创建了一个Street_Factory单例(这里的源码可能会有不同,之前看过一版是通过枚举创建的单例);再看下City_MembersInjector.java
1 | public final class City_MembersInjector implements MembersInjector<City> { |
City_MembersInjector只是将传进来的Street_Factory赋值给自己的成员变量;
注释2
后面的功能都一样,就用伪代码列出:
City_Factory.java
1 | public final class City_Factory implements Factory<City> { |
注释3
Province_MembersInjector.java
1 | public final class Province_MembersInjector implements MembersInjector<Province> { |
注释4
Province_Factory.java
1 | public final class Province_Factory implements Factory<Province> { |
注释5
1 | public final class MainActivity_MembersInjector implements MembersInjector<MainActivity> { |
到此目标工厂和注入器都已经创建完成,但是此时目标对象和依赖关系还没产生。
inject()
inject()通过调用injectMembers()完成真正目标对象实例化以及依赖操作,代码也没多少,就把整个完整的过程涉及到的代码贴出来:
1 | //DaggerMainActivityComponent.java |
可以看到inject()和initialize()刚好是相反的过程,直到找到依赖的源头完成源头对象的实例化,即这里的Street_Factory的get()方法的返回值;在Street实例化完成之后返回给City完成City实例化,City实例化完之后对自己的成员重新赋值了一遍,即产生依赖关系:
1 | /City_MembersInjector.java |
instance.street赋值的时候调用了streetProvider.get(),这是一个怎样的过程呢,还是画个图吧
step1的时候已经完成了Street和City的实例化,接着执行instance.street = streetProvider.get(),这句代码即下面step2的过程:
红色的就变成垃圾了,所以在这个过程中Street被new了两次,第一次作为创建City的条件,第二次在City创建完成后对自身成员变量赋值;继续分析创建Province以及建立依赖的过程:
可以看到在整个过程中Street其实new了4次,如果依赖更多的话(比如最外层再加个Country),Street在内存中被new的次数也会*2,不过过程中这些对象最终都会变成垃圾被回收(总觉得这是额外的开销,依赖多了岂不是编译就慢了?),而不用dagger2只要new一次就可以了(如下图),但是两者最终都是一条依赖链
总结
整个流程:
黑色的流程线是initialize()的过程,用来创建目标实例的工厂和注入器;红色流程线是inject()/injectMembers()的过程,用来创建目标实例以及实现依赖。最后在回过头来看下@inject和@component这两个标注,可以得出一些结论:
1.若一个类(Xx)的构造函数被@inject标注,则该类编译时会产生Xx_Factory;
2.若一个类中的成员变量被@inject标注,则该类编译时会产生Xx_MembersInjector;
3.@Component标注的接口(Xx)在编译时生成DaggerXx,负责将上面两个联系起来。
一个图
当然dagger2还有很多很强大的功能,之后我们再来看和mvp结构的项目如何整合。