AOP之注解处理器APT在Android中的FinderView实际详解

Posted 小钟视野

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了AOP之注解处理器APT在Android中的FinderView实际详解相关的知识,希望对你有一定的参考价值。

一 前言

         android中现今流行的各大框架,比如ButterFly、eventBus、OrmLite、Retrofit等都使用注解,注解是什 么呢?注解就是元数据,可以理解为属性、方法、类等的一个说明,具体详解可百度,也可移步我的另一篇注解原理详解。一下就以ButterFly为例,解读徒手打造一个FinderView的框架。

       获取注解的元数据的方式有以下两种:

           1、直接通过Class|Method|Field.getAnnotation(xxxAnnotation.class)获取注解实例,在获取元数据,                  具体可查看注解原理详解

           2、通过注解处理器APT来获取。

      这里使用的是方式二。注解器处理是在build编译时执行的

二 原理

        1、定义Method、Field的注解类分别为OnClick、BindView,分别应用于Method和Field

        2、apt注解处理器解析注解类,获取Method/Field,随后通过javapoet框架创建一个类为xxxFind实现为每               个Field生成findViewById()的实现方法,为每个Method生成OnClickListener监听器并实现调用被注解                  的Method

       3、通过工具类Inject(activity)调用,实现反射xxxFind,调用上述的方法实现Method、Field初始化

三 详解

   注:

为了减少麻烦 这里需要使用如下依赖:

项目的build.gradle中

   向仓库中添加组件对apt的依赖 :classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8'

module中的build.gradle引用两个库:

   apt编译执行时的库 :   compile 'com.squareup:javapoet:1.7.0'

   生成java代码的库:      compile 'com.google.auto.service:auto-service:1.0-rc2'

1.明确我们的注解是在编译时期进行的,而且只用在控件属性和点击方法,所以定义了如下注解类:

        (1)方法注解类OnClick:

@Retention(RetentionPolicy.CLASS)
@Target(ElementType.METHOD)
public @interface OnClick 
    int[] value();

       (2)控件属性注解类BindView:

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

   因为初始化Method和Field都需要控件id,所以每个注解都要带控件id,所以需要value值,而且是必须的。

 2.apt的解析和生成自动初始化类以及相关方法实现

    如要生成如下格式:  

public class MainActivity$$Finder implements Finder<MainActivity> 
  @Override
  public void inject(final MainActivity host, Object source, Provider provider) 
    host.mTextView = (TextView)(provider.findView(source, 2131427414));
    host.mButton = (Button)(provider.findView(source, 2131427413));
    host.mEditText = (EditText)(provider.findView(source, 2131427412));
    View.OnClickListener listener;
    listener = new View.OnClickListener() 
      @Override
      public void onClick(View view) 
        host.onButtonClick();
      
     ;
    provider.findView(source, 2131427413).setOnClickListener(listener);
    listener = new View.OnClickListener() 
      @Override
      public void onClick(View view) 
        host.onTextClick();
      
     ;
    provider.findView(source, 2131427414).setOnClickListener(listener);
  

apt代码走起:

 apt需要继承AbstractProcessor并实现如下方法:

/**
 * 使用 Google 的 auto-service 库可以自动生成 META-INF/services/javax.annotation.processing.Processor 文件
 */
@AutoService(Processor.class)//用 @AutoService 来注解这个处理器,可以自动生成配置信息
public class ViewFinderProcesser extends AbstractProcessor 

    private Filer mFiler; //文件的类
    private Elements mElementUtils; //元素相光类
    private Messager mMessager;//日志相关类,也可以是用java的system.out输出日志

	/***初始化会调用,一个处理器只执行一次*/
    @Override
    public synchronized void init(ProcessingEnvironment processingEnv) 
        super.init(processingEnv);
       
    

    /**
     * 支持的注解类型
     * @return types  指定哪些注解应该被注解处理器注册
     */
    @Override
    public Set<String> getSupportedAnnotationTypes() 
       
    

    /**
     * @return 指定使用的 Java 版本。通常返回 SourceVersion.latestSupported()。
     */
    @Override
    public SourceVersion getSupportedSourceVersion() 
        return SourceVersion.latestSupported();
    

    private Map<String, AnnotatedClass> mAnnotatedClassMap = new HashMap<>();

    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) 
        
