IOC是Inversion of Control的缩写,多数书籍翻译成“控制反转”。
IOC理论提出的观点大体是这样的:借助于“第三方”实现具有依赖关系的对象之间的解耦,Martin Fowler探讨到,是“获得依赖对象的过程被反转了”。控制被反转之后,获得依赖对象的过程由自身管理变为了由IOC容器主动注入。于是,他给“控制反转”取了一个更合适的名字叫做“依赖注入(Dependency Injection)”,这篇博客中说的挺清楚的。
那么接下来讲一下Android中的依赖注入,咱们通常使用的Butterknife则使用到了这种方式,咱们来实现一个最简单的。
平时使用butterknife主要就使用了一个bindView方法,然后在就自动处理了findViewById和view的onClick等设置处理,其实这都是借助了编译时注解来生成辅助类处理的,所以也不损耗性能。今天咱们来简单看看BindView的注解,并生成一个辅助类来做findViewById处理。
简易过程如图:
大致就是:
- 编译时:通过注解处理器生成辅助类,添加辅助寻找findViewById代码
- 运行时:通过lib接口方法来调用辅助类对象,实现寻找到findViewById处理
另外这篇文章中除了注解处理器,还会用到啊两个三方框架:
- AutoService :作用是注解 processor 类,并对其生成 META-INF 的配置信息.
- javapoet:用于生成java.class文件
接下来进入正题
一. 创建所需library
- apt-annotation:java-library,注解存放库,用于存放咱们要使用的注解
- apt-processor:java-library,注解解析库,用于进行解析编译时注解并生成咱们这里的绑定空间id的辅助类。依赖:apt-annotation、auto-service、javapoet
- bt-library:android-library,外观类存放库,用于提供外观类给外界调用,内部调用apt-processor编译生成出来的类对象。依赖:apt-annotation
- app:application工程,依赖:bt-library、apt-processor
项目结构:
二. 开始
(1) apt-annotation添加注解
- 在apt-annotation中创建咱们要使用的注解,这里是BindView
/**
* 1. 编译时注解
* 2. 目标类型是给Field字段用
*/
@Retention(RetentionPolicy.CLASS)
@Target(ElementType.FIELD)
public @interface BindView {
/**
* 该注解存入int类型的值
*/
int value();
}
(2) apt-processor进行解析注解
- apt-processor库的gradle配置如下:
apply plugin: 'java-library'
dependencies {
//引用存放注解的lib,目的可以使用到定义的注解类
implementation project(':apt-annotation')
//引用auto-service,AutoService 主要的作用是注解 processor 类,并对其生成 META-INF 的配置信息
implementation 'com.google.auto.service:auto-service:1.0-rc2'
//javapoet 生成java源文件
implementation group: 'com.squareup', name: 'javapoet', version: '1.11.1'
}
//jdk版本
sourceCompatibility = "1.8"
targetCompatibility = "1.8"
依赖说明:
依赖 | 说明 |
---|---|
apt-annotation | 注解类提供 |
auto-service | 注解 processor 类,并对其生成 META-INF 的配置信息 |
javapoet | 生成java源文件框架 |
- 解析编译时注解
解析编译时注解,主要使用一个Processor接口,那么咱们自己写一个BindViewProgresser实现Processor接口。我们这里通过继承AbstractProcessor的方式,其内部有一个protected的全局变量processingEnv,它有一些工具处理,比如:
工具 | 说明 |
---|---|
Filer | 写文件相关,后面输出生成的文件到包中就需要它 |
Messager | 编译处理过程中的打印日志相关 |
Elements | Element的工具方法,可以获取包信息等 |
好了,我们开始看看咱们要实现的BindViewProgresser:
BindViewProgresser继承自AbstractProcessor,要实现的接口有几个主要使用的方法:
签名 | 说明 |
---|---|
void init(ProcessingEnvironment processingEnvironment) | init()方法会被注解处理工具调用,并输入ProcessingEnviroment参数。 ProcessingEnviroment提供很多有用的工具类Elements, Types 和 Filer, @param processingEnvironment 提供给 processor 用来访问工具框架的环境 |
SourceVersion getSupportedSourceVersion() | 指定使用的Java版本,通常这里返回SourceVersion.latestSupported() |
Set getSupportedAnnotationTypes() | 这里必须指定,这个注解处理器是注册给哪个注解的。注意,它的返回值是一个字符串的集合,包含本处理器想要处理的注解类型的合法全称 @return 注解器所支持的注解类型集合,如果没有这样的类型,则返回一个空集合 |
boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) | 这相当于每个处理器的主函数main(),你在这里写你的扫描、评估和处理注解的代码,以及生成Java文件。 输入参数RoundEnviroment,可以让你查询出包含特定注解的被注解元素 @param set 请求处理的注解类型 @param roundEnvironment 有关当前和以前的信息环境 @return 如果返回 true,则这些注解已声明并且不要求后续 Processor 处理它们;如果返回 false,则这些注解未声明并且可能要求后续 Processor 处理它们 |
- 先看看init方法:
在初始化这里咱们需要从环境里获取如下两个对象后面使用:
- 打印日志的
- 用于写入文件对象
/**
* init()方法会被注解处理工具调用,并输入ProcessingEnviroment参数。
* ProcessingEnviroment提供很多有用的工具类Elements, Types 和 Filer
*
* @param processingEnvironment 提供给 processor 用来访问工具框架的环境
*/
@Override
public synchronized void init(ProcessingEnvironment processingEnvironment) {
super.init(processingEnvironment);
//mMessager用于打印日志
mMessager = processingEnvironment.getMessager();
//filer用于将最后的生成代码输出到咱们编译的文件中
filer = processingEnvironment.getFiler();
}
- 再看看getSupportedAnnotationTypes()方法:
咱们这个注解解析只有BindView注解,所以只添加一个,否则添加可以把需要解析的都添加到Set中
/**
* 这里必须指定,这个注解处理器是注册给哪个注解的。注意,它的返回值是一个字符串的集合,包含本处理器想要处理的注解类型的合法全称
*
* @return 注解器所支持的注解类型集合,如果没有这样的类型,则返回一个空集合
*/
@Override
public Set<String> getSupportedAnnotationTypes() {
//这里是BindView注解的
Set<String> supportTypes = new LinkedHashSet<>();
supportTypes.add(BindView.class.getCanonicalName());
return supportTypes;
}
- 接着咱们的重头戏process方法:
public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment)
process作为每个注解编译器被调用的核心方法,有两个参数,第一个:请求处理的注释类型。我们主要使用第二个roundEnvironment,可以用它的方法:
Set<? extends Element> getElementsAnnotatedWith(Class<? extends Annotation> var1)
可看到是传入一个注解类型,然后返回一个Element的Set,那么注解类型就是咱们要解析的注解类型,这里的:BindView.class
- 所以第一步: 获取
//1. 得到所有包含注解的元素
Set<? extends Element> elementsAnnotatedWith = roundEnvironment.getElementsAnnotatedWith(BindView.class);
- 解析,获取到包含注解对象的相关信息
先了解一下后面会用到的接口:
名称 | 说明 | 使用 |
---|---|---|
Element | 一个接口,程序元素,如程序包,类或方法。 每个元素表示静态的语言级结构(而不是虚拟机的运行时结构)。 | 使用其类型为VariableElement的使用 |
VariableElement | 一个接口,表示一个字段, 枚举恒定,方法或构造参数,局部变量,变量资源,或异常参数 | 使用其getEnclosingElement()方法,返回此变量的包围元素。 方法或构造函数参数的封闭元素是声明参数的可执行文件,即TypeElement getSimpleName()方法获取其元素名字,就是放咱们注解的控件名称 asType()方法获取类型,比如android.widget.TextView类型 |
TypeElement | 一个接口,表示一个类或接口程序元素。 提供关于类型及其成员的信息的访问。 注意,枚举类型是一种类,注释类型是一种接口。 | 使用其getQualifiedName()方法,返回此类型元素的完全限定名称。 更准确地说,它返回规范名称,比如 java.util.Map.Entry |
如下代码:获取了注解元素所在类的类信息、注解中的值
//2. 遍历注解Element
for (Element element : elementsAnnotatedWith) {
/*检查类型*/
if (!(element instanceof VariableElement)) {
return false;
}
VariableElement variableElement = (VariableElement) element;
/*获取类*/
TypeElement typeElement = (TypeElement) variableElement.getEnclosingElement();
/*类的绝对路径,比如 xin.iocdemo.MainActivity*/
String qualifiedName = typeElement.getQualifiedName().toString();
mMessager.printMessage(Diagnostic.Kind.NOTE, "qualifiedName:" + qualifiedName);
/*所在类名 : MainActivity*/
String clsName = typeElement.getSimpleName().toString();
mMessager.printMessage(Diagnostic.Kind.NOTE, "clsName:" + clsName);
/*获取包名 : xin.iocdemo*/
String packageName = processingEnv.getElementUtils().getPackageOf(typeElement).getQualifiedName().toString();
mMessager.printMessage(Diagnostic.Kind.NOTE, "packageName:" + packageName);
/*获取到该元素的注解其中包含的值*/
BindView annotation = variableElement.getAnnotation(BindView.class);
int id = annotation.value();
/*元素名,这里就是控件的名字*/
String name = variableElement.getSimpleName().toString();
/*元素对象类,即控件的类型*/
String type = variableElement.asType().toString();
mMessager.printMessage(Diagnostic.Kind.NOTE, "type:" + type);
}
- 得到相关信息后,就是写入文件了
写入文件咱们使用的是javapoet库,相对完全自己用StringBuilder来组装好太多了,具体API也可查看Github上的说明。
/*3. 接下来则开始编写辅助类文件*/
//获取接口的名字,这里就是咱们要提供inject方法的ViewInjecter,采用getName的方式,全路径名,这样生成的包会自动有import,否则没有
ClassName InterfaceName = ClassName.bestGuess(ViewInjecter.class.getName());
//获取Activity的名字,用于作为参数传进去,也用于创建类的名称前缀,比如:MainActivity
ClassName host = ClassName.bestGuess(qualifiedName);
//准备创建方法,名称inject
MethodSpec main = MethodSpec.methodBuilder("inject")
//公共方法
.addModifiers(Modifier.PUBLIC)
//返回类型为void
.returns(void.class)
//添加Override注解
.addAnnotation(Override.class)
//添加参数host即咱们的MainActivity
.addParameter(host, "host")
//添加参数Object
.addParameter(Object.class, "object")
//添加方法内部代码,这里则为咱们平时的findviewById的代码,分为是在Activity中查找,和在view中查找(比如Fragment)
.addCode(""
+ " if(object instanceof android.app.Activity){\n"
+ " host." + name + " = (" + type + ")(((android.app.Activity)object).findViewById(" + id + "));\n"
+ " }\n"
+ "else{\n"
+ " host." + name + " = (" + type + ")(((android.view.View)object).findViewById(" + id + "));\n"
+ "}\n")
.build();
//创建类,名称为类名_ViewInjector
TypeSpec helloWorld = TypeSpec.classBuilder(clsName + "_ViewInjector")
.addModifiers(Modifier.PUBLIC)
.addMethod(main)
//添加实现接口ViewInjector<MainActivity>
.addSuperinterface(ParameterizedTypeName.get(InterfaceName, host))
.build();
try {
// 生成 xin.iocdemo.MainActivity_ViewInjector.java
JavaFile javaFile = JavaFile.builder(packageName, injector)
.addFileComment(" This codes are generated automatically. Do not modify!")
.build();
// 写入到文件 在咱们build/intermediates/classes/目录下
javaFile.writeTo(filer);
} catch (IOException e) {
e.printStackTrace();
}
如上,创建一个MainActivity_ViewInjector就完成了,创建后的代码如下:
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//
package xin.iocdemo;
import android.app.Activity;
import android.view.View;
import android.widget.TextView;
import xin.apt.annotation.ViewInjecter;
public class MainActivity_ViewInjector implements ViewInjecter<MainActivity> {
public MainActivity_ViewInjector() {
}
public void inject(MainActivity host, Object object) {
if (object instanceof Activity) {
host.testView = (TextView)((TextView)((Activity)object).findViewById(2131165260));
} else {
host.testView = (TextView)((TextView)((View)object).findViewById(2131165260));
}
}
}
(3) bt_library创建XinBind类:
bt-library的依赖:
//引用注解处理,供主工程可使用到注解
api project(':apt-annotation')
然后咱们再在bt_library中创建一个工具类,提供一个bind方法供外界调用,来间接根据规则来调用前面创建好的类来初始化控件,可以传入Activity或者传入Fragment和view:
/**
* Bind接口类
*/
public class XinBind {
/**
* 供Ativity调用
* @param activity
*/
public static void bind(Object activity) {
bind(activity, activity);
}
/**
* 供Fragment或者其他调用
* @param host 携带控件的对象
* @param root 具体的View
*/
public static void bind(Object host, Object root) {
Class<?> clazz = host.getClass();
String proxyClassFullName = clazz.getName() + "_ViewInjector";
try {
Class<?> proxyClazz = Class.forName(proxyClassFullName);
ViewInjecter viewInjector = (ViewInjecter) proxyClazz.newInstance();
viewInjector.inject(host, root);
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
}
到此,库的编写完成。
(4) 在主工程Activity中调用bind:
- 主工程依赖:
//bind类库
implementation project(':bt-library')
//注解编译器
annotationProcessor project(':apt-processor')
- 在MainActivity中调用bind方法
XinBind.bind(this);
这样即可完成绑定了。
这里讲的是只有一个控件创建了一个inject类,最基础最简单就这个样子。
复杂后,对于一个对象中有多个控件的那么在生成代码那里就要做更多的操作,然后再想想还要加onClick注解等,那么需要对比注入的id值来处理,一连串下来还是非常复杂的。
结束语:编译时注解十分强大,给了我们很多的想象空间。了解他,没道理