前言#
之前我们已经理解了什么是运行时注解,并且实现了一个含金量较高的数据库框架,同时我们也发现,使用反射会使运行的效率的变低,很多流行的注解框架已经考虑用编译注解来解决这个问题,今天我们用编译注解的形式来实现setContentView和findViewById。
正文#
首先,我们来弄清楚使用编译注解的目的和优缺点:
编译注解主要是在编译过程中,生成必要的文件,这样在运行时调用,就不需要再通过大量的反射(低效)来进行操作。
这种形式大大提高了注解在运行时的效率,但同时也增加了编译的时间。
我们的最终目的就是让应用跑的更加流畅,所有编译时间的增加,我们还是可以接受的。
下面看一下具体的准备:
我画了一个简单的关系图,我们需要创建三个Library和一个demoModule,要注意的是:
注解库和编译库,必须为Java Library,且指定编译的Jdk为1.7。
创建四个库之后,按照上图进行依赖关系,其中编译的关系,请使用:
annotationProcessor project(':ioc-compiler')
这个插件提供了编译关系,与依赖关系不同,对应的库不会被打入到apk中,但是会编译库中的代码,会在之后的在具体的说明。
先来看看注解库:
/**
* Created by li.zhipeng on 2017/3/17.
*
* 绑定View的注解,实现findViewById的功能
*/
@Retention(RetentionPolicy.CLASS)
@Target(ElementType.FIELD)
public @interface BindView {
int value();
}
/**
* Created by li.zhipeng on 2017/3/21.
*
* 绑定ContentView的注解
*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.CLASS)
public @interface ContentView {
int value();
}
// gradle文件
apply plugin: 'java'
dependencies {
compile fileTree(dir: 'libs', include: ['*.jar'])
targetCompatibility = '1.7'
sourceCompatibility = '1.7'
}
那么之后就是最重要的编译库了,我们最核心的内容都在这里,先看一下gradle文件:
apply plugin: 'java'
dependencies {
compile fileTree(dir: 'libs', include: ['*.jar'])
// auto-service库可以帮我们去生成META-INF等信息。
compile 'com.google.auto.service:auto-service:1.0-rc2'
compile project (':ioc-annotation')
// 如果找不到javax包,可以直接引入本地jdk的jar包
compile files ('/Library/Java/JavaVirtualMachines/jdk1.8.0_91.jdk/Contents/Home/jre/lib/rt.jar')
targetCompatibility = '1.7'
sourceCompatibility = '1.7'
}
其中有注释写到,如果在使用javax包中的类的时,出现找不到指定的类的错误,需要自己手动的去导入jdk的jar包。
为了完整编译的操作,我在编译库中创建了三个类,分别提出他们的代码,里面已经有很详细的注释:
package com.lzp.ioc;
import javax.lang.model.element.Element;
import javax.lang.model.element.Modifier;
import javax.lang.model.element.TypeElement;
/**
* Created by li.zhipeng on 2017/3/17.
*
* 类的工具类
*/
public class ClassValidator {
/**
* 判断是否是private修饰
* */
static boolean isPrivate(Element annotatedClass){
return annotatedClass.getModifiers().contains(Modifier.PRIVATE);
}
/**
* 获取类的完整路径
* */
static String getClassName(TypeElement type, String packageName){
int packageLen = packageName.length() + 1;
return type.getQualifiedName().toString().substring(packageLen).replace(".", "$");
}
}
package com.lzp.ioc;
import java.util.HashMap;
import java.util.Map;
import javax.lang.model.element.PackageElement;
import javax.lang.model.element.TypeElement;
import javax.lang.model.element.VariableElement;
import javax.lang.model.util.Elements;
/**
* Created by li.zhipeng on 2017/3/17.
*
* 含有注解信息的辅助类
*/
public class ProxyInfo {
/**
* 包名
* */
private String packageName;
/**
* 要编译生成的类名
* */
private String proxyClassName;
/**
* 注解修饰的元素
* */
private TypeElement typeElement;
/**
* 保存了所有的BindView 注解的信息
* */
public Map<Integer, VariableElement> injectVariables = new HashMap<>();
/**
* contentView的id
* */
public int contentViewId;
public static final String PROXY = "ViewInject";
public ProxyInfo(Elements elementUtils , TypeElement classElement){
this.typeElement = classElement;
PackageElement packageElement = elementUtils.getPackageOf(typeElement);
String packageName = packageElement.getQualifiedName().toString();
String className = ClassValidator.getClassName(typeElement, packageName);
this.packageName = packageName;
this.proxyClassName = className + "$$" + PROXY;
}
/**
* 生成的java文件的代码
* */
public String generateJavaCode(){
StringBuilder builder = new StringBuilder();
builder.append("// Generated code. Do not modify!\n");
builder.append("package ").append(packageName).append(";\n\n");
builder.append("import com.lzp.ioc.*;\n");
builder.append('\n');
builder.append("public class ").append(proxyClassName).
append(" implements ").append(ProxyInfo.PROXY).
append("<").append(typeElement.getQualifiedName()).append(">");
builder.append("{\n");
generateMethods(builder);
builder.append('\n');
builder.append("}\n");
return builder.toString();
}
/**
* 生成inject方法的代码
* */
private void generateMethods(StringBuilder builder){
builder.append("@Override\n");
builder.append("public void inject(").append(typeElement.getQualifiedName()).append(" host, Object source) {\n");
StringBuilder ifStr = new StringBuilder();
StringBuilder elseStr = new StringBuilder();
//遍历所有的BindView注解的信息
for (int id : injectVariables.keySet()){
VariableElement variableElement = injectVariables.get(id);
String name = variableElement.getSimpleName().toString();
String type = variableElement.asType().toString();
ifStr.append("host.").append(name).append(" = ");
ifStr.append("(").append(type).append(")(((android.app.Activity)source).findViewById(").append(id).append("));");
elseStr.append("host.").append(name).append(" = ");
elseStr.append("(").append(type).append(")(((android.view.View)source).findViewById(").append(id).append("));");
}
// if
builder.append(" if(source instanceof android.app.Activity) {\n");
// 设置ContentView
if (contentViewId != 0){
builder.append("host.setContentView(").append(contentViewId).append(");\n");
}
builder.append(ifStr);
// else
// 如果是View类型,不用设置ContentView
builder.append("\n}\nelse {\n");
builder.append(elseStr);
builder.append("\n}\n");
builder.append("};");
}
public String getProxyClassFullName(){
return packageName + "." + proxyClassName;
}
public TypeElement getTypeElement(){
return typeElement;
}
}
package com.lzp.ioc;
import com.google.auto.service.AutoService;
import com.lzp.io.BindView;
import com.lzp.io.ContentView;
import java.io.IOException;
import java.io.Writer;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Set;
import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.Messager;
import javax.annotation.processing.ProcessingEnvironment;
import javax.annotation.processing.Processor;
import javax.annotation.processing.RoundEnvironment;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.Element;
import javax.lang.model.element.ElementKind;
import javax.lang.model.element.TypeElement;
import javax.lang.model.element.VariableElement;
import javax.lang.model.util.Elements;
import javax.tools.Diagnostic;
import javax.tools.JavaFileObject;
/**
* Created by li.zhipeng on 2017/3/17.
*
* 自定义编译、处理器
*/
@AutoService(Processor.class)
public class IocProcessor extends AbstractProcessor {
/**
* 日志打印类
* */
private Messager messager;
/**
* 元素工具类
* */
private Elements elementsUtils;
/**
* 保存所有的要生成的注解文件信息
* */
private Map<String, ProxyInfo> mProxyMap = new HashMap<>();
@Override
public synchronized void init(ProcessingEnvironment processingEnv) {
super.init(processingEnv);
messager = processingEnv.getMessager();
elementsUtils = processingEnv.getElementUtils();
// 在这里打印gradle文件传进来的参数
Map<String, String> map = processingEnv.getOptions();
for (String key : map.keySet()) {
messager.printMessage(Diagnostic.Kind.NOTE, "key" + ":" + map.get(key));
}
}
/**
* 此方法用来设置支持的注解类型,没有设置的无效(获取不到)
* */
@Override
public Set<String> getSupportedAnnotationTypes() {
HashSet<String> supportTypes = new LinkedHashSet<>();
// 把支持的类型添加进去
supportTypes.add(BindView.class.getCanonicalName());
supportTypes.add(ContentView.class.getCanonicalName());
return supportTypes;
}
@Override
public SourceVersion getSupportedSourceVersion() {
return SourceVersion.latestSupported();
}
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
messager.printMessage(Diagnostic.Kind.NOTE, "process...");
/**
* 防止处理多次,要清空
* */
mProxyMap.clear();
// 获取全部的带有BindView注解的Element
Set<? extends Element> elesWidthBind = roundEnv.getElementsAnnotatedWith(BindView.class);
// 对BindView进行循环,构建ProxyInfo信息
// 对BindView进行循环,构建ProxyInfo信息
for (Element element : elesWidthBind) {
// 检查element的合法性
checkSAnnotationValid(element, BindView.class);
// 强转成属性元素
VariableElement variableElement = (VariableElement) element;
// 我们知道属性元素的外层一定是类元素
TypeElement typeElement = (TypeElement) variableElement.getEnclosingElement();
// 获取类元素的类名
String fqClassName = typeElement.getQualifiedName().toString();
// 以class名称为key,保存到mProxyMap中
ProxyInfo proxyInfo = mProxyMap.get(fqClassName);
if (proxyInfo == null) {
proxyInfo = new ProxyInfo(elementsUtils, typeElement);
mProxyMap.put(fqClassName, proxyInfo);
}
// 获取BindView注解,把信息放入proxyInfo中
BindView bindAnnotation = element.getAnnotation(BindView.class);
int id = bindAnnotation.value();
proxyInfo.injectVariables.put(id, variableElement);
}
// 获取所有的ContentView注解,操作原理和上面的BindView一样
Set<? extends Element> contentAnnotations = roundEnv.getElementsAnnotatedWith(ContentView.class);
for (Element element : contentAnnotations) {
TypeElement typeElement = (TypeElement) element;
String fqClassName = typeElement.getQualifiedName().toString();
ProxyInfo proxyInfo = mProxyMap.get(fqClassName);
if (proxyInfo == null) {
proxyInfo = new ProxyInfo(elementsUtils, typeElement);
mProxyMap.put(fqClassName, proxyInfo);
}
ContentView contentViewAnnotation = element.getAnnotation(ContentView.class);
proxyInfo.contentViewId =contentViewAnnotation.value();
}
// 循环生成源文件
for (String key : mProxyMap.keySet()) {
ProxyInfo proxyInfo = mProxyMap.get(key);
try {
JavaFileObject jfo = processingEnv.getFiler().createSourceFile(proxyInfo.getProxyClassFullName(), proxyInfo.getTypeElement());
Writer writer = jfo.openWriter();
writer.write(proxyInfo.generateJavaCode());
writer.flush();
writer.close();
} catch (IOException e) {
error(proxyInfo.getTypeElement(), "Unable to write injector for type %s: %s ", proxyInfo.getTypeElement(), e.getMessage());
}
}
return true;
}
/**
* 检查BindView修饰的元素的合法性
* */
private boolean checkSAnnotationValid(Element element, Class<?> clazz) {
if (element.getKind() != ElementKind.FIELD) {
error(element, "%s must be delared on field.", clazz.getSimpleName());
return false;
}
if (ClassValidator.isPrivate(element)) {
error(element, "%s() must can not be private.", element.getSimpleName());
return false;
}
return true;
}
/**
* 打印错误日志方法
* */
private void error(Element element, String message, Object... args) {
if (args.length > 0) {
message = String.format(message, args);
}
messager.printMessage(Diagnostic.Kind.NOTE, message, element);
}
}
现在来挑重点的地方来说明一下:
1、gradle文件中 compile 'com.google.auto.service:auto-service:1.0-rc2',这是google提供的编译处理的库,继承AbstractProcessor自定义编译处理器,然后通过注解@AutoService(Processor.class),就会在编译时自动执行。
2、 在AbstractProcessor的核心方法 public boolean process(Set annotations, RoundEnvironment roundEnv) ,有两个参数,他们都可以获取注解信息,但其实他俩有根本的区别:
Set annotations
里面包含的是所有使用的注解的信息,例如BindView,ContentViewRoundEnvironment roundEnv
他返回的是所有被注解的元素,例如类,属性等
3、JavaFileObject是java文件对象,可以直接创建,通过流的形式,对文件进行编写。
现在看一下api库:
package com.lzp.ioc;
/**
* Created by li.zhipeng on 2017/3/17.
*
* 注入接口
*/
public interface ViewInject<T> {
void inject(T target, Object source);
}
package com.lzp.ioc;
import android.app.Activity;
import android.view.View;
/**
* Created by li.zhipeng on 2017/3/17.
*
* 提供注入的静态方法,间接调用了io-complier的编译生成的类方法
*/
public class ViewInjector {
private static final String SUFFIX = "$$ViewInject";
public static void injectView(Activity activity) {
ViewInject proxyActivity = findProxyActivity(activity);
proxyActivity.inject(activity, activity);
}
public static void injectView(Object object, View view) {
ViewInject proxyActivity = findProxyActivity(object);
proxyActivity.inject(object, view);
}
/**
* 通过反射创建要使用的类的对象
* */
private static ViewInject findProxyActivity(Object activity) {
try {
Class<?> clazz = activity.getClass();
Class<?> injectorClazz = Class.forName(clazz.getName() + SUFFIX);
return (ViewInject) injectorClazz.newInstance();
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
throw new RuntimeException(String.format("can not find %s , something when compiler.", activity.getClass().getSimpleName() + SUFFIX));
}
}
在编译库中定义了命名规则,类名+SUFFIX,通过反射创建一个对象就得到了编译的文件,然后调用文件中的方法。
最后就是Demo的MainActivity:
package com.lzp.compileannotationstudy;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.widget.ImageView;
import android.widget.TextView;
import com.lzp.io.BindView;
import com.lzp.io.ContentView;
import com.lzp.ioc.ViewInjector;
@ContentView(R.layout.activity_main)
public class MainActivity extends AppCompatActivity {
@BindView(R.id.textView)
public TextView textView;
@BindView(R.id.imageView)
ImageView imageView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
ViewInjector.injectView(this);
textView.setText("hahahahaha");
}
}
看一下运行效果:
再看一下编出来的文件,这就是通过JavaFileObject写出来的文件:
ok,我们的编译注解运行的很成功!!!
总结#
虽然是看完了整体的大概流程,但是肯定还会有人觉得一头雾水,有种似懂非懂的感觉,那是因为我们对细节还不够了解,对新的api都刚刚接触,所以要拿出更多的精力,仔细的去体会感受,相信对看了几遍之后,一定会有大众焕然大悟的感觉。
由于每个人的思维不同,我也很难做到各个知识点面面俱到,如果你有什么问题,可以留言。
下一篇,来聊一聊annotationProcessor这个框架。