//        return true;//返回是True,那么后续的处理器就不会在处理
        return false;//返回是false,那么后续处理器会继续处理它们。一个处理器可能总是返回同样的逻辑值,或者是根据选项改变结果。

    

最主要的方法:

1.init():初始化方法,一个处理器执行一次

2.getSupportedAnnotationTypes():指定这个处理器只处理哪些注解类

3.getSupportedSourceVersion():指定处理器使用的jdk版本

4.process(annotations,RoundEnvironment ):处理器最主要的方法,用于处理注解

   annotations:是此次处理注解类的集合

   RoundEnvironment :当作处理器和元素之间的上下文,就是个通信桥梁

接下来主要看看主要分析:

1.引入自动处理器的库之后在处理器实现类中使用注解的方式编译器就会执行这个类

@AutoService(Processor.class)//用 @AutoService 来注解这个处理器,可以自动生成配置信息
public class ViewFinderProcesser extends AbstractProcessor 

2.init()初始化文件相关类、元素(包括类、属性、方法等)相关类、日志管理类

@Override
    public synchronized void init(ProcessingEnvironment processingEnv) 
        super.init(processingEnv);
        System.out.println("=== init ");
        mFiler = processingEnv.getFiler();
        mElementUtils = processingEnv.getElementUtils();
        mMessager = processingEnv.getMessager();
    

3.这里标注处理器要处理的注解类:有BindView和OnClick

/**
     * 支持的注解类型
     * @return types  指定哪些注解应该被注解处理器注册
     */
    @Override
    public Set<String> getSupportedAnnotationTypes() 
        Set<String> types = new LinkedHashSet<>();
        types.add(BindView.class.getCanonicalName());
        types.add(OnClick.class.getCanonicalName());
        return types;
    
4.通过上下文roundEnv处理注解器
 @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) 
        mAnnotatedClassMap.clear();
        try 
            info("process== %s", annotations.toString());//
            processBindView(roundEnv);
            processOnClick(roundEnv);
         catch (IllegalArgumentException e) 
            info("Generate file failed,1111 reason: %s", e.getMessage());
            return false; // stop process
        

//        System.out.println("=== annotatedClass "+mAnnotatedClassMap.size());
        for (AnnotatedClass annotatedClass : mAnnotatedClassMap.values()) 
            try 
//                System.out.println("=== annotatedClass "+annotatedClass.getFullClassName());
                info("Generating file for %s", annotatedClass.getFullClassName());
                annotatedClass.generateFinder().writeTo(mFiler);
             catch (IOException e) 
                info("Generate file failed,2222 reason: %s", e.getMessage());
                return false;
            
        
//        return true;//不再执行这个
        return false;//

    

mAnnotatedClassMap:是一个map用于缓存每一个注解相关信息

processBindView(roundEnv);
processOnClick(roundEnv);

这两个方法实现都差不多,如下:

private void processBindView(RoundEnvironment roundEnv) throws IllegalArgumentException 
        //get BindView AnnotionType for all Current Class  of Elements
        for (Element element : roundEnv.getElementsAnnotatedWith(BindView.class)) 
            // TODO: 16/8/4
            AnnotatedClass annotatedClass = getAnnotatedClass(element);
            info("element name %s", element.getSimpleName());
            BindViewField field = new BindViewField(element);
            annotatedClass.addField(field);
        
    

    private void processOnClick(RoundEnvironment roundEnv) 
        for (Element element : roundEnv.getElementsAnnotatedWith(OnClick.class)) 
            AnnotatedClass annotatedClass = getAnnotatedClass(element);
            info("element OnClick %s", element.getSimpleName());
            OnClickMethod method = new OnClickMethod(element);
            annotatedClass.addMethod(method);
        
    

主要看这个方法:

roundEnv.getElementsAnnotatedWith(OnClick.class)
roundEnv.getElementsAnnotatedWith(BindView.class)

这里是获取被注解类OnClick和BindView注解的所有元素(包括类、方法、属性等)

