手写ButterKnife来搞明白Android注解处理器

Posted 丶笑看退场

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了手写ButterKnife来搞明白Android注解处理器相关的知识,希望对你有一定的参考价值。

Butterknife现在在项目中基本没用到了,逐渐被ViewBinding所代替,而我们所熟知它的内部原理是通过自定义注解+自定义注解解析器来动态生成代码并为我们的view绑定id的。今天就通过重新手写ButterKinife来搞明白我们今天的主角–Anotation Processing(注解处理器)。

源码地址APTDemo

运行时注解

在写注解处理器之前,先用运行时注解来操作下。这里我们先新建一个library取名lib-reflection

然后自定义注解,我们只实现了View与id的绑定功能,所以我们这里定义:

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

Target的Type说明这是一个用于修饰类和接口的注解,它也就是成员变量,即需要绑定资源id的view成员。

同时这个注解Retention是RUNTIME,表示注解不仅被保存到class文件中,jvm加载class文件之后,仍然存在。

注解定义好后就可以直接在项目中使用了;

public class MainActivity extends AppCompatActivity 
 
    @BindView(R.id.textView)
    TextView textView;

    @Override
    protected void onCreate(Bundle savedInstanceState) 
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
      	Binding.bind(this);
       textView.setText("哈哈哈哈");
    

注意这里我们加了个Binding.bind(this),就是它来帮助我们做注解的解析,之后在内部调用MainActivity.textView=(TextView)MainActivity.findViewById()来实现为view绑定id的。还是在刚才的目录lib-reflect下创建了Binding类,具体代码如下:

public class Binding 
    public static void bind(Activity activity)
        //反射获取注解注释
        for (Field field: activity.getClass().getDeclaredFields())
            BindView bindView = field.getAnnotation(BindView.class);
            if (bindView != null)
                try 
                    //扩大范围
                    field.setAccessible(true);
                    field.set(activity, activity.findViewById(bindView.value()));
                 catch (IllegalAccessException e) 
                    e.printStackTrace();
                
            
        
    

再运行就可以绑定了view的id了,不再需要我们手动的绑定id了。但是依靠反射始终是消耗性能的,这时候就用到我们的注解处理器了。

编译时注解

这里,我们新创建个Java Library的项目,就叫lib-processor吧。然后再这个目录下创建BindingProcessor,继承自AbstractProcessor。这个就是用于解析自定义注解的解析器了。不过要想让它生效还必须在resource下新建如下目录(现在google给提供了一个注册处理器的库@AutoService(Processor.class)的注解来简化我们的操作。):

javax.annotation.processing.Processor的文本文件里面内容就一行:

com.pince.lib_processor.BindingProcessor

接下来将之前定义的BindView注解改为编译时注解,放进新创建的lin-annotations目录下:

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

还需要修改build.gradle文件,加入:

app依赖:

implementation project(':lib')
annotationProcessor project(':lib-processor')

lib-processor依赖:

implementation project(':lib-annotations')

lib依赖:

//主项目需要的依赖
api project(':lib-annotations')

这么做就是为了让编译器使用我们的解析器用于解析注解。

后面的的工作都是在BindingProcessor操作了。通过读取类中的自定义注解,生成相应的绑定视图的代码,还需要引入一个库。

compile 'com.squareup:javapoet:1.9.0'

先展示下最后自动生成的类:

public class MainActivityBinding 
  public MainActivityBinding(MainActivity activity) 
    activity.textView = activity.findViewById(2131231093);
  

上面的内容就是由javapoet生成的,下面就按照上面这个最终效果来一步一步分析要怎么生成我们的代理类。

看下面我们需要创建一个构造函数<? extend Activity>作为参数传入:

String packageStr = element.getEnclosingElement().toString();
MethodSpec.Builder constructorBuilder = MethodSpec.constructorBuilder()
                    .addModifiers(Modifier.PUBLIC)
                    .addParameter(ClassName.get(packageStr, classStr), "activity");

然后查找所有标注了BidView注解的成员变量:

 for (Element enclosedElement: element.getEnclosedElements())
                if (enclosedElement.getKind() == ElementKind.FIELD)
                    //寻找BindView注释
                    BindView bindView = enclosedElement.getAnnotation(BindView.class);
                    if (bindView != null)
                        ....
                    
                
            

再然后是生成findViewById的具体语句代码:

for (Element enclosedElement: element.getEnclosedElements())
                if (enclosedElement.getKind() == ElementKind.FIELD)
                    //寻找BindView注释
                    BindView bindView = enclosedElement.getAnnotation(BindView.class);
                    if (bindView != null)
                        hasBinding = true;
                        constructorBuilder.addStatement("activity.$N = activity.findViewById($L)",
                                enclosedElement.getSimpleName(), bindView.value());
                    
                
            

做好了上面步骤,主要的代码也写完了,最后就是生成这个MainActivityBinding这个类:

