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

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<? extends TypeElement> annotations,
			RoundEnvironment roundEnv) {

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

		return true;
	}

@SupportedAnnotationTypes,定义要支持注解的完整路径,也可以通过getSupportedAnnotationTypes方法来定义

@Override
	public Set<String> getSupportedAnnotationTypes() {
		Set<String> types = new LinkedHashSet<>();
		types.add(InjectView.class.getCanonicalName());

		return types;
	}

@SupportedSourceVersion(SourceVersion.RELEASE_6)表示支持的jdk的版本

3、创建制定文件resources/META-INF/services/javax.annotation.processing.Processor,并填写注解解析器的类路径,这样在编译的时候就能自动找到解析器

152641_JQy7_134491.png

看上去实现编译时注解还是很容易的,但是真要完整的实现一个类似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> 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> 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> 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<Class<?>, AbstractInjector<Object>> INJECTORS = new LinkedHashMap<Class<?>, AbstractInjector<Object>>();
	
	public static void inject(Activity activity){
		AbstractInjector<Object> injector = findInjector(activity);
		injector.inject(Finder.ACTIVITY, activity, activity);
	}
	
	public static void inject(Object target, View view){
		AbstractInjector<Object> injector = findInjector(target);
		injector.inject(Finder.VIEW, target, view);
	}
	
	private static AbstractInjector<Object> findInjector(Object target){
		Class<?> clazz = target.getClass();
		AbstractInjector<Object> injector = INJECTORS.get(clazz);
		if(injector == null){
			try{
				Class injectorClazz = Class.forName(clazz.getName()+"$$"+ProxyInfo.PROXY);
				injector = (AbstractInjector<Object>) 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功能引用这个工程并实现余下的解析。


4、参考

详细的实现过程可以参考这些博客,描述的都很详细

http://www.trinea.cn/android/java-annotation-android-open-source-analysis/

http://blog.csdn.net/lmj623565791/article/details/43452969

https://segmentfault.com/a/1190000002785541

http://blog.zenfery.cc/archives/78.html

http://www.cnblogs.com/avenwu/p/4173899.html




转载于:https://my.oschina.net/u/134491/blog/663124

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值