随后调用getAnnotatedClass(element);

private AnnotatedClass getAnnotatedClass(Element element) 
        TypeElement classElement = (TypeElement) element.getEnclosingElement();
        String fullClassName = classElement.getQualifiedName().toString();
        info("element fullClassName %s", fullClassName+" e "+element.getSimpleName());
        AnnotatedClass annotatedClass = mAnnotatedClassMap.get(fullClassName);
        if (annotatedClass == null) 
            annotatedClass = new AnnotatedClass(classElement, mElementUtils);
            mAnnotatedClassMap.put(fullClassName, annotatedClass);
        
        return annotatedClass;
    

主要是获取当前类名,创建一个AnnotationClass实例,将类名作为map的key,实例作为value缓存

为了帮助理解补充如下:

element.getEnclosingElement();// 获取父元素
element.getEnclosedElements();// 获取子元素

其中父元素、子元素是根据xml中dom树来决定的不是java中继承关系上的父子元素。

所以这里使用TypeElement classElement = (TypeElement) element.getEnclosingElement();

获取父元素并强转成TypeElement。那怎么知道是TypeElement而不是其VariableElement。这就要理解关系如下

public class Foo  // TypeElement

	private int a; // VariableElement
	private Foo other; // VariableElement

	public Foo()  // ExecuteableElement

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

这里表示每个类型关系:映射到dom树的层级关系中。如定义的是方法、属性那么element实际类型是ExecuteableElement和VariableElement,那么element.getEnclosingElement();就是获取父元素,父元素就是TypeElement.如果了解html查找摸个元素层级,应该会很好理解。

以上补充完毕,代码相信读者都很好理解了,言归正传回到之前的思路:

调用getAnnatationClasss()之后,将当前类的所有的注解类转化成一个AnnotationClass缓存到Map中

也就是Map中缓存了一个以当前类名为key,AnnotationClass为Value的Map,当前类有使用BindView或OnClick注解。在这里Map会缓存两个AnnotationClass。如下:

Map的key:

com.sample.MainActivity  : 在MainActivity下使用了注解,所以会生成一个对应的AnnotationClass

com.sample.SecondActivity:在SecondActivity下使用了注解,所以会生成一个对应的AnnotationClass

获取到AnnotationClass实例之后,回到方法出:再次贴出方法:

private void processBindView(RoundEnvironment roundEnv) throws IllegalArgumentException 
        //get BindView AnnotionType for all Current Class  of Elements
        for (Element element : roundEnv.getElementsAnnotatedWith(BindView.class)) 
            // TODO: 16/8/4
            AnnotatedClass annotatedClass = getAnnotatedClass(element);
            info("element name %s", element.getSimpleName());
            BindViewField field = new BindViewField(element);
            annotatedClass.addField(field);
        
    

new BindViewField(element)的实现如下:

public BindViewField(Element element) throws IllegalArgumentException 
        if (element.getKind() != ElementKind.FIELD) 
            throw new IllegalArgumentException(
                String.format("Only fields can be annotated with @%s", BindView.class.getSimpleName()));
        

        mFieldElement = (VariableElement) element;
        BindView bindView = mFieldElement.getAnnotation(BindView.class);
        mResId = bindView.value();

        if (mResId < 0) 
            throw new IllegalArgumentException(
                String.format("value() in %s for field %s is not valid !", BindView.class.getSimpleName(),
                    mFieldElement.getSimpleName()));
        
    

主要是解析注解的元数据,获取控件属性的id,并保存到实例当中,

new OnClickMethod(element)和BindViewField(element)一模一样,不重复解析。

创建相应的类:方法实例和属性实例 随后通过addxxx()加入到annotation实例中,最后看看map调用处

for (AnnotatedClass annotatedClass : mAnnotatedClassMap.values()) 
            try 
//                System.out.println("=== annotatedClass "+annotatedClass.getFullClassName());
                info("Generating file for %s", annotatedClass.getFullClassName());
                annotatedClass.generateFinder().writeTo(mFiler);
             catch (IOException e) 
                info("Generate file failed,2222 reason: %s", e.getMessage());
                return false;
            
        

