Android 编译时注解生成代码
* 本项目 只是学习使用,项目中推荐ButterKnife*
1 简介
在现阶段的Android开发中,注解越来越流行起来,比如ButterKnife,Retrofit,Dragger,EventBus等等都选择使用注解来配置。按照处理时期,注解又分为两种类型,一种是运行时注解,另一种是编译时注解,运行时注解由于性能问题被一些人所诟病。编译时注解的核心依赖APT(Annotation Processing Tools)实现,原理是在某些代码元素上(如类型、函数、字段等)添加注解,在编译时编译器会检查AbstractProcessor的子类,并且调用该类型的process函数,然后将添加了注解的所有元素都传递到process函数中,使得开发人员可以在编译器进行相应的处理,例如,根据注解生成新的Java类,这也就是EventBus,Retrofit,Dragger等开源库的基本原理。
Java API已经提供了扫描源码并解析注解的框架,你可以继承AbstractProcessor类来提供实现自己的解析注解逻辑。下边我们将学习如何在android Studio中通过编译时注解生成java文件。[摘自:javascript:void(0)]
本次,咱们打算做一个类似于butterKnife的框架,但是由于时间有限,暂时只做一下view的绑定,大致使用如下
package cn.yzl.abstractprocessor;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.widget.TextView;
import cn.yzl.viewinject.ViewInjectHelper;
import cn.yzl.viewinject.annotation.ViewInject;
public class MainActivity extends AppCompatActivity {
@ViewInject(R.id.myview)
TextView myView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
//类似于butterKnifer的使用方式
ViewInjectHelper.inject(this);
myView.setText("赋值成功了");
}
}
生成的类
package cn.yzl.abstractprocessor;
import android.view.View;
import android.widget.TextView;
import cn.yzl.abstractprocessor.MainActivity;
import cn.yzl.viewinject.Utils;
class MainActivity_ViewInject {
public MainActivity_ViewInject(MainActivity target, View rootView) {
target.myView = (TextView)Utils.getViewById(rootView, 2131427422);
}
}
关于生成的类,为什么要写成这个样子,而不是一个包含静态方法的类,这里是为了处理 onclick事件,而且希望能够把这个类缓存起来,不需要没进一次都用classloader加载一下这个类,,其实这里使用构造方法是不合理的,不过,,,,我懒….
2 项目结构
- app 咱们的demo
- viewinject-annotation 注解包,java-library module
- viewinject-complie 编译处理包,java-library module
- viewinject 项目真正需要依赖的包,Android-library module,查找咱们编译时生成的类,并执行,并且改包依赖于viewinject-annotation,这样在app中只需要添加本依赖包就好
3 自定义注解
package cn.yzl.viewinject.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* View绑定注解
* Created by YZL on 2017/8/4.
*/
@Retention(RetentionPolicy.CLASS)//
@Target(ElementType.FIELD)
public @interface ViewInject {
int value();
}
这个注解很简单,作用于变量,注意这里的Retention不再是以往的Runtime,而是CLASS,表示这个注解只保留到编译期,在运行的时候是没有的
4 编译时处理器
先看一下它的依赖库
//生成代码的一个库,比手写方便的多,功能强大,自动导包
compile 'com.squareup:javapoet:1.9.0'
//google的 auto service库,自动生成service,省去了手动配置resources/META-INF/services
compileOnly 'com.google.auto.service:auto-service:1.0-rc3'
//依赖注解项目,因为要在这里读取注解
compile project(':viewinject-annotation')
- ViewInjectProcess
package cn.yzl.process.library;
import com.google.auto.service.AutoService;
import com.squareup.javapoet.ClassName;
import com.squareup.javapoet.CodeBlock;
import com.squareup.javapoet.JavaFile;
import com.squareup.javapoet.MethodSpec;
import com.squareup.javapoet.TypeName;
import com.squareup.javapoet.TypeSpec;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.Filer;
import javax.annotation.processing.Messager;
import javax.annotation.processing.ProcessingEnvironment;
import javax.annotation.processing.Processor;
import javax.annotation.processing.RoundEnvironment;
import javax.annotation.processing.SupportedAnnotationTypes;
import javax.annotation.processing.SupportedSourceVersion;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.Element;
import javax.lang.model.element.ExecutableElement;
import javax.lang.model.element.Modifier;
import javax.lang.model.element.TypeElement;
import javax.lang.model.element.VariableElement;
import javax.lang.model.util.Elements;
import javax.lang.model.util.Types;
import javax.tools.Diagnostic;
import cn.yzl.viewinject.annotation.ViewInject;
import cn.yzl.viewinject.annotation.ViewOnClick;
@AutoService(Processor.class)
@SupportedAnnotationTypes("cn.yzl.viewinject.annotation.ViewInject")
@SupportedSourceVersion(SourceVersion.RELEASE_7)
public class ViewInjectProcess extends AbstractProcessor {
//生成的类的 后缀名
public static final String SUFFIX = "_ViewInject";
//获取View类,用于javapoet 生成代码
ClassName VIEW = ClassName.get("android.view", "View");
//获取Utils类,就一个方法,findviewbyid,可有可无吧
ClassName UTILS = ClassName.get("cn.yzl.viewinject", "Utils");
//打印消息类
private Messager messager;
//最终的生成的文件要通过这个写入进去
private Filer filer;
@Override
public synchronized void init(ProcessingEnvironment processingEnvironment) {
super.init(processingEnvironment);
messager = processingEnvironment.getMessager();
filer = processingEnvironment.getFiler();
}
@Override
public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnv) {
List<TypeBean> types = findAnnotations(roundEnv);
if (types.size() == 0) {
return false;
} else {
generateJavaFile(types);
}
return false;
}
/**
* 查找被注解的 变量和其所在的class,封装成typeBean存到list里
* @param roundEnv
* @return
*/
public List<TypeBean> findAnnotations(RoundEnvironment roundEnv) {
List<TypeBean> types = new ArrayList<>();
Set<? extends Element> eFilds = roundEnv
.getElementsAnnotatedWith(ViewInject.class);
Set<? extends Element> eMethods = roundEnv
.getElementsAnnotatedWith(ViewOnClick.class);
for (Element e : eFilds) {
TypeElement typeElement = (TypeElement) e.getEnclosingElement();
String qualifiedName = typeElement.getQualifiedName().toString();
TypeBean typeBean = getTypeBeanByName(types, typeElement, qualifiedName);
typeBean.addField((VariableElement) e);
}
for (Element e : eMethods) {
TypeElement typeElement = (TypeElement) e.getEnclosingElement();
String qualifiedName = typeElement.getQualifiedName().toString();
TypeBean typeBean = getTypeBeanByName(types, typeElement, qualifiedName);
if (typeBean.onClickmethod != null) {
// error(e,msg);
} else {
typeBean.onClickmethod = (ExecutableElement) e;
}
}
return types;
}
/**
* 根据解析出来的类生成 辅助类
* @param types
*/
private void generateJavaFile(List<TypeBean> types) {
for (TypeBean typebean : types) {
generateJavaFileForOneType(typebean);
}
}
/**
* 生成辅助类
* @param typebean
*/
private void generateJavaFileForOneType(TypeBean typebean) {
CodeBlock.Builder codeBuilder = CodeBlock.builder();
//写findviewbyid 代码
for (int i = 0; i < typebean.fields.size(); i++) {
//findViewById
VariableElement variableElement = typebean.fields.get(i);
ViewInject annotation = variableElement.getAnnotation(ViewInject.class);
codeBuilder.add(
"target." + variableElement.getSimpleName().toString() + "=" +
"$T.getViewById(rootView," + annotation.value() + ");\n"
, UTILS);
}
//TODO onclick
// 生成构造方法
MethodSpec method = MethodSpec
.constructorBuilder()
.addModifiers(Modifier.PUBLIC)
.addParameter(TypeName.get(typebean.typeElement.asType()), "target")
.addParameter(VIEW, "rootView")
.addCode(codeBuilder.build())
.build();
//生成类文件
TypeSpec typeSpec = TypeSpec.classBuilder(typebean.simpleName + SUFFIX)
.addMethod(method)
.build();
writeJavaFile(typebean.packName, typeSpec);
}
/**
* @param types
* @param typeElement
* @param className @return
*/
public TypeBean getTypeBeanByName(List<TypeBean> types, TypeElement typeElement, String className) {
for (int i = 0; i < types.size(); i++) {
if (types.get(i).equalsClass(className))
return types.get(i);
}
TypeBean typeBean = new TypeBean(typeElement, className);
types.add(typeBean);
return typeBean;
}
/**
* 写入类
* @param packName
* @param typeSpec
*/
public void writeJavaFile(String packName, TypeSpec typeSpec) {
JavaFile javaFile = JavaFile.builder(packName, typeSpec).build();
try {
javaFile.writeTo(filer);
} catch (IOException e) {
e.printStackTrace();
messager.printMessage(Diagnostic.Kind.ERROR, e.getMessage());
}
}
}
这里面有一个类,TypeBean,里面就是存放的 一个类中所有的被Viewinject注解的方法,和类的包名和全称,这里不再做解析
使用
在app的gradle中
dependencies {
compile project(':ViewInject')
annotationProcessor project(':viewinject-complie')
}
这样就可以了,在编译的时候就会生成咱们需要的辅助类
这里有个问题,processor中不可以有中文,包括注释,否则会报以下错误
5 处理辅助类
这个类特别简单,就是拿到咱们生成的class,获得咱们生成的方法,掉一下就ok
package cn.yzl.viewinject;
import android.app.Activity;
import android.util.Log;
import android.view.View;
import java.lang.annotation.Target;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
/**
* Created by YZL on 2017/8/4.
*/
public class ViewInjectHelper {
public static final String SUFFIX = "_ViewInject";
public static void inject(Activity target) {
ClassLoader classLoader = target.getClass().getClassLoader();
try {
//拿到生成的类的全名
String proxyClazzName = target.getClass().getName() + SUFFIX;
Log.e("ViewInjectHelper#inject", proxyClazzName);
//加载类
Class<?> aClass = classLoader.loadClass(proxyClazzName);
//拿到docView
View rootView = target.getWindow().getDecorView();
//获得申明的构造函数,这里就一个
Constructor<?>[] declaredConstructors = aClass.getDeclaredConstructors();
//取消安全校验
declaredConstructors[0].setAccessible(true);
//执行构造方法
declaredConstructors[0].newInstance(target, rootView);
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
}
}
好了,这样我们就可以使用了
6 调试编译过程
- gradle.properties文件中加入以下两行代码,然后同步gradle文件
org.gradle.daemon=true
org.gradle.jvmargs=-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=8888
- 编辑运行选项
-添加remote
- 采用默认配置就好
- 选择咱们配置好的remote,点击debug按钮,成功的话,下面会弹出连接成功的提示
- 在咱们的ViewInjectProcessor中打断点,然后rebuild项目,就可以debug了
7 补充代码参考
//另外一种生成代码的方式
JavaFileObject source = processingEnvironment.getFiler().createSourceFile("cn.yzl.library.generated.GeneratedClass");
Writer writer = source.openWriter();
writer.write("你生成的代码");
writer.flush();
writer.close();
8 参考