文档章节

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

cxlin007
 cxlin007
发布于 2016/04/20 16:44
字数 1218
阅读 242
收藏 4

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,并填写注解解析器的类路径,这样在编译的时候就能自动找到解析器

看上去实现编译时注解还是很容易的,但是真要完整的实现一个类似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




© 著作权归作者所有

下一篇: App Manifest
cxlin007

cxlin007

粉丝 2
博文 47
码字总数 28683
作品 0
福州
私信 提问
HappyBKs教你写Java注解(1)——注解的分类、运行机制、作用域及概念汇总

注解这东西,已经在我们的编程生活中习以为常了。覆盖一个父类的方法,套用Spring、Mybatis中的编程套路,编写JUnit测试函数等等。你会发现,作为一个Java Coder,你无时无刻不在接触它们。 ...

HappyBKs
2015/09/07
700
0
Android 框架学习3:从 EventBus 中学到的精华

关联文章: EventBus 3.0 的特点与如何使用 源码分析 EventBus 3.0 如何实现事件总线 学习的目的是为了超越,经过前面对 EventBus 3.0 的学习,我们已经对它相当熟悉了,现在来总结下,从这个...

u011240877
2017/07/06
0
0
夯实 Java 基础 - 注解

不知道大家有没有一种感觉,当你想要了解某个知识点的时候,就会发现好多技术类 APP 或者公众号在推一些关于这个知识点的文章。也许这就是大数据的作用,这也说明总有人比你抢先一步。学习不...

群星纪元
03/31
5
0
使用编译时注解简单实现类似 ButterKnife 的效果

这篇文章是学习鸿洋前辈的 Android 如何编写基于编译时注解的项目 的笔记,用于记录我的学习收获。 读完本文你将了解: 什么是编译时注解 APT 编译时注解如何使用与编写 举个例子 完成编写使...

u011240877
2017/07/05
0
0
源码解析ButterKnife

0、引子 话说我们做程序员的,都应该多少是个懒人,我们总是想办法驱使我们的电脑帮我们干活,所以我们学会了各式各样的语言来告诉电脑该做什么——尽管,他们有时候也会误会我们的意思。 突...

waffle930
2016/09/22
24
0

没有更多内容

加载失败,请刷新页面

加载更多

RxJava进行单元测试的方式

@Test public void completeTask_retrievedTaskIsComplete() { // Given a new task in the persistent repository final Task newTask = new Task(TITLE, ""); ......

SuShine
34分钟前
5
0
正则表达式大全

检验手机号码 # 要求:手机号码必须为11位数字,以1开头,第二位为1或5或8。import redef verify_mobile(): mob = input("请输入手机号码:") ret = re.match(r"1[358]\d{9}", m......

彩色泡泡糖
38分钟前
5
0
QT之border-image属性

一、border-image的兼容性 border-image可以说是CSS3中的一员大将,将来一定会大放光彩,其应用潜力真的是非常的惊人。可惜目前支持的浏览器有限,仅Firefox3.5,chrome浏览器,Safari3+支持...

shzwork
39分钟前
6
0
Kubernetes Operator简易教程

1. 安装operator-sdk //安装 operator-sdk$ apt-get install operator-sdk.....$ operator-sdk versionoperator-sdk version: v0.7.0$ go versiongo version go1.11.4 darwin/amd64 2......

Robotcl_Blog
39分钟前
5
0
再谈DAG任务分解和Shuffle RDD

1、DagScheduler分析 DagScheduler功能主要是负责RDD的各个stage的分解和任务提交。Stage分解是从触发任务调度过程的finalStage开始倒推寻找父stage,如果父stage没有提交任务则循环提交缺失...

守望者之父
45分钟前
4
0

没有更多内容

加载失败,请刷新页面

加载更多

返回顶部
顶部