前言:
在上篇文章中讲解了通过IOS(依赖注入)的方式来为View创建对象并设置事件监听 从而简化我们的代码 方便维护 但是其缺点就是:在运行时通过大量的注解反射去执行,在性能上有所欠缺。
本篇将带大家去了解通过APT(Annotation Processing Tools)的方式在.java文件编译为.class文件时动态的创建对象 其优点就是:对象的创建在编译时就已经确立,对程序运行性能上没有影响。目前网上使用APT技术比较火的库有:
butterknife
dagger
Retrofit
APT
介绍:
APT(Annotation Processing Tool):是一种处理注释的工具,它对源代码文件进行检测找出其中的Annotation,使用Annotation处理器进行额外的处理。 Annotation处理器在处理Annotation时可以根据源文件中的Annotation生成额外的源文件和其它的文件(文件具体内容由Annotation处理器的编写者决定),APT还会编译生成的源文件和原来的源文件,将它们一起生成class文件。
Annotation处理器(注解处理器):是一个在javac中的,用来编译时扫描和处理的注解的工具。你可以为特定的注解,注册你自己的注解处理器
核心原理:
编译时Annotation解析的基本原理是,在某些代码元素上(如类型、函数、字段等)添加注解,在编译时javac编译器会检查AbstractProcessor的子类,并且调用该类型的process函数,然后将添加了注解的所有元素都传递到process函数中,使得开发人员可以在编译器进行相应的处理,例如,根据注解生成新的Java类,这也就是butterknife dagger等开源库的基本原理
案例
上面介绍了那么一大堆文绉绉的东西(我看着也烦) ,还是来点代码实际:
public class MainActivity extends AppCompatActivity {
@BindView(R.id.textView)
TextView textview;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
InjectView.bind(this);
textview.setText("我是通过APT编译时生成的对象文件");
}
写法跟IOC的方式相同,但是实现原理却不一样,使用APT编译后其实是在MainActivity的类里面创建了一个内部类 其中内部类是在编译时通过APT生成的 最终也编译成.class文件:
public class MainActivity extends AppCompatActivity {
TextView textview;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
InjectView binView=new InjectView();
binView.bind(this);
textview.setText("我是通过APT编译时生成的对象文件");
}
public class InjectView{
public void bind(MainActivity mainActivity){
mainActivity.textview= (TextView) mainActivity.findViewById(R.id.textView);
}
}
}
使用
正式开车,在项目中使用APT 需要做如下配置操作
- 在项目的gradle中配置:
buildscript {
repositories {
jcenter()
mavenCentral()//--->添加mavenCentral仓库
}
dependencies {
classpath 'com.android.tools.build:gradle:2.3.0'
classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8'//--->导入APT
}
}
- 除了你的工程Module 还需要创建一个 Android Library 和两个java Library
- 创建好后进行引用
首先APP的gradle
apply plugin: 'com.neenbedankt.android-apt'//导入apt插件
dependencies {
compile project(':injectlibrary')//导入Android Library库
apt project(':inject-complier')//将java Library(inject-complier) 作为apt
}
Android Library(injectlibrary)库的gradle
dependencies {
compile project(':inject-annotion')//引入java Library(inject-annotion)java工程
}
inject-complier 的gradle
dependencies {
compile project(':inject-annotion')////引入java Library(inject-annotion)java工程
compile 'com.google.auto:auto-common:0.8'
compile 'com.google.auto.service:auto-service:1.0-rc3'
compile 'com.squareup:javapoet:1.8.0'//生成java代码的辅助工具类
}
- 他们之间的引用关系及作用
APP: 这个不用讲是我们的主工程
Android Library: 库里定义了InjectView等始化相关的类
Inject-annotion: 该Java工程里面只负责声明我们需要的自定义注解 比如@Onclick等
Inject-complier: 该Java工程会被APP作为APT插件使用 里面声明了AbstractProcessor的子类(注解处理器) 也是本章的重点
- 上面步骤做完后就开始写代码 先从java工程Inject-annotion开始
Inject-annotion里面只创建我们需要的自定义注解:
@Target(ElementType.FIELD)//声明在成员变量上面
@Retention(RetentionPolicy.CLASS)//编译时运行
public @interface BindView {
int value();
}
- Android Library:里面定义一些初始化的相关类:
public interface ViewBinder <T>{
void bind(T tartget);
}
public class InjectView {
public static void bind(Activity activity) {
//类名
String clsName = activity.getClass().getName();
try {
//加载内部类
Class<?> viewBidClass = Class.forName(clsName + "$$ViewBinder");
//创建内部类对象
ViewBinder viewBinder = (ViewBinder) viewBidClass.newInstance();
//执行内部类方法
viewBinder.bind(activity);
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
}
ViewBinder是一个接口 在APT编译时凡有用到自定义注解的类都会在该类创建一个内部类 并且实现我们定义的ViewBinder接口 并在接口的回调方法里做一些对象创建 初始化的操作。
这时我们在Activity中就可以使用了,但是运行肯定会报错,因为通过@BindViewz注解的成员变量还没赋值
public class MainActivity extends AppCompatActivity {
@BindView(R.id.textView)
TextView textview;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
InjectView.bind(this);
textview.setText("我是通过APT编译时生成的对象文件");
}
}
- Inject-complier 本篇的重点注解处理器 同时需要注意的是在编写AbstractProcessor(注解处理器)的时候一定要细心 因为该类会在编译时执行出错的话是没办法调试的 (PS:心里的苦说不出)
首先创建一个类继承 AbstractProcessor(注解处理器) 并做相关初始化操作 注释都做的非常详细 其中@AutoService(Processor.class)这个注解一定不要忘记添加 就是通它来标示该类可以处理我们自定义注解的能力 在JAVAC编译时源码中遇到我们自定义的注解时都会交由这个类来编译处理 这也是为什么我们能在编译时向源码中添加代码的原因
@AutoService(Processor.class)//该标记表明可以处理注解的能力
public class BindViewProcessor extends AbstractProcessor {
private Elements elementUtil;//处理节点的工具类
private Types typesUtil;//类型工具类
private Filer filer;//生成java文件的辅助类
@Override
public synchronized void init(ProcessingEnvironment processingEnvironment) {
super.init(processingEnvironment);
elementUtil = processingEnvironment.getElementUtils();
typesUtil = processingEnvironment.getTypeUtils();
filer = processingEnvironment.getFiler();
}
/**
* 包含本处理器想要处理的注解类型的合法全称。换句话说,你在这里定义你的注解处理器注册到哪些注解上。
*比如:@BindView @OnClick
* @return
*/
@Override
public Set<String> getSupportedAnnotationTypes() {
//创建Set集合添加需要支持的自定义注解
Set<String> types = new LinkedHashSet<>();
types.add(BindView.class.getCanonicalName());
types.add(OnClick.class.getCanonicalName());
return types;
}
/**
* 支持JDK最新版本
* @return
*/
@Override
public SourceVersion getSupportedSourceVersion() {
return SourceVersion.latestSupported();
}
上面的只是一些初始化的操作 真正核心的方法是AbstractProcessor的process方法 整个APP中我们定义的注解都会传递到这里 供我们编程处理 我将分为两段来讲
@Override
public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
//存放着整个App的所有注解类型 以类为Key 类下的注解成员变量为value
Map<TypeElement, List<FieldViewBinding>> targetMap = new HashMap<>();
//遍历整个app的BindView注解成员变量 并将该类 和成员变量保存到Map中
setElemtBindView(roundEnvironment, targetMap, BindView.class);
//向源码中添加代码
addClassAndMethod(targetMap);
return false;
}
首先setElemtBindView(roundEnvironment, targetMap, BindView.class); 这个方法主要是得到整个app含有@BindView注解的类
并以类名为key 对声明了@BindView注解的成员变量 获取其 id,成员变量名,成员变量类型 保存到一个对象中再添加到List集合 并作为Map的Value
private void setElemtBindView(RoundEnvironment roundEnvironment, Map<TypeElement, List<FieldViewBinding>> targetMap, Class<BindView> annotation) {
//得到整个app含有@BindView注解的类 Element代表类结构
for (Element element : roundEnvironment.getElementsAnnotatedWith(annotation)) {
//获取类名
TypeElement enclosingElement = (TypeElement) element.getEnclosingElement();
//根据类名获取所有的注解
List<FieldViewBinding> list = targetMap.get(enclosingElement);
if (null == list) {
list = new ArrayList<>();
targetMap.put(enclosingElement, list);
}
//获取控件id 自定义注解@BindView的value返回值
int id = element.getAnnotation(annotation).value();
//获取成员变量的名--->titleText
String fieldName = element.getSimpleName().toString();
//获取成员变量类型信息---->TextView
TypeMirror typeMirror = element.asType();
FieldViewBinding fieldViewBinding = new FieldViewBinding(fieldName, typeMirror, id);
list.add(fieldViewBinding);
}
}
创建一个类来保存获取到的(成员变量名称,成员变量类型,布局中id值)等相关信息
public class FieldViewBinding {
private String name; //成员变量名称
private TypeMirror typeMirror;//成员变量类型
private int resId;//布局中id值
public FieldViewBinding(String name, TypeMirror typeMirror, int resId) {
this.name = name;
this.typeMirror = typeMirror;
this.resId = resId;
}
public String getName() { return name; }
public TypeMirror getTypeMirror() { return typeMirror; }
public int getResId() { return resId;}
}
其中涉及到的知识点Element : Element代表一个类的结构 其对应关系
package com.example; ---> PackageElement
public class 类名 { ---> TypeElement
private int 成员变量; ---> VariableElement
private Foo 成员变量; ---> VariableElement
public 构造函数 () {} ---> ExecuteableElement
public void 普通方法 ( ---> ExecuteableElement
方法形参 ---> TypeElement
) {}
}
在来看addClassAndMethod(targetMap);方法 该方法就是向源码中添加代码的主要逻辑
private void addClassAndMethod(Map<TypeElement, List<FieldViewBinding>> targetMap) {
//遍历Map
for (Map.Entry<TypeElement, List<FieldViewBinding>> item : targetMap.entrySet()) {
List<FieldViewBinding> list = item.getValue();
if (null == list || list.size() == 0) {
continue;
}
//类类型 com.example....MainActivity
TypeElement typeElement = item.getKey();
//获取包名 com.example...
String packageName = getPackageName(typeElement);
//根据包名获取类名 MainActivity
String className = getClassName(typeElement, packageName);
//类型 <T extends MainActivity>
ClassName name = ClassName.bestGuess(className);
//获取我们定义的接口包名 和类名
ClassName viewBinder = ClassName.get("com.example.injectlibrary", "ViewBinder");
//生成java类 MainActivity$$ViewBinder
TypeSpec.Builder result = TypeSpec.classBuilder(className + "$$ViewBinder")
.addModifiers(Modifier.PUBLIC)//将该类声明为public
.addTypeVariable(TypeVariableName.get("T", name))//声明该类的类型 <T extends MainActivity>
.addSuperinterface(ParameterizedTypeName.get(viewBinder, name));//该类的实现接口 以及接口类型
//生成方法
MethodSpec.Builder methodBuilder = MethodSpec.methodBuilder("bind")//方法名 "bind"与ViewBinder接口中的回调保持一致
.addModifiers(Modifier.PUBLIC)//方法声明为public
.returns(TypeName.VOID)//方法返回值为void
.addAnnotation(Override.class)//方法注解 实现接口方法
.addParameter(name, "target", Modifier.FINAL);//参数类型(MainActivity) 参数名 参数修饰符
//遍历该类下声明了@BindView注解的成员变量List集合
for (int i = 0; i < list.size(); i++) {
FieldViewBinding fieldViewBinding = list.get(i);
//成员变量类型信息 --android.text.TextView
String packageNameString = fieldViewBinding.getTypeMirror().toString();
//得到成员变量的类名---TextView
ClassName viewclassName = ClassName.bestGuess(packageNameString);
//方法里面添加执行逻辑 $L $T 占位符 参数顺序一定要对 以及“target”一定要与上面的行参保持一致 代表的就是mainActivity
//相当于:mainActivity.textview= (TextView) mainActivity.findViewById(R.id.textView);
methodBuilder.addStatement("target.$L=($T)target.findViewById($L)", fieldViewBinding.getName(), viewclassName, fieldViewBinding.getResId());
}
result.addMethod(methodBuilder.build());//往类里面添加方法
try {
//生成Java类信息 包名 类
JavaFile.builder(packageName, result.build())
.addFileComment("auto create make")//类注释
.build()
.writeTo(filer);//写出
} catch (IOException e) {
e.printStackTrace();
}
}
}
/**
* 获取类名(通过截取包名获取 若是内部类会将"."替换为"$")
* @param typeElement
* @param packageName
*/
private String getClassName(TypeElement typeElement, String packageName) {
int packageNameLength = packageName.length() + 1;
return typeElement.getQualifiedName().toString().substring(packageNameLength).replace(".", "$");
}
/**
* 获取包名
* @param enclosingElement
* @return
*/
private String getPackageName(TypeElement enclosingElement) {
return elementUtil.getPackageOf(enclosingElement).getQualifiedName().toString();
}
到这里就完成了 试着运行一次
public class MainActivity extends AppCompatActivity {
@BindView(R.id.textView)
TextView textview;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
InjectView.bind(this);
textview.setText("我是通过APT编译时生成的对象文件");
}
}
同时我们打开项目编译的class文件也可以看到通过APT插件生成一个内部类
总结:
使用APT javaC在对.java文件进行编译的时候会对源代码文件进行检测找出其中的我们自定义的注解 并交由我们定义的注解处理器进行额外的处理(往源码中添加类 方法等) 最后在通过APT编译成class文件 交由JVM虚拟机运行
(本案例针对控件的初始化操作 就是通过自定义注解处理器 在process方法中向源码中添加控件的初始化话的相关代码 从而达到在编译时就确定了对象的初始化)
优点:
- 编译时创建对象 对程序运行性能无影响
- 代码整洁 方便维护
缺点: - 代码编写难度增加
- 若某处出错 不方便调试
不过现在网上已经有很成熟的开源库来满足我们的要求 实现快速开发 其核心思想是一样的:
butterknife
dagger
Retrofit