最主要的代码就是annotatedClass.generateFinder().writeTo(mFiler);也就是生成初始化代码类以及实现,然后写入到文件中,也就是生成了一个java类,类名是当前类名$$Finder

AnnotationClass如下:

public class AnnotatedClass 

    public TypeElement mClassElement;//父元素:这里表示当前类
    public List<BindViewField> mFields;//被注解的元素:这里是属性
    public List<OnClickMethod> mMethods;//被注解的元素:这里是方法
    public Elements mElementUtils;//元素操作类

    public AnnotatedClass(TypeElement classElement, Elements elementUtils) 
        this.mClassElement = classElement;
        this.mFields = new ArrayList<>();
        this.mMethods = new ArrayList<>();
        this.mElementUtils = elementUtils;
    

    public String getFullClassName() 
        return mClassElement.getQualifiedName().toString();
    

    public void addField(BindViewField field) 
        mFields.add(field);
    

    public void addMethod(OnClickMethod method) 
        mMethods.add(method);
    

	/***
	*生成实现类,实现开头3.2贴出的实现格式
	*/
    public JavaFile generateFinder() 

        // method inject(final T host, Object source, Provider provider)
        MethodSpec.Builder injectMethodBuilder = MethodSpec.methodBuilder("inject")
            .addModifiers(Modifier.PUBLIC)
            .addAnnotation(Override.class)
            .addParameter(TypeName.get(mClassElement.asType()), "host", Modifier.FINAL)
            .addParameter(TypeName.OBJECT, "source")
            .addParameter(TypeUtil.PROVIDER, "provider");

        for (BindViewField field : mFields) 
            // find views
            injectMethodBuilder.addStatement("host.$N = ($T)(provider.findView(source, $L))", field.getFieldName(),
                ClassName.get(field.getFieldType()), field.getResId());
        

        if (mMethods.size() > 0) 
            injectMethodBuilder.addStatement("$T listener", TypeUtil.ANDROID_ON_CLICK_LISTENER);
        
        for (OnClickMethod method : mMethods) 
            // declare OnClickListener anonymous class
            TypeSpec listener = TypeSpec.anonymousClassBuilder("")
                .addSuperinterface(TypeUtil.ANDROID_ON_CLICK_LISTENER)
                .addMethod(MethodSpec.methodBuilder("onClick")
                    .addAnnotation(Override.class)
                    .addModifiers(Modifier.PUBLIC)
                    .returns(TypeName.VOID)
                    .addParameter(TypeUtil.ANDROID_VIEW, "view")
                    .addStatement("host.$N()", method.getMethodName())
                    .build())
                .build();
            injectMethodBuilder.addStatement("listener = $L ", listener);
            for (int id : method.ids) 
                // set listeners
                injectMethodBuilder.addStatement("provider.findView(source, $L).setOnClickListener(listener)", id);
            
        
        // generate whole class
        TypeSpec finderClass = TypeSpec.classBuilder(mClassElement.getSimpleName() + "$$Finder")
            .addModifiers(Modifier.PUBLIC)
            .addSuperinterface(ParameterizedTypeName.get(TypeUtil.FINDER, TypeName.get(mClassElement.asType())))
            .addMethod(injectMethodBuilder.build())
            .build();

        String packageName = mElementUtils.getPackageOf(mClassElement).getQualifiedName().toString();

        return JavaFile.builder(packageName, finderClass).build();
    

这里主要用到了javapoet库,所以要讲解主要用到的方法是什么玩意:

1.addModifiers:添加修饰符:如public private protected等

2.addAnnotation:添加注解

3.addParameter:添加参数 如Override

4.addStatement("$L listerner",ClassName):添加语句:

   如 :

   injectMethodBuilder.addStatement("provider.findView(source, $L).setOnClickListener(listener)", id);

    $L是变量,id是这个变量的值

5.returns(TypeName.VOID):返回类型

6.addSuperinterface(TypeUtil.ANDROID_ON_CLICK_LISTENER)实现接口

7.TypeSpec.anonymousClassBuilder("")构建一个匿名内部类

8.MethodSpec.methodBuilder("inject")构建一个方法名为inject

