如何写一个编译时注解框架

 我来答
己学好4
2016-08-13 · TA获得超过1.5万个赞
知道大有可为答主
回答量:1.1万
采纳率:91%
帮助的人:5157万
展开全部
1、注解

我们日常使用的很多开源库都有注解的实现,主要有运行时注解和编译时注解两种。
运行时注解:主要作用就是得到注解的信息
Retrofit:
调用

@GET("/users/{username}")
User getUser(@Path("username") String username);

定义
@Documented
@Target(METHOD)
@Retention(RUNTIME)
@RestMethod("GET")
public @interface GET {
String value();
}

编译时注解:主要作用动态生成代码

Butter Knife

调用
@InjectView(R.id.user)
EditText username;

定义
@Retention(CLASS)
@Target(FIELD)
public @interface InjectView {
int value();
}

2、编译时注解

要实现编译时注解需要3步:

1、定义注解(关于注解的定义可以参考下面引用的博客)
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.ElementType;

@Target({ ElementType.FIELD, ElementType.TYPE })
@Retention(RetentionPolicy.CLASS)
public @interface Seriable
{

}

2、编写注解解析器
@SupportedAnnotationTypes("annotation.Seriable")
@SupportedSourceVersion(SourceVersion.RELEASE_6)
public class ViewInjectProcessor extends AbstractProcessor {

@Override
public boolean process(Set annotations,
RoundEnvironment roundEnv) {

for (Element ele : roundEnv.getElementsAnnotatedWith(InjectView.class)) {
if(ele.getKind() == ElementKind.FIELD){
//todo
}

return true;
}

@SupportedAnnotationTypes,定义要支持注解的完整路径,也可以通过getSupportedAnnotationTypes方法来定义
@Override
public Set getSupportedAnnotationTypes() {
Set types = new LinkedHashSet<>();
types.add(InjectView.class.getCanonicalName());

return types;
}

@SupportedSourceVersion(SourceVersion.RELEASE_6)表示支持的jdk的版本
3、创建制定文件resources/META-INF/services/javax.annotation.processing.Processor,并填写注解解析器的类路径,这样在编译的时候就能自动找到解析器

看上去实现编译时注解还是很容易的,但是真要完整的实现一个类似Butter Knife的框架,这还只是开始。
Butter Knife是专注View的注入,在使用注解的类编译后,查看编译后的class文件,会发现多出了文件,如:
SimpleActivity$$ViewInjector.java
SimpleActivity$$ViewInjector.java,就是通过编译时注解动态创建出来的,查看SimpleActivity$$ViewInjector.java的内容
// Generated code from Butter Knife. Do not modify!
package com.example.butterknife;

import android.view.View;

import butterknife.ButterKnife.Finder;

public class SimpleActivity$$ViewInjector {

public static void inject(Finder finder, final com.example.butterknife.SimpleActivity target, Object source) {
View view;
view = finder.findRequiredView(source, 2131230759, "field 'title'");
target.title = (android.widget.TextView) view;
view = finder.findRequiredView(source, 2131230783, "field 'subtitle'");
target.subtitle = (android.widget.TextView) view;
view = finder.findRequiredView(source, 2131230784, "field 'hello', method 'sayHello', and method 'sayGetOffMe'");
target.hello = (android.widget.Button) view;
view.setOnClickListener(
new butterknife.internal.DebouncingOnClickListener() {
@Override
public void doClick(
android.view.View p0
) {
target.sayHello();
}
});
view.setOnLongClickListener(
new android.view.View.OnLongClickListener() {
@Override
public boolean onLongClick(
android.view.View p0
) {
return target.sayGetOffMe();
}
});
view = finder.findRequiredView(source, 2131230785, "field 'listOfThings' and method 'onItemClick'");
target.listOfThings = (android.widget.ListView) view;
((android.widget.AdapterView) view).setOnItemClickListener(
new android.widget.AdapterView.OnItemClickListener() {
@Override
public void onItemClick(
android.widget.AdapterView p0,
android.view.View p1,
int p2,
long p3
) {
target.onItemClick(p2);
}
});
view = finder.findRequiredView(source, 2131230786, "field 'footer'");
target.footer = (android.widget.TextView) view;
}

public static void reset(com.example.butterknife.SimpleActivity target) {
target.title = null;
target.subtitle = null;
target.hello = null;
target.listOfThings = null;
target.footer = null;
}
}

inject方法进行初始化,reset进行释放。inject都是调用Finder的方法与android系统的findViewById等方法很像,再来看Finder类,只截取部分。

public enum Finder {
public T findRequiredView(Object source, int id, String who) {
T view = findOptionalView(source, id, who);
if (view == null) {
String name = getResourceEntryName(source, id);
throw new IllegalStateException("Required view '"
+ name
+ "' with ID "
+ id
+ " for "
+ who
+ " was not found. If this view is optional add '@Nullable' (fields) or '@Optional'"
+ " (methods) annotation.");
}
return view;
}

public T findOptionalView(Object source, int id, String who) {
View view = findView(source, id);
return castView(view, id, who);
}

@Override protected View findView(Object source, int id) {
return ((View) source).findViewById(id);
}

@SuppressWarnings("unchecked") // That's the point.
public T castView(View view, int id, String who) {
try {
return (T) view;
} catch (ClassCastException e) {
if (who == null) {
throw new AssertionError();
}
String name = getResourceEntryName(view, id);
throw new IllegalStateException("View '"
+ name
+ "' with ID "
+ id
+ " for "
+ who
+ " was of the wrong type. See cause for more info.", e);
}
}

findRequiredView方法实际上就是我们常用findViewById的实现,其动态帮我们添加了这些实现。view注入的原理我们就清楚了。
虽然编译时创建了这个类,运行的时候如何使用这个类呢,这里我用自己实现的一个类来描述
public class XlViewInjector {

static final Map, AbstractInjector> INJECTORS = new LinkedHashMap, AbstractInjector>();

public static void inject(Activity activity){
AbstractInjector injector = findInjector(activity);
injector.inject(Finder.ACTIVITY, activity, activity);
}

public static void inject(Object target, View view){
AbstractInjector injector = findInjector(target);
injector.inject(Finder.VIEW, target, view);
}

private static AbstractInjector findInjector(Object target){
Class clazz = target.getClass();
AbstractInjector injector = INJECTORS.get(clazz);
if(injector == null){
try{
Class injectorClazz = Class.forName(clazz.getName()+"$$"+ProxyInfo.PROXY);
injector = (AbstractInjector) injectorClazz.newInstance();
INJECTORS.put(clazz, injector);
}catch(Exception e){
e.printStackTrace();
}
}

return injector;
}
}
XlViewInjector与ButterKnife,比如调用时我们都会执行XlViewInjector.inject方法,通过传入目标类的名称获得封装后的类实例就是SimpleActivity$$ViewInjector.java,再调用它的inject,来初始化各个view。
总结一下整个实现的流程:
1、通过编译时注解动态创建了一个包装类,在这个类中已解析了注解,实现了获取view、设置监听等代码。
2、执行时调用XlViewInjector.inject(object)方法,实例化object类对应的包装类,并执行他的初始化方法inject;
因此我们也能明白为什么XlViewInjector.inject(object)方法一定要在setContentView之后执行。

3、实现注解框架时的坑

解析有用到android api,因此需要创建Android工程,但是android library并没有javax的一些功能,
在eclipse环境下,右键build-path add library把jdk加进来
在android studio下,需要先创建java Library功能,实现与view无关的解析,再创建一个android library功能引用这个工程并实现余下的解析。
推荐律师服务: 若未解决您的问题,请您详细描述您的问题,通过百度律临进行免费专业咨询

为你推荐:

下载百度知道APP,抢鲜体验
使用百度知道APP,立即抢鲜体验。你的手机镜头里或许有别人想知道的答案。
扫描二维码下载
×

类别

我们会通过消息、邮箱等方式尽快将举报结果通知您。

说明

0/200

提交
取消

辅 助

模 式