String packageStr = element.getEnclosingElement().toString();
ClassName className = ClassName.get(packageStr, classStr + "Binding");
TypeSpec builtClass = TypeSpec.classBuilder(className)
                    .addModifiers(Modifier.PUBLIC)
                    .addMethod(constructorBuilder.build())
                    .build();
try 
                    JavaFile.builder(packageStr, builtClass)
                            .build().writeTo(filer);
                 catch (IOException e) 
                    e.printStackTrace();
                

注意这里的包名,生成的类的包名尽量与需要绑定的Activity所在的包名一致,这样BindView修饰的成员变量只需是包内可见就行。到了这里我们再执行./gradlew :app:compileDebugJava编译就可以自动生成我们所需要的类了。
到了这一步还没完,我们还需要在lib moodule目录下创建新的Binding帮助类:

public class Binding 
    public static void bind(Activity activity)
        try 
            Class bindingClass = Class.forName(activity.getClass().getCanonicalName() + "Binding");
            Constructor constructor = bindingClass.getDeclaredConstructor(activity.getClass());
            constructor.newInstance(activity);
         catch (ClassNotFoundException e) 
            e.printStackTrace();
         catch (NoSuchMethodException e) 
            e.printStackTrace();
         catch (IllegalAccessException e) 
            e.printStackTrace();
         catch (InstantiationException e) 
            e.printStackTrace();
         catch (InvocationTargetException e) 
            e.printStackTrace();
        
    

在这里利用了一点反射new出了MainActivityBinding实体,传入相应的activity在内部进行绑定操作。到这里简单的ButterKnife版本就实现了。下面BindingProcessor给出完整代码:

public class BindingProcessor extends AbstractProcessor 

    Filer filer;

    @Override
    public synchronized void init(ProcessingEnvironment processingEnv) 
        super.init(processingEnv);
      //使用Filer你可以创建文件
        filer = processingEnv.getFiler();
    

    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) 
				//获取到全部的类
      	//Element代表的是源代码
   			// Element可以是类、方法、变量等
        for (Element element: roundEnv.getRootElements())
            String packageStr = element.getEnclosingElement().toString();
            String classStr = element.getSimpleName().toString();
            
          //构建新的类的名字:原类名 + Binding
            ClassName className = ClassName.get(packageStr, classStr + "Binding");
          构建新的类的构造方法
            MethodSpec.Builder constructorBuilder = MethodSpec.constructorBuilder()
                    .addModifiers(Modifier.PUBLIC)
                    .addParameter(ClassName.get(packageStr, classStr), "activity");
            boolean hasBinding = false;
  				//还有个getEnclosingElement  单数 被包住的外层
            //子类里的元素  字段 方法 内部类
            for (Element enclosedElement: element.getEnclosedElements())
              //仅获取成员变量
                if (enclosedElement.getKind() == ElementKind.FIELD)
                    //寻找BindView注解
                    BindView bindView = enclosedElement.getAnnotation(BindView.class);
                    if (bindView != null)
                        hasBinding = true;
                      //在构造方法中加入代码
                        constructorBuilder.addStatement("activity.$N = activity.findViewById($L)",
                                enclosedElement.getSimpleName(), bindView.value());
                    
                
            

            TypeSpec builtClass = TypeSpec.classBuilder(className)
                    .addModifiers(Modifier.PUBLIC)
                    .addMethod(constructorBuilder.build())
                    .build();

            if (hasBinding)
                try 
                  //生成 Java 文件
                    JavaFile.builder(packageStr, builtClass)
                            .build().writeTo(filer);
                 catch (IOException e) 
                    e.printStackTrace();
                
            
        

        return false;
    

    @Override
    public Set<String> getSupportedAnnotationTypes() 
        //对这个注解进行注解处理
        return Collections.singleton(BindView.class.getCanonicalName());
    

源码地址APTDemo

注解后话

元注解是用来定义其他注解的注解(在自定义注解的时候,需要使用到元注解来定义我们的注解)。java.lang.annotation提供了四种元注解:@Retention@Target@Inherited@Documented

元注解说明
@Target表明我们注解可以出现的地方。是一个ElementType枚举
@Retention这个注解的的存活时间
@Document表明注解可以被javadoc此类的工具文档化
@Inherited是否允许子类继承该注解,默认为false

@Target

@Target-ElementType类型说明
ElementType.TYPE接口、类、枚举、注解
ElementType.FIELD字段、枚举的常量
ElementType.METHOD方法
ElementType.PARAMETER方法参数
ElementType.CONSTRUCTOR构造函数
ElementType.LOCAL_VARIABLE局部变量
ElementType.ANNOTATION_TYPE注解
ElementType.PACKAGE

@Retention

表示需要在什么几倍保存该注释信息,用于描述注解的生命周期。

@Retention-RetentionPolicy类型说明
RetentionPolicy.SOURCE注解只保留在源文件,当Java文件编译成class文件的时候,注解被遗弃
RetentionPolicy.CLASS注解被保留到class文件,但jvm加载class文件时候被遗弃,这是默认的生命周期
RetentionPolicy.RUNTIME解不仅被保存到class文件中,jvm加载class文件之后,仍然存在

