您的位置:首页 > 移动开发 > Android开发

Android注解-编译时生成代码 (APT)

2016-08-09 22:52 423 查看
Android注解越来越引领潮流,比如 Dagger2, ButterKnife, EventBus3 等,他们都是注解类型,而且他们都有个共同点就是编译时生成代码,而不是运行时利用反射,这样大大优化了性能;而这些框架都用到了同一个工具就是:APT(Annotation Processing Tool ),可以在代码编译期解析注解,并且生成新的 Java 文件,减少手动的代码输入。

今天我们要自己实现的就是类似ButterKnife的简单的view初始化和点击事件;

先看下整个项目的目录结构:



inject :API module用来把生成的文件与控件相关联

viewinject-annotation :注解module

viewinject-compiler : 用来生成java文件module

先从最简单入手,注解moudle:

1.创建名字为
viewinject-annotation
的java类型module

2.该module只有两个类:

1.
BindView
用来对成员变量进行注解,并且接收一个 int 类型的参数

* Created by JokAr on 16/8/6.
*/
@Retention(RetentionPolicy.CLASS)
@Target(ElementType.FIELD)
public @interface BindView {
int value();
}


2.
OnClick
对方法进行注解,接收一个或一组 int 类型参数,相当于给一组 View 指定点击响应事件。

/**
* Created by JokAr on 16/8/6.
*/
@Retention(RetentionPolicy.CLASS)
@Target(ElementType.METHOD)
public @interface OnClick {
int[] value();
}


注解module就完成了,下面看看API module

1.首先创建一个Android moudle 的
inject
,然后创建
interface


/**
* Created by JokAr on 16/8/6.
*/
public interface Inject<T> {

void inject(T host, Object object, Provider provider);
}


/**
* Created by JokAr on 16/8/6.
*/
public interface Provider {
Context getContext(Object object);

View findView(Object object, int id);
}


因为我们需要生成的文件是这么写的:

public class MainActivity$$ViewInject implements Inject<MainActivity> {
@Override
public void inject(final MainActivity host, Object source, Provider provider) {
host.textView = (TextView)(provider.findView(source, 2131427412));
host.button1 = (Button)(provider.findView(source, 2131427413));
View.OnClickListener listener = new View.OnClickListener() {
@Override
public void onClick(View view) {
host.click();
}
} ;
provider.findView(source, 2131427412).setOnClickListener(listener);
}
}


当然这个生成文件是根据自己需求生成,然后需要一个类来关联自己的activity类与生成的类:

/**
* Created by JokAr on 16/8/6.
*/
public class ViewInject {
private static final ActivityProvider activityProvider = new ActivityProvider();

private static final ViewProvider viewProvider = new ViewProvider();
private static final ArrayMap<String, Inject> injectMap = new ArrayMap<>();

public static void inject(Activity activity) {
inject(activity, activity, activityProvider);
}

public static void inject(View view) {
inject(view, view);
}

private static void inject(Object host, View view) {
inject(host, view, viewProvider);
}

private static void inject(Object host, Object object, Provider provider) {
String className = host.getClass().getName();
try {
Inject inject = injectMap.get(className);

if (inject == null) {
Class<?> aClass = Class.forName(className + "$$ViewInject");
inject = (Inject) aClass.newInstance();
injectMap.put(className, inject);
}
inject.inject(host, object, provider);
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
}


使用方法就是:

ViewInject.inject(this);


host 表示注解 View 变量所在的类,也就是注解类

object 表示查找 View 的地方,Activity & View 自身就可以查找,Fragment 需要在自己的 itemView 中查找

provider 是一个接口,定义了不同对象(比如 Activity、View 等)如何去查找目标 View,项目中分别为 Activity、View 实现了 Provider 接口(具体实现参考项目代码)

为了提高效率,避免每次注入的时候都去找
Inject
对象,用一个 Map 将第一次找到的对象缓存起来,后面用的时候直接从 Map 里面取。

API module类就完成了

再看
viewinject-compiler
module:

首先创建名为
iewinject-compiler
的Java module ,然后在该module的buile.gradle加上一些依赖:

compile project(':viewinject-annotation')
compile 'com.squareup:javapoet:1.7.0'
compile 'com.google.auto.service:auto-service:1.0-rc2'


Javapoet是square一个工具,提供了各种 API 让你用各种姿势去生成 Java 代码文件,避免了徒手拼接字符串的尴尬。

auto-service
主要用于注解 Processor,对其生成 META-INF 配置信息。

首先创建
ViewInjectProcesser
类:

/**
* Created by JokAr on 16/8/8.
*/
@AutoService(Processor.class)
public class ViewInjectProcesser extends AbstractProcessor {
private Filer mFiler; //文件相关的辅助类
private Elements mElementUtils; //元素相关的辅助类
private Messager mMessager; //日志相关的辅助类

@Override
public synchronized void init(ProcessingEnvironment processingEnv) {
super.init(processingEnv);
mFiler = processingEnv.getFiler();
mElementUtils = processingEnv.getElementUtils();
mMessager = processingEnv.getMessager();
mAnnotatedClassMap = new TreeMap<>();
}

@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {

return false;
}

/**
* 指定使用的 Java 版本。通常返回SourceVersion.latestSupported()。
* @return
*/
@Override
public SourceVersion getSupportedSourceVersion() {
return SourceVersion.latestSupported();
}

/**
* 指定哪些注解应该被注解处理器注册
* @return
*/
@Override
public Set<String> getSupportedAnnotationTypes() {
Set<String> types = new LinkedHashSet<>();
types.add(BindView.class.getCanonicalName());
types.add(OnClick.class.getCanonicalName());
return types;
}

}


@AutoService
来注解这个处理器,可以自动生成配置信息。

在 init() 可以初始化拿到一些实用的工具类。

这里涉及到了
Element
元素,借用一下别人的分析:



这个类的的基本内容就完成了,

现在创建
BindViewField
类,来解析
BindView
注解类来获取用该注解的相关信息

/**
* Created by JokAr on 16/8/8.
*/
public class BindViewField {
private VariableElement mVariableElement;
private int mresId;

public BindViewField(Element element) throws IllegalArgumentException{
if (element.getKind() != ElementKind.FIELD) {
throw new IllegalArgumentException(String.format("Only fields can be annotated with @%s",
BindView.class.getSimpleName()));
}
mVariableElement = (VariableElement) element;

BindView bindView = mVariableElement.getAnnotation(BindView.class);
mresId = bindView.value();
if (mresId < 0) {
throw new IllegalArgumentException(
String.format("value() in %s for field %s is not valid !", BindView.class.getSimpleName(),
mVariableElement.getSimpleName()));
}
}

/**
* 获取变量名称
* @return
*/
public Name getFieldName() {
return mVariableElement.getSimpleName();
}

/**
* 获取变量id
* @return
*/
public int getResId() {
return mresId;
}

/**
* 获取变量类型
* @return
*/
public TypeMirror getFieldType() {
return mVariableElement.asType();
}
}


创建
OnClickMethod
类来解析使用
OnClick
注解的方法,获取相关信息

public class OnClickMethod {
private ExecutableElement mExecutableElement;
private int[] resIds;
private Name mMethodName;

public OnClickMethod(Element element) throws IllegalArgumentException {
if (element.getKind() != ElementKind.METHOD) {
throw new IllegalArgumentException(
String.format("Only methods can be annotated with @%s",
OnClick.class.getSimpleName()));
}

mExecutableElement = (ExecutableElement) element;

resIds = mExecutableElement.getAnnotation(OnClick.class).value();

if (resIds == null) {
throw new IllegalArgumentException(String.format("Must set valid ids for @%s",
OnClick.class.getSimpleName()));
} else {
for (int id : resIds) {
if (id < 0) {
throw new IllegalArgumentException(String.format("Must set valid id for @%s",
OnClick.class.getSimpleName()));
}
}
}
mMethodName = mExecutableElement.getSimpleName();
List<? extends VariableElement> parameters = mExecutableElement.getParameters();

if (parameters.size() > 0) {
throw new IllegalArgumentException(
String.format("The method annotated with @%s must have no parameters",
OnClick.class.getSimpleName()));
}
}

/**
* 获取方法名称
* @return
*/
public Name getMethodName() {
return mMethodName;
}

/**
* 获取id数组
* @return
*/
public int[] getResIds() {
return resIds;
}
}


然后重点就是生成Java代码文件的类:

/**
* Created by JokAr on 16/8/8.
*/
public class AnnotatedClass {

private TypeElement mTypeElement;
private ArrayList<BindViewField> mFields;
private ArrayList<OnClickMethod> mMethods;
private Elements mElements;

public AnnotatedClass(TypeElement typeElement, Elements elements) {
mTypeElement = typeElement;
mElements = elements;
mFields = new ArrayList<>();
mMethods = new ArrayList<>();
}

public String getFullClassName() {
return mTypeElement.getQualifiedName().toString();
}

public void addField(BindViewField field) {
mFields.add(field);
}

public void addMethod(OnClickMethod method) {
mMethods.add(method);
}

public JavaFile generateFile() {
//generateMethod
MethodSpec.Builder injectMethod = MethodSpec.methodBuilder("inject")
.addModifiers(Modifier.PUBLIC)
.addAnnotation(Override.class)
.addParameter(TypeName.get(mTypeElement.asType()), "host", Modifier.FINAL)
.addParameter(TypeName.OBJECT, "source")
.addParameter(TypeUtil.PROVIDER,"provider");

for(BindViewField field : mFields){
// find views
injectMethod.addStatement("host.$N = ($T)(provider.findView(source, $L))",
field.getFieldName(),
ClassName.get(field.getFieldType()), field.getResId());
}

for(OnClickMethod method :mMethods){
TypeSpec listener = TypeSpec.anonymousClassBuilder("")
.addSuperinterface(TypeUtil.ANDROID_ON_CLICK_LISTENER)
.addMethod(MethodSpec.methodBuilder("onClick")
.addAnnotation(Override.class)
.addModifiers(Modifier.PUBLIC)
.returns(TypeName.VOID)
.addParameter(TypeUtil.ANDROID_VIEW, "view")
.addStatement("host.$N()", method.getMethodName())
.build())
.build();
injectMethod.addStatement("View.OnClickListener listener = $L ", listener);
for (int id : method.getResIds()) {
// set listeners
injectMethod.addStatement("provider.findView(source, $L).setOnClickListener(listener)", id);
}
}

//generaClass
TypeSpec injectClass = TypeSpec.classBuilder(mTypeElement.getSimpleName() + "$$ViewInject")
.addModifiers(Modifier.PUBLIC)
.addSuperinterface(ParameterizedTypeName.get(TypeUtil.INJET, TypeName.get(mTypeElement.asType())))
.addMethod(injectMethod.build())
.build();

String packgeName = mElements.getPackageOf(mTypeElement).getQualifiedName().toString();

return JavaFile.builder(packgeName, injectClass).build();
}
}


具体的可以看javapoet的API,然后我们需要完善
ViewInjectProcesser
类,增加:

private Map<String, AnnotatedClass> mAnnotatedClassMap;

@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
mAnnotatedClassMap.clear();

try {
processBindView(roundEnv);
processOnClick(roundEnv);
} catch (IllegalArgumentException e) {
e.printStackTrace();
error(e.getMessage());
}

for (AnnotatedClass annotatedClass : mAnnotatedClassMap.values()) {
try {
annotatedClass.generateFile().writeTo(mFiler);
} catch (IOException e) {
error("Generate file failed, reason: %s", e.getMessage());
}
}
return true;
}

private void processBindView(RoundEnvironment roundEnv) throws IllegalArgumentException {

for (Element element : roundEnv.getElementsAnnotatedWith(BindView.class)) {
AnnotatedClass annotatedClass = getAnnotatedClass(element);
BindViewField bindViewField = new BindViewField(element);
annotatedClass.addField(bindViewField);
}
}

private void processOnClick(RoundEnvironment roundEnv) throws IllegalArgumentException {
for (Element element : roundEnv.getElementsAnnotatedWith(OnClick.class)) {
AnnotatedClass annotatedClass = getAnnotatedClass(element);
OnClickMethod onClickMethod = new OnClickMethod(element);
annotatedClass.addMethod(onClickMethod);
}
}
private void error(String msg, Object... args) {
mMessager.printMessage(Diagnostic.Kind.ERROR, String.format(msg, args));
}


实际使用

Android Stduio 2.2以下使用方法

在项目的根目录的build.gradle添加:

classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8'


在项目的主module的build.gradle添加:

apply plugin: 'com.neenbedankt.android-apt'

compile project(':viewinject-annotation')
compile project(':inject')
apt project(':viewinject-compiler')


Android Stduio 2.2以上使用方法

compile project(':viewinject-annotation')
compile project(':inject')
annotationProcessor project(':viewinject-compiler')


在自己的activity类使用:

/**
* Created by JokAr on 16/8/8.
*/
public class MainActivity extends AppCompatActivity {

@BindView(R.id.textView)
TextView textView;
@BindView(R.id.button1)
Button button1;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
ViewInject.inject(this);

}

@OnClick(R.id.textView)
public void click() {
Toast.makeText(getApplicationContext(), "hello", Toast.LENGTH_SHORT).show();
}

}


点击makeProject 就编译完成后就可以在主项目module的/build/generated/source/apt/debug 目录下看到生成的java类文件了

一个学习级的apt项目就完成了。

项目源码

实战项目:Android6.0权限管理 工具,我用java重写别人的kotlin项目;地址:

https://github.com/a1018875550/PermissionDispatcher
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: 
相关文章推荐