9.TypeSpec.classBuilder(mClassElement.getSimpleName() + "$$Finder")构建一个实体类,名为当前类名        $$Finder

10. JavaFile.builder(packageName, finderClass).build();构造这模式创建JavaFile实例

11.ParameterizedTypeName.get(TypeUtil.FINDER, TypeName.get(mClassElement.asType()))

     泛型参数。第一个参数是实现的接口,第二个参数是具体的泛型类型

最后调用了javaFile.writeTo(mFiler);将构建的内容写入文件,即生成了一个java文件。编译器也会对齐进行编译。

这样自动生成代码就完成了。接下来看看如何实现调用的?

自动生成方法:inject(final T host, Object source, Provider provider)

大致思路:

为了解耦Provider是个接口具体实现初始化的地方就是Provider的方法findView();

所以这里有两个实现:

ActivityProvider:在activity调用ViewInject.inject(activity),使用这个findView()

public class ActivityProvider implements Provider 
    @Override
    public Context getContext(Object source) 
        return ((Activity) source);
    

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

ViewProvide:

public class ViewProvider implements Provider 
    @Override
    public Context getContext(Object source) 
        return ((View) source).getContext();
    

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


一般使用:

@Override
    protected void onCreate(Bundle savedInstanceState) 
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        ViewFinder.inject(this);
    

ViewFinder:

public class ViewFinder 

    private static final ActivityProvider PROVIDER_ACTIVITY = new ActivityProvider();
    private static final ViewProvider PROVIDER_VIEW = new ViewProvider();

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

    public static void inject(Activity activity) 
        inject(activity, activity, PROVIDER_ACTIVITY);
    

    public static void inject(View view) 
        inject(view, view);
    

    public static void inject(Object host, View view) 
        // for fragment
        inject(host, view, PROVIDER_VIEW);
    

    /****
     *
     * @param host 是传入的宿主,属性方法所在的对象 实现解耦的地方
     * @param source 传入的source.findViewById()的source:可能是View/Activity/Fragement
     * @param provider 具体实现findViewByid()复制给属性的具体实现 ViewProvider/ActivityProvide/FragementProvider
     */
    public static void inject(Object host, Object source, Provider provider) 
        String className = host.getClass().getName();
        try 
            Finder finder = FINDER_MAP.get(className);
            if (finder == null) 
                Class<?> finderClass = Class.forName(className + "$$Finder");
                finder = (Finder) finderClass.newInstance();
                FINDER_MAP.put(className, finder);
            
            finder.inject(host, source, provider);
         catch (Exception e) 
            throw new RuntimeException("Unable to inject for " + className, e);
        
    

具体的参数host、source、provider的职责在参数上写的很清楚了。

传入的宿主,然后生成一个宿主类创建,通过反射实例化通过javapoet自动生成的具体实现,随后调用它的具体实现方法进行初始化。

看看ViewFinder.inject()有如下3个重载方法,第四个是留作扩展

1.inject(activity):用于在activity初始化控件

2.inject(View):用于view初始化子view:比如手动载入一个xml

3.inject(Fragment):用于fragment方法

4.inject(Hold,source):  Hold作为泛型的具体实现,自己生成hold的自动代码生成,仿造xxx$$Finder的实现。

这样就完成了。

总结:

1.ViewFinde.inject(this);会调用相应的方法,

2.通过反射调用javapoet生成的代码实现初始化

借此来了解整个apt工作方式。之后还会讲解AOP之Aspectj和javassist框架的实际应用。

此文章的解读原作者博客:如有侵权请告知

https://brucezz.itscoder.com/use-apt-in-android



demo:https://download.csdn.net/download/zhongwn/10426802













            

以上是关于AOP之注解处理器APT在Android中的FinderView实际详解的主要内容,如果未能解决你的问题,请参考以下文章

android注解处理技术APT

框架手写系列---apt注解处理器方式实现ButterKnife框架

拓展篇:注解处理器最佳实践

04注解处理器(APT)是什么?——《Android打怪升级之旅》

日志异常处理-spring aop注解

Android注解使用之通过annotationProcessor注解生成代码实现自己的ButterKnife框架