Android 注解APT

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/xiey94/article/details/80251082

开始是接触注解,然后就想着注解怎么用,我看到的很多都是通过在运行期的注解反射获取值,然后使用,这样挺方便的

下面这个demo出自于Android,几分钟教你怎么应用自定义注解

注解:

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface LayoutInject {
    int value() default -1;
}
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD, ElementType.PARAMETER, ElementType.METHOD})
public @interface ViewInject {
    int value() default -1;
}

两个注解,一个用于setContentView,一个用于findViewById
然后解析:

private int mLayoutId = -1;
/**
     * 注解布局 LayoutId
     */
    private void displayInjectLayout() {
        Class<?> clazz = this.getClass();
        if (clazz.getAnnotations() != null) {
            if (clazz.isAnnotationPresent(LayoutInject.class)) {
                LayoutInject inject = clazz.getAnnotation(LayoutInject.class);
                mLayoutId = inject.value();
                setContentView(mLayoutId);
            }
        }
    }

这个很好理解:首先获取到当前类,判断当前类上是否有使用注解LayoutInject,如果有,则拿到这个注解上的值,然后调用setContentView这个方法,(这里为什么会有这个方法,因为这两个解析方法,我定义在BaseActivity中的,继承了Activity)

    /**
     * 解析注解view id
     */
    private void displayInjectView() {
        if (mLayoutId <= 0) return;
        Class<?> clazz = this.getClass();
        Field[] fields = clazz.getDeclaredFields();
        for (Field field : fields) {
            try {
                if (field.getAnnotations() != null) {
                    if (field.isAnnotationPresent(ViewInject.class)) {
                        field.setAccessible(true);
                        ViewInject inject = field.getAnnotation(ViewInject.class);
                        field.set(this, this.findViewById(inject.value()));
                    }
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

这个也好理解:判断布局是否存在,如果不在,也就没必要进行下去了;同样获取到当前类,通过反射拿到它的所有Field,然后循环查找判断这个Field上是否有ViewInject这个注解,有的话就取到值,然后调用它的findViewById方法。

使用:

public class BaseActivity extends AppCompatActivity {
    private int mLayoutId = -1;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        displayInjectLayout();
        displayInjectView();
    }

    private void displayInjectView() {
        ......
    }

    private void displayInjectLayout() {
        ......
    }

}
@LayoutInject(R.layout.activity_main)
public class MainActivity extends BaseActivity {

    @ViewInject(R.id.tv_message)
    private TextView tv_message;

    @ViewInject(R.id.dbText)
    private TextView dbText;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        ......
    }
}

这样用起来也是挺方便的,但是使用反射总为人诟病,虽然实际上用于setContentView和findViewById在反射上并没有降低性能,JAVA反射会降低你的程序性能吗?(原文的网址我打不开,我看的就是这个,能科学上网的自己去xxx)上测试后说只有只能执行100万遍的时候才能看出差别,那我们这个百万分之一的性能能影响多少呢?

现在大部分都使用apt,就跟着学习一下;在摸索了好几天之后,才有点收获,因为现在gradle2.2之后普遍使用annotationProcessor,所以我也就从这里开始,之前的android.apt我就不深入碰了

这个主要分四部分:
apt-annotation 注解
apt-compiler 解析器(主要)
apt-api 调用的封装
apt-service 声明
apt-app 注解应用

主要还是前四个部分,最后一个就是example

现在根据一个demo来说:
一小时搞明白注解处理器(Annotation Processor Tool)
apt-demo

apt-annotation

注解这个没什么好说的,元注解就那么几个,只是这里保留的时期不再是RUNTIME

@Retention(RetentionPolicy.CLASS)
@Target(ElementType.FIELD)
public @interface BindView {
    int value();
}

apt-compiler

这个就比较重要了,写法比较套路化
一个就四个方法:

public synchronized void init(ProcessingEnvironment processingEnv)
public SourceVersion getSupportedSourceVersion()
public Set<String> getSupportedAnnotationTypes()
public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnv)

其中中间两个还可以作为注解申明在类头上

第一个主要是获取辅助工具:比如打印信息,生成文件,元素工具
第二个是声明支持的java版本
第三个是支持解析的注解
第四个就是注解解析并生成java文件

    private Elements elementUtils;
    private Filer filer;

    @Override
    public synchronized void init(ProcessingEnvironment processingEnv) {
        super.init(processingEnv);
        elementUtils = processingEnv.getElementUtils();
        filer = processingEnv.getFiler();
    }

    @Override
    public SourceVersion getSupportedSourceVersion() {
        return SourceVersion.latestSupported();
    }

    @Override
    public Set<String> getSupportedAnnotationTypes() {
        Set<String> types = new LinkedHashSet<>();
        types.add(ContentView.class.getCanonicalName());
        types.add(BindView.class.getCanonicalName());
        types.add(OnClick.class.getCanonicalName());
        return types;
    }

这三个方法写起了基本固定

主要是最后一个:
这里有一个概念很重要Element:

package com.example;        // PackageElement  

public class MyClass {      // TypeElement  

    private int a;          // VariableElement  

    private Foo other;      // VariableElement  

    public Foo () {}        // ExecuteableElement  

    public void setA (      // ExecuteableElement  
                int newA    // TypeElement  
                ) {  

    }  
}  

对照着能看出来,(包pacekage)、(类、枚举、接口type)、(属性variable)、(方法excuteable)

@AutoService(Processor.class)
public class BindViewProcessor extends AbstractProcessor {
    private Elements elementUtils;
    private Filer filer;
    private Map<String, AnnotatedClass> annotatedClassMap = new HashMap<>();

    @Override
    public synchronized void init(ProcessingEnvironment processingEnv) {
        super.init(processingEnv);
        elementUtils = processingEnv.getElementUtils();
        filer = processingEnv.getFiler();
    }

    @Override
    public SourceVersion getSupportedSourceVersion() {
        return SourceVersion.latestSupported();
    }

    @Override
    public Set<String> getSupportedAnnotationTypes() {
        Set<String> types = new LinkedHashSet<>();
        types.add(ContentView.class.getCanonicalName());
        types.add(BindView.class.getCanonicalName());
        types.add(OnClick.class.getCanonicalName());
        return types;
    }

    @Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnv) {
        annotatedClassMap.clear();
        try {
            processBindView(roundEnv);
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }

        try {
            for (AnnotatedClass annotatedClass : annotatedClassMap.values()) {
                annotatedClass.generateFinder().writeTo(filer);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }

        return true;
    }

    private void processBindView(RoundEnvironment roundEnv) throws IllegalAccessException {
        for (Element element : roundEnv.getElementsAnnotatedWith(BindView.class)) {
            AnnotatedClass annotatedClass = getAnnotatedClass(element);
            BindViewField field = new BindViewField(element);
            annotatedClass.addField(field);
        }
    }

    private AnnotatedClass getAnnotatedClass(Element element) {
        TypeElement encloseElement = (TypeElement) element.getEnclosingElement();
        String fullClassName = encloseElement.getQualifiedName().toString();
        AnnotatedClass annotatedClass = annotatedClassMap.get(fullClassName);
        if (annotatedClass == null) {
            annotatedClass = new AnnotatedClass(encloseElement, elementUtils);
            annotatedClassMap.put(fullClassName, annotatedClass);
        }
        return annotatedClass;
    }

}

从上往下看:
@AutoService这个注解就是用来顶替apt-service这个模块的

implementation 'com.google.auto.service:auto-service:1.0-rc2'

Elements是元素操作的辅助工具
Filer是生成额Java文件的辅助工具
然后主要就集中在processor中

还可以看到多出来两个方法,又涉及到两个实体:

package com.xiey94.bindview_compiler;

import com.squareup.javapoet.ClassName;
import com.squareup.javapoet.JavaFile;
import com.squareup.javapoet.MethodSpec;
import com.squareup.javapoet.ParameterizedTypeName;
import com.squareup.javapoet.TypeName;
import com.squareup.javapoet.TypeSpec;
import com.xiey94.bindview_annotation.ContentView;

import java.util.ArrayList;
import java.util.List;

import javax.lang.model.element.Modifier;
import javax.lang.model.element.TypeElement;
import javax.lang.model.util.Elements;

/**
 * Created by xieyang on 2018/5/7.
 * Email: [email protected]
 */

public class AnnotatedClass {
    private TypeElement classElement;
    private List<BindViewField> fields;
    private List<OnClickMethod> methods;
    private List<ContentViewClass> contentViewClasses;
    private ContentViewClass contentViewClass;
    public Elements elementUtils;

    public AnnotatedClass(TypeElement classElement, Elements elements) {
        this.classElement = classElement;
        this.elementUtils = elements;
        this.fields = new ArrayList<>();
        this.methods = new ArrayList<>();
        this.contentViewClasses = new ArrayList<>();
    }

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

    public JavaFile generateFinder() {
        MethodSpec.Builder methodBuilder = MethodSpec.methodBuilder("inject")
                .addModifiers(Modifier.PUBLIC)
                .addAnnotation(Override.class)
                .addParameter(TypeName.get(classElement.asType()), "host", Modifier.FINAL)
                .addParameter(TypeName.OBJECT, "source")
                .addParameter(TypeUtil.FINDER, "finder");

        for (BindViewField field : fields) {
            methodBuilder.addStatement("host.$N=($T)finder.findView(source,$L)", field.getFieldName()
                    , ClassName.get(field.getFieldType()), field.getId());
        }

        String packageName = getPackageName(classElement);
        String className = getClassName(classElement, packageName);
        ClassName bindClassName = ClassName.get(packageName, className);

        TypeSpec finderClass = TypeSpec.classBuilder(bindClassName.simpleName() + "$$Injector")
                .addModifiers(Modifier.PUBLIC)
                .addSuperinterface(ParameterizedTypeName.get(TypeUtil.INJECTOR, TypeName.get(classElement.asType())))
                .addMethod(methodBuilder.build())
                .build();
        return JavaFile.builder(packageName, finderClass).build();
    }

    public String getPackageName(TypeElement type) {
        return elementUtils.getPackageOf(type).getQualifiedName().toString();
    }

    public String getClassName(TypeElement type, String packageName) {
        int packageLen = packageName.length() + 1;
        return type.getQualifiedName().toString().substring(packageLen).replace('.', '$');
    }


}
package com.xiey94.bindview_compiler;

import com.xiey94.bindview_annotation.BindView;

import javax.lang.model.element.Element;
import javax.lang.model.element.ElementKind;
import javax.lang.model.element.Name;
import javax.lang.model.element.VariableElement;
import javax.lang.model.type.TypeMirror;

/**
 * Created by xieyang on 2018/5/7.
 * Email: [email protected]
 */

public class BindViewField {
    private int id;
    private VariableElement fieldElement;

    public BindViewField(Element element) throws IllegalAccessException {
        if (element.getKind() != ElementKind.FIELD) {
            throw new IllegalAccessException(String.format("wrong1"));
        }

        fieldElement = (VariableElement) element;
        BindView bindView = fieldElement.getAnnotation(BindView.class);
        id = bindView.value();
        if (id < 0) {
            throw new IllegalAccessException(String.format("wrong2"));
        }
    }

    public int getId() {
        return id;
    }

    public Name getFieldName() {
        return fieldElement.getSimpleName();
    }

    public TypeMirror getFieldType() {
        return fieldElement.asType();
    }
}

这里需要提前将apt-api放出来,里面的代码思想是:
定义一个接口,让生成的代码实现这个接口,然后解析的时候,将解析的代码放置到这个方法中,然后反射拿到这个类的实例,调用这个接口方法

public interface Injector<T> {
    void inject(T host, Object source, IFinder finder);
}
public class ButterKnife {

    private ButterKnife() {
    }

    private static final ActivityFinder ACTIVITY_FINDER = new ActivityFinder();

    private static final ViewFinder VIEW_FINDER = new ViewFinder();

    private static Map<String, Injector> FINDER_MAP = new HashMap<>();

    public static void bind(Activity activity) {
        bind(activity, activity, ACTIVITY_FINDER);
    }

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

    public static void bind(Object host, View view) {
        bind(host, view, VIEW_FINDER);
    }

    private static void bind(Object host, Object source, IFinder finder) {
        String className = host.getClass().getName();
        try {
            Injector injector = FINDER_MAP.get(className);
            if (injector == null) {
                Class<?> finderClass = Class.forName(className + "$$Injector");
                injector = (Injector) finderClass.newInstance();
                FINDER_MAP.put(className, injector);
            }
            injector.inject(host, source, finder);
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (InstantiationException e) {
            e.printStackTrace();
        }
    }
}

这个接口主要是用来寻找id的

public interface IFinder {
    Context getContext(Object source);

    View findView(Object source, int id);
}
public class ActivityFinder implements IFinder {
    @Override
    public Context getContext(Object source) {
        return (Activity) source;
    }

    @Override
    public View findView(Object source, int id) {
        return ((Activity) source).findViewById(id);
    }
}
public class ViewFinder implements IFinder {
    @Override
    public Context getContext(Object source) {
        return ((View) source).getContext();
    }

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

再回到解析器那块,
BindViewField:这个主要是把带注解的属性封装成一个实体
在构造器方法中,拿到元素,判断元素所属的类型,然后拿到元素上的注解,拿到注解的值,两个get方法,一个是该元素锁代表的属性的名称和id值

AnnotatedClass:将一个类封装成一个实体(类中包含带注解的属性)
在构造器方法中获取需要的工具;一个addField方法,将所属当前类的带注解的属性装进集合中,然后就是生成文件generateFinder,这个方法先放下,回去看看BindViewProcessor方法
在process方法中调用了processBindView方法,这个方法就是针对整个应用取出来所有的带BindView注解的Element(也就是Field属性),然后初始化AnnotateClass,调用了getAnnotatedClass方法,在这个方法里面,首先是获取类名(这个可作为key为唯一标示),然后判断当前集合中是否存在这个实例,避免重复创建,然后初始化BindViewField对象,将这个对象扔到AnnotatedClass中,这样就完成了分拣工作,(将带注解的Element扔到自己对应的类中,再包所有的类放到集合中,最后遍历这个集合生成Java文件),也就到了generateFilder这个方法;

这里用到了一个生成Java文件的库,省的去拼装

implementation 'com.squareup:javapoet:1.9.0'

具体用法去他们的github上看看JavaPoet

开始生成一个inject方法,继承Injector,在该方法中调用IFinder的findView方法,最后生成xx$$Injector类文件

apt-api

看上面

apt-app

调用比较简单,

@ContentView(R.layout.activity_main)
public class MainActivity extends AppCompatActivity {

    @BindView(R.id.tv_1)
    public TextView tv1;
    @BindView(R.id.tv_2)
    public TextView tv2;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        ButterKnife.bind(this);
        tv1.setText("success !!!");
        tv2.setText("success two !!!");
    }

    @OnClick({R.id.tv_1, R.id.tv_2})
    public void onClick(View view) {
        switch (view.getId()) {
            case R.id.tv_1:
                Toast.makeText(MainActivity.this, "1", Toast.LENGTH_SHORT).show();
                break;
            case R.id.tv_2:
                Toast.makeText(MainActivity.this, "2", Toast.LENGTH_SHORT).show();
                break;
            default:
                break;
        }
    }
}

注解生成文件,

public class MainActivity$$Injector implements Injector<MainActivity> {
  @Override
  public void inject(final MainActivity host, Object source, IFinder finder) {
    host.setContentView(2131296283);
    host.tv1=(TextView)finder.findView(source,2131165315);
    host.tv2=(TextView)finder.findView(source,2131165316);
    View.OnClickListener listener;
    listener=new View.OnClickListener() {
      @Override
      public void onClick(View view) {
        host.onClick(view);
      }
    } ;
    finder.findView(source,2131165315).setOnClickListener(listener);
    finder.findView(source,2131165316).setOnClickListener(listener);
  }
}
ButterKnife.bind(this);

通过apt-api调用生成的文件
整个流程大概就是这样,再来梳理一下:
1、定义注解(BindView)
2.1、将带注解的元素封装成实体,主要是用来获取元素名称和注解所带的参数(BindViewField)
2.2、将注解所在的类封装成实体,将注解实体扔进来,获取注解实体的值,然后生成Java文件(AnnotatedClass)
2.3、不同的注解排队一个一个来进行如上操作(process-processBindView)
3、在调用方法中反射获取到生成的Java文件当前类的实例,调用定义好的接口方法(injector.inject)

demo:GitHub

Android,几分钟教你怎么应用自定义注解
JAVA反射会降低你的程序性能吗?
一小时搞明白注解处理器(Annotation Processor Tool)
apt-demo
JavaPoet
JavaPoet - 优雅地生成代码
APT
Android 利用 APT 技术在编译期生成代码
Android Studio中使用apt
利用APT实现Android编译时注解
你必须知道的APT、annotationProcessor、android-apt、Provided、自定义注解
Android之使用apt编写编译时注解
Android APT(编译时生成代码)
Android Apt之Activity Route的示例
FastRoute
安卓AOP三剑客:APT,AspectJ,Javassist
你必须知道的APT、annotationProcessor、android-apt、Provided、自定义注解
GitHub


猜你喜欢

转载自blog.csdn.net/xiey94/article/details/80251082