@Document

@Document表明我们标记的注解可以被javadoc此类的工具文档化

@Inherited

@Inherited表明我们标记的注解是被继承的。比如,如果一个父类使用了@Inherited修饰的注解,则允许子类继承该父类的注解

注解解析

Class类里面常用方法如下:

/**
     * 包名加类名
     */
    public String getName();

    /**
     * 类名
     */
    public String getSimpleName();

    /**
     * 返回当前类和父类层次的public构造方法
     */
    public Constructor<?>[] getConstructors();

    /**
     * 返回当前类所有的构造方法(public、private和protected)
     * 不包括父类
     */
    public Constructor<?>[] getDeclaredConstructors();

    /**
     * 返回当前类所有public的字段,包括父类
     */
    public Field[] getFields();

    /**
     * 返回当前类所有申明的字段,即包括public、private和protected,
     * 不包括父类
     */
    public native Field[] getDeclaredFields();

    /**
     * 返回当前类所有public的方法,包括父类
     */
    public Method[] getMethods();

    /**
     * 返回当前类所有的方法,即包括public、private和protected,
     * 不包括父类
     */
    public Method[] getDeclaredMethods();

    /**
     * 获取局部或匿名内部类在定义时所在的方法
     */
    public Method getEnclosingMethod();

    /**
     * 获取当前类的包
     */
    public Package getPackage();

    /**
     * 获取当前类的包名
     */
    public String getPackageName$();

    /**
     * 获取当前类的直接超类的 Type
     */
    public Type getGenericSuperclass();

    /**
     * 返回当前类直接实现的接口.不包含泛型参数信息
     */
    public Class<?>[] getInterfaces();

    /**
     * 返回当前类的修饰符,public,private,protected
     */
    public int getModifiers();

Field和Method都实现了AnnotatedElement接口,常用方法如下:

/**
     * 指定类型的注释是否存在于此元素上
     */
    default boolean isAnnotationPresent(Class<? extends Annotation> annotationClass) 
        return getAnnotation(annotationClass) != null;
    

    /**
     * 返回该元素上存在的指定类型的注解
     */
    <T extends Annotation> T getAnnotation(Class<T> annotationClass);

    /**
     * 返回该元素上存在的所有注解
     */
    Annotation[] getAnnotations();

    /**
     * 返回该元素指定类型的注解
     */
    default <T extends Annotation> T[] getAnnotationsByType(Class<T> annotationClass) 
        return AnnotatedElements.getDirectOrIndirectAnnotationsByType(this, annotationClass);
    

    /**
     * 返回直接存在与该元素上的所有注释(父类里面的不算)
     */
    default <T extends Annotation> T getDeclaredAnnotation(Class<T> annotationClass) 
        Objects.requireNonNull(annotationClass);
        // Loop over all directly-present annotations looking for a matching one
        for (Annotation annotation : getDeclaredAnnotations()) 
            if (annotationClass.equals(annotation.annotationType())) 
                // More robust to do a dynamic cast at runtime instead
                // of compile-time only.
                return annotationClass.cast(annotation);
            
        
        return null;
    

    /**
     * 返回直接存在该元素岸上某类型的注释
     */
    default <T extends Annotation> T[] getDeclaredAnnotationsByType(Class<T> annotationClass) 
        return AnnotatedElements.getDirectOrIndirectAnnotationsByType(this, annotationClass);
    

    /**
     * 返回直接存在与该元素上的所有注释
     */
    Annotation[] getDeclaredAnnotations();

注解处理器后话

注解处理器(Annotation Processor)是javac的一个工具,它用来在编译时扫描和处理注解(Annotation)。你可以自定义注解,并注册相应的注解处理器(自定义的注解处理器需继承自AbstractProcessor)。

定义一个注解处理器,需要继承自AbstractProcessor。如下所示:

public class MyProcessor extends AbstractProcessor 

  /**
     * 每个Annotation Processor必须有一个空的构造函数。
     * 编译期间,init()会自动被注解处理工具调用,并传入ProcessingEnvironment参数,
     * 通过该参数可以获取到很多有用的工具类(Element,Filer,Messager等)
     */
    @Override
    public synchronized void init(ProcessingEnvironment env) 

  /**
     * 用于指定自定义注解处理器(Annotation Processor)是注册给哪些注解的(Annotation),
     * 注解(Annotation)指定必须是完整的包名+类名
     */
    @Override
    public boolean process(Set<? extends TypeElement> annoations, RoundEnvironment env)  

  /**
     * 用于指定你的java版本,一般返回:SourceVersion.latestSupported()
     */
    @Override
    public Set<String> getSupportedAnnotationTypes

以上是关于手写ButterKnife来搞明白Android注解处理器的主要内容,如果未能解决你的问题,请参考以下文章

手写ButterKnife

手写ButterKnife

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

Android编程入门--开源框架ButterKnife

动脑学院-手写ButterKnife框架(不包含自动生成代码)

Butterknife 在我的 Android 应用程序中根本不起作用