如何寫一個編譯時註解框架

一、註解

咱們平常使用的不少開源庫都有註解的實現,主要有運行時註解和編譯時註解兩種。html

運行時註解:主要做用就是獲得註解的信息java

Retrofit:android

調用
segmentfault

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

定義api

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

編譯時註解:主要做用動態生成代碼
框架

Butter Knife
eclipse

調用ide

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

定義ui

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


二、編譯時註解

要實現編譯時註解須要3步:
this

一、定義註解(關於註解的定義能夠參考下面引用的博客)

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  
{  
      
}

二、編寫註解解析器

@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的版本

三、建立制定文件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。

總結一下整個實現的流程:

一、經過編譯時註解動態建立了一個包裝類,在這個類中已解析了註解,實現了獲取view、設置監聽等代碼。

二、執行時調用XlViewInjector.inject(object)方法,實例化object類對應的包裝類,並執行他的初始化方法inject;

所以咱們也能明白爲何XlViewInjector.inject(object)方法必定要在setContentView以後執行。


三、實現註解框架時的坑

解析有用到android api,所以須要建立Android工程,可是android library並無javax的一些功能,

在eclipse環境下,右鍵build-path add library把jdk加進來

在android studio下,須要先建立java Library功能,實現與view無關的解析,再建立一個android library功能引用這個工程並實現餘下的解析。


四、參考

詳細的實現過程能夠參考這些博客,描述的都很詳細

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

相關文章
相關標籤/搜索