前言
在许多程序设计语言中,比如Java、C#。依赖注入是一种比较流行的设计模式,在android开发中也有很多实用的依赖注入框架,可以帮助我们少些一些样板代码,达到各个类之间解耦的目的。
注解
从JDK 5开始,Java增加了注解,注解就是代码里的特殊标记,这些标记可以在编译、类加载、运行时被读取,并执行相应的处理。通过使用注解,开发人员可以在不改变原有逻辑的情况下,在源文件中嵌入一些补充的信息。代码分析工具和部署工具可以通过这些补充信息进行验证、处理或者进行部署。
注解分类
注解分为标准注解和元注解。
标准注解
标准注解有以下4种。
- @Override:对覆盖超类中的方法进行标记,如果被标记的方法并没有实际覆盖超类中的方法,则编译器会发出错误警告。
- @Deprecated:对不鼓励使用或者已过时的方法添加注解,当编译人员使用这些方法时,将会在编译时显示提示信息。
- @SuppressWarnings:选择性的取消特定代码段中的警告。
- @SafeVarargs:JDK1.7新增,用来声明使用了可变长参数的方法,其在与泛型类一起使用时不会出现类型安全问题。
元注解
除了标准注解,还有元注解,它用来创建新的注解。元注解有以下几种。
- @Targe:注解所修饰的对象范围。
- @Inherited:表示注解可以被继承。
- Documented:表示这个注解应该被JavaDoc工具记录。
- Retention:用来声明注解的保留策略。
- Repeatable:JDK 8新增,允许一个注解在同一声明类型(类、属性或者方法)上多次使用。
其中@Targe注解取值是一个ElementType类型的数组,其中有以下几种取值,对应不同的对象范围。
- ElementType.TYPE:能修饰类、接口或枚举类型。
- ElementType.FIELD:能修饰成员变量。
- ElementType.METHORD:能修饰方法。
- ElementType.PARAAMETER:能修饰参数。
- ElementType.CONSTRUCTOR:能修饰构造函数。
- ElementType.LOCAL_VARIABLE:能修饰局部变量。
- ElementType.ANNOTATION_TYPR:能修饰注解。
- ElementType.PACKAGE:能修饰包。
- ElementType.TYPE_PARAMENTER:类型参数声明。
- ElementType.TYPE_USE:使用类型。
其中@Retention注解有3种类型,分别表示不同级别的保留策略。
- RetentionPolicy.SOURCE:源码级注解。注解信息只会保留在.java源码中,源码在编译后,注解信息被丢弃,不会保留在.class中。
- RetentionPolicy.CLASS:编译时注解。注解信息会保留在.java源码以及.class中。当运行Java程序时,JVM会丢弃该注解信息,不会保留在JVM中。
- RetentionPolicy.RUNTIME:运行时注解。当运行Java程序时,JVM也会保留该注解信息,可以通过反射获取该注解信息。
定义注解
基本定义
定义新的注解类型使用@interface关键字,这与定义一个接口很像,如下所示。
123public Swordsman {...}定义完注解后,就可以在程序中使用该注解。
1234public class AnnotationTest {...}定义成员变量
注解只有成员变量,没有方法。注解的成员变量在注解定义中以“无形参的方法”形式来声明,其“方法名”定义了该成员变量的名字,其返回值定义了该成员变量的类型。
1234public Swordsman {String name();int age();}上面的代码定义了两个成员变量,这两个成员变量以方法的形式来定义。定义了成员变量后,使用该注解时就应该为该注解的成员变量指定值。
123456public class AnnotationTest {"username", age = 20)(name =public void fighting() {...}}也可以在定义注解的成员变量时,使用default关键字为其指定默认值,如下所示。
1234public Swordsman {String name() default "username";int age() default 20;}因为注解定义了默认值,所以使用时可以不为这些成员变量指定值,而是直接使用默认值。
12345public class AnnotationTest {public void fighting() {...}}定义运行时注解
可以用@Retention来设定注解的保留策略,这3个策略的生命周期长度为SOURCE < CLASS < RUNTIME。生命周期短的能起作用的地方,生命周期长的一定也能起作用。一般如果在运行时去动态获取注解信息,那只能用RetentionPolicy.RUNTIME;如果要在编译时进行一些预处理操作,比如生成一些辅助代码,就用RetentionPolicy.CLASS;如果只是做一些检查型的操作,比如@Override和@SuppressWarnings,则可选用RetentionPolicy.SOURCE。当设定为RetentionPolicy.RUNTIME时,这个注解就是运行时注解,如下所示。
12345(RetentionPolicy.RUNTIME)public Swordsman {String name() default "username";int age() default 20;}定义编译时注解
同样地,如果将@Retention的保留策略设定为RetentionPolicy.CLASS,这个注解就是编译时注解,如下所示。
12345(RetentionPolicy.CLASS)public Swordsman {String name() default "username";int age() default 20;}
注解处理器
如果没有处理注解的工具,那么注解也不会有什么大的用处。对于不同的注解有不同的注解处理器。虽然注解处理器的编写会千变万化,但是其也有处理标准,比如,针对运行时注解会采用反射机制处理,针对编译时注解会采用AbstractProcessor来处理。
运行时注解处理器
处理运行时注解需要使用到反射机制。首先我们要定义运行时注解,如下所示。
123456(METHOD)(RUNTIME)public GET {String value() default "";}上面的代码是Retrofit中定义的@GET注解。其定义了@Target(METHOD),这等效于@Target(ElementType.METHOD),意味着GET注解应用于方法。接下来应用该注解,如下所示。
12345678910public class AnnotationText {"http://ip.taobao.com/59.108.54.37")(value =public String getIpMsg() {return "";}"http://ip.taobao.com/")(value =public String getIp() {return "";}}上面的代码为@GET的成员变量赋值。接下来写一个简单的注解处理器,如下所示。
123456789public class AnnotationProcessor {public static void main(String[] args) {Method[] methods = AnnotationTest.class.getDeclaredMethods();for (Method m : methods) {GET get = m.getAnnotation(GET.class);Systen.out.println(get.value());}}}上面的代码用到了两个反射方法:getDeclaredMethods和getAnnotation,它们都属于AnnotatedElement接口,Class、Method和Field等类都实现了该接口。调用getAnnotation方法返回指定类型的注解对象,也就是GET。最后调用了GET的value方法返回从GET对象中提取的元素的值。输出结果为:
12http://ip.taobao.com/59.108.54.37http://ip.taobao.com/编译时注解处理器
处理编译时注解的步骤如下。
① 定义注解
这里首先在项目中新建一个Java Library来专门存放注解,这个Library名为annotations。接下来定义注解,如下所示。
12345(CLASS)(FIELD)public BindView {int value() default 1;}② 编写注解处理器
在项目中在新建一个Java Library来存放注解处理器,这个Library名为processor。接着来配置processor库的build.gradle。
1234567apply plugin: 'java'dependencies {compile fileTree(include: ['*.jar'], dir, 'libs')compile project(':annotations')}sourceCompatibility = "1.7"targetCompatibility = "1.7"接下来编写注解处理器ClassProcessor,它继承AbstractProcessor,如下所示。
123456789101112131415161718192021public class ClassProcessor extends AbstractProcessor {public synchronized void init(ProcessingEnviroment processingEnv) {super.init(processingEnv);}public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {...return true;}public Set<String> getSupportedAnnotationTypes() {Set<String> anotations = new LinkedHashSet<String>();annotations.add(BindView.class.getCanonicalName());return annotations;}public SourceVersion getSupportedSourceVersion() {return SourceVersion.lastestSupported();}}首先分别介绍这4个方法的作用。
- init:被注解处理工具调用,并输入ProcessingEnviroment参数。ProcessingEnviroment提供很多有用的工具类,比如Element、Types、Filer和Messager等。
- process:相当于每个处理器的主函数main(),在这里写你的描述、评估和处理注解的代码,以及生成Java文件。输入参数RoundEnviroment,可以让你查询出包含特定注解的被注解元素。
- getSupportedAnnotationTypes:这是必须指定的方法,指定这个注解处理器是注册给哪个注解的。它的返回值是一个字符串的集合,包含本处理器想要处理的注解类型的合法全称。
- getSupportedSourceVersion:用来指定使用的Java版本,通常这里返回SourceVersion.lastestSupported()。
在Java 7 之后,也可以使用注解来代替getSupportedAnnotationTypes方法和getSupportedSourceVersion方法,如下所示。
12345678910111213(SourceVersion.RELEASE_8)"com.example.annotation.cls.BindView")(public class ClassProcessor extends AbstractProcessor {public synchronized void init(ProcessingEnviroment processingEnv) {super.init(processingEnv);}public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {...return true;}}但是考虑到android兼容性的问题,不建议采用这种注解的方式。接下来编写还未实现的process方法,如下所示。
1234567891011public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {Messager messager = processingEnv.getMessager();for (Element elemnt : roundEnv.getElementsAnnotatedWith(BindView.class)) {if (element.getKind() == ElementKind.FIELD) {messager.printMessage(Diagnostic.Kind.NOTE, "printMessaage:" + element.toStrintg());}}...return true;}这里用到Messager的printMessage方法来打印出注解修饰的成员变量的名称。
③ 注册注解处理器
为了能使用注解处理器,需要用一个服务文件来注册它。要创建这个服务文件,首先在processor库的main目录下resources资源文件夹,接下来在resources中再建立META-INF/services目录文件夹。最后在META-INF/services中创建javax.annotation.processing.Processor文件,这个文件中的内容是注解处理器的名称。也可以使用Google开源的AutoService,它用来帮助开发者生成META-INF/service/javax.annotation.processing.Processor文件。首先需要添加一个开源库,也可以在processor的build.gradle中直接添加如下代码。
1234dependencies {...complie 'com.google.auto.service:auto-service:1.0-rc2'}最后在注解处理器ClassProcessor中添加@AutoService(Processor.class)就可以了。
1234(Processor.class)public class ClassProcessor extends AbstractProcessor {...}④ 应用注解
接下来在我们定位主工程项目(app)中引用注解。首先要在主工程项目的build.gradle中引用annotations和processor这两个库。
12345dependencies {...complie project(':annotations')complie project(':processor')}接下来在MainActivity中应用注解,如下所示。
123456789public class MainActivity extends AppCompatActivity {TextView tv_text;protected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);}}最后,clean project在make project,在Gradle Console窗口中打印的结果就是@BindView注解修饰的成员变量名:tv_text。
⑤ 使用android-apt插件
我们在主工程项目(app)中引用了processor库,但注解处理器只在编译处理期间需要用到,编译处理完后就没有实际作用了,而在主工程项目添加了这个库会引入很多不必要的文件。为了处理这个问题我们需要引入插件android-apt。它主要有两个作用:
- 仅仅在编译时期去依赖注解处理器所在函数库并进行工作,但不会打包到APK中。
- 为注解处理器生成的代码设置好路径,以便android studio能够找到它。
接下来介绍如何使用它。首先需要在整个工程(project)的build.gradle中添加如下语句。
1234567buildscript {...dependencies {...classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8'}}接下来在主工程项目(app)的build.gradle中以apt的方式引入注解处理器processor,如下所示。
12345678...apply plugin: 'com.neenbedankt.android-apt'...dependencies {...// complie project(':processor')apt project(':processor')}
依赖注入的原理
控制反转与依赖注入
控制反转
Inversion of Control(IoC)。IoC理论提出的观点大体是这样的:借助于“第三方”实现具有依赖关系的对象之间的解耦。在软件系统没有引入IoC容器之前,对象之间相互依赖,假设对象A依赖与对象B,那么对象A在初始化或者运行到某一点的时候,自己必须手动去创建对象B或使用已经创建的对象B,无论是创建对象B还是使用对象B,控制权都在自己手上。软件系统在引入IoC容器之后,情形就完全改变了,由于IoC容器的加入,对象A与对象B之间失去了直接联系,所以,当对象A运行到需要对象B的时候,IoC容器会主动创建一个对象B注入到对象A需要的地方。通过引入IoC容器前后的对比,可以看出:对象A获得依赖对象B的过程,由主动行为变为被动行为,控制权颠倒过来了,这就是控制反转这个名称的由来。
依赖注入
控制反转之后,获得依赖对象的过程由自身管理变为由IoC容器主动注入。于是,控制反转有了另一个更合适的名字,叫做依赖注入(Dependency Injection),简称DI。所谓依赖注入,是指IoC容器在运行期间,动态地将某种依赖关系注入到对象中去。
依赖注入的实现方式
编写代码时常常会发现有一些类是依赖于其他类的,所以类A可能需要一个类B的引用或对象。举个例子。
|
|
代码本身没有错,但是Car和Engine高度耦合,在Car中需要自己创建Engine,并且Car还需要Engine的实现方式,也就是Engine的实现类PetrolEngine的存在。另外,一旦Engine的类型变为其他的实现比如DieselEngine,则需要修改Car的构造方法。以上问题需要用依赖注入来解决。接下来,就用依赖注入的3种常用方式来改造上面的代码。
构造方法注入
通过Car的构造方法,向Car传递了Engine对象,如下所示。
123456public class Car {private Engine mEngine;public Car(Engine engine) {this.mEngine = engine;}}Setter方法注入
通过Car的set方法向Car传递Engine对象,如下所示。
123456public class Car {private Engine mEngine;public void set(Engine engine) {this.mEngine = engine;}}接口注入
在接口中定义需要注入的信息,并通过接口完成注入,如下所示。
123public interface ICar {public void setEngine(Engine engine);}1234567public class Car implements ICar {private Engine mEngine;public void setEngine(Engine engine) {this.mEngine = engine;}}
通过以上3种注入方法,明显将Car和Engine解耦了。Car不关心Engine的实现,即使Engine的类型变换了,Car也无须做任何修改。
依赖注入框架
android目前主流的依赖注入框架有ButterKnife和Dagger2。