Android进阶9:手写Bufferknife(编译时注解)

说到Bufferknife,相信基本都用过。在Activity中使用:


class ExampleActivity extends Activity {
  @BindView(R.id.title) TextView title;
  @BindView(R.id.subtitle) TextView subtitle;
  @BindView(R.id.footer) TextView footer;
 
  @Override public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.simple_activity);
    ButterKnife.bind(this);
    // TODO Use fields...
  }
}

这样的话 我们就初始化绑定了View,那么其内部是什么原理呢? 我相信很多小伙伴都能说出来:注解+反射,但是具体怎么实现的,懵逼了吧?= - = ? 看完本篇博客我相信你也可以自己写一个。

需要掌握的知识点

  1. JavaPoet
  2. 注解
  3. 注解处理器(AbstractProcessor)
  4. apt (annotationProcessor)

上面的四个知识点是我们手写Bufferknife比需要掌握和理解的,下面我们一个一个讲解

apt (annotationProcessor)

ATP是一种处理注解的工具,它对源代码文件进行检测找出其中的Annotation,根据注解自动生成代码。 这里的源代码文件就是我们自己的包含@BindView注解的XXXActivity或者XXX Fragment。

annotationProcessor是APT工具的一种,是Google开发的内置框架,不需要特殊引入,比如我们是不是在引入Bufferknife的时候需要加入:

annotationProcessor 'com.jakewharton:butterknife-compiler:10.2.0'

这就表明,要自动检测我们的代码从而在编译的时候生成代码类。生成什么代码类呢?

public class MainActivity$ViewBinder implements ViewBinder 

也就是生成类似上面的代码类,这些自动生成的类的功能就是:findViewById 和 setOnClickListener。 好,那么生成这些类有什么用处呢? 试想一下,如果我们把这个类和我们的自己的XXXActivity关联起来,
也就是说让这些生成的类代替XXXActivity执行findViewById的操作,那么是不是就相当于实现了Bufferknife呢? 我想说是的!
好,现在总结一点:就是说,我们如果有我们自己的注解处理器,然后在我们的工程中引入:

annotationProcessor XXXX

这样的话,就可以自动扫描我们代码中使用该注解的Java文件,最后自动在生成Java文件。

我们创建了注解BindView, 然后在XXXActivity中使用该注解,通过APT在编译器的扫描,那最后可以产出Java文件,而这个Java文件是我们自己定义创建的。

好,这样的话APT我们知道了,上图中的注解是怎么定义的呢?

注解

说到注解我们肯定很熟悉Override吧? 看下源代码

package java.lang;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target({ElementType.METHOD})
@Retention(RetentionPolicy.SOURCE)
public @interface Override {
}

这是一个标准的注解定义,首先Retention定义了注解的“生命周期”,也就是起作用的时间段:只在源码阶段保留,在编译器进行编译时它将被丢弃忽视。
其次Target定义了该注解要用在什么用途?换句话说就是:这个注解要用在字段?类?方法?接口?枚举?

说到Retention,分三种类型:

  • RetentionPolicy.SOURCE 注解只在源码阶段保留,在编译器进行编译时它将被丢弃忽视。
  • RetentionPolicy.CLASS 注解只被保留到编译进行的时候,它并不会被加载到 JVM 中。
  • RetentionPolicy.RUNTIME 注解可以保留到程序运行的时候,它会被加载进入到 JVM 中,所以在程序运行时可以获取到它们。

RUNTIME是运行时注解,在程序运行的时候才起作用,然后在程序运行的时候,再通过反射,所具体的操作。
比如:

   /**
     * 绑定View
     * @param object
     * @param view
     */
    private static void bindView(Object object, View view) {
        Field[] declaredFields = object.getClass().getDeclaredFields();
        for(Field field : declaredFields) {
            boolean annotationPresent = field.isAnnotationPresent(BindView.class);
            if(!annotationPresent) {
                return;
            }
            BindView fieldAnnotation = field.getAnnotation(BindView.class);
            field.setAccessible(true);
            try {
                field.set(object, view.findViewById(fieldAnnotation.resIdValue()));

                //view设置点击事件
                bindClick(object, view);
            } catch (IllegalAccessException e) {
                e.printStackTrace();
            }
        }
    }

这样的话,我们是不是可以想一下,RUNTME 是在运行的时候在起作用,是不是多多少少会消耗一点性能?! 答案是肯定的。对于寸土寸金的移动端,是不能容忍的!

Android进阶9:手写Bufferknife(编译时注解)_第1张图片

那么能不能在编译的时候就把代码生成好,然后在运行的时候直接用呢? 答案还是肯定的!RetentionPolicy.CLASS华丽出场!
RetentionPolicy.CLASS作用于编译时期,那么也就是说作用域字节码上。然后配合注解处理器,生成我们想要的Java文件。

小提示:据说Bufferknife早期用的是运行时注解,但是我们都知道啦,用的是编译时注解。 = - =

好,说完了注解的存活时间,接下来聊聊Target;
这个意思顾名思义就是:“目标”,这个注解可以用在哪里? 哪里?

  • ElementType.ANNOTATION_TYPE 可以给一个注解进行注解
  • ElementType.CONSTRUCTOR 可以给构造方法进行注解
  • ElementType.FIELD 可以给属性进行注解
  • ElementType.LOCAL_VARIABLE 可以给局部变量进行注解
  • ElementType.METHOD 可以给方法进行注解
  • ElementType.PACKAGE 可以给一个包进行注解
  • ElementType.PARAMETER 可以给一个方法内的参数进行注解
  • ElementType.TYPE 可以给一个类型进行注解,比如类、接口、枚举

在Override注解定义的是:METHOD,也就是Override只能用于方法上,其它种类的使用,各位小伙伴可以自己试一试。

提问:我们要手写Bufferknife,那么我们的注解BindView和onClick应该怎么定义呢?
提示:都是编译时;一个是用在Field上,一个是用在Method上。

好,既然注解存活时间和目标我们已经选好了,那么怎么处理这些注解呢? 这是我们的注解处理器闪亮登场。

注解处理器
注解处理器是Javac的一个工具,会在编译期间扫描和处理注解。通过自定义注解处理器,可以自定义处理标注了类,方法,字段等注解。然后可以通过JavaPoet生成自定义的Java类。空口无凭,举个栗子?

/**
 * author: liumengqiang
 * Date : 2019/10/27
 * Description :
 */
public class TestProcessor extends AbstractProcessor {

    @Override
    public synchronized void init(ProcessingEnvironment processingEnvironment) {
        super.init(processingEnvironment);
        /**
         * ProcessingEnvironment提供很多有用的工具类Elements, Types 和 Filer,用来访问工具框架的环境
         */
    }

    @Override
    public boolean process(Set set, RoundEnvironment roundEnvironment) {
        /**
         * TODO 处理注解
         */
        return false;
    }

    @Override
    public Set getSupportedAnnotationTypes() {
        Set annotataions = new LinkedHashSet();
        annotataions.add(MyAnnotation.class.getCanonicalName());
        return annotataions;
    }

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

上面就是一个标准的自定义注解处理器,首先我们需要继承AbstractProcessor, 然后重写四个方法

  • init方法:会被注解处理工具调用,并输入ProcessingEnvironment参数。 ProcessingEnvironment提供很多有用的工具类Elements, Types 和 Filer。
  • process方法:这个方法相当于我们Java该方法中的main函数,在这里个方法里,我们可以扫描处理各种注解。处理当然也包括:生成新的Java类。该方法返回boolean类型,返回true: 代表我们这个注解已声名且不需要后去的注解处理器去处理它们;返回false:代表虽然后我们已声名并处理了这个注解,但是其它的注解处理器还可以处理它们。
  • getSupportedAnnotationTypes方法:这里必须指定,这个注解处理器是注册给哪个注解的。注意,它的返回值是一个字符串的集合,包含本处理器想要处理的注解类型的合法全称。
  • 指定使用的Java版本,通常这里返回SourceVersion.latestSupported(),默认返回SourceVersion.RELEASE_6

说到注解处理器,那么怎么注册注解处理器呢?
方案1

  • 运行注解处理器
  • 1、在 processors 库的 main 目录下新建 resources 资源文件夹;
  • 2、在 resources文件夹下建立 META-INF/services 目录文件夹;
  • 3、在 META-INF/services 目录文件夹下创建 javax.annotation.processing.Processor 文件;
  • 4、在 javax.annotation.processing.Processor 文件写入注解处理器的全称,包括包路径;

卧槽,怎么这么复杂? 有没有简单的方法?答案是有的!
方案2
直接使用Google提供的Auto-service注解,纳:

@AutoService(Processor.class)
public class TestProcessor extends AbstractProcessor{
		.......
}

就这就注册了,然后省去了很多步骤,因为这个注解会帮我们执行方案1的步骤。

好,说完了注解处理器和注册处理器,接下来说一下,在process方法中生成Java类。

JavaPoet
Github 传送门
实际上JavaPoet,也就是Java诗人,也就是通过写代码方式,然后自动根据规则生成Java类,这个网上太多博客了,并且人家写的太优秀了 = - =。

JavaPoet简介
JavaPoet的使用指南
我只说一点吧,Element元素,是一个接口,有五个子接口:

  • ExecutableElement:表示某个类或接口的方法、构造方法或初始化程序(静态或实例),包括注解类型元素 ;
  • PackageElement:表示一个包程序元素;
  • TypeElement:表示一个类或接口程序元素;
  • TypeParameterElement:表示一般类、接口、方法或构造方法元素的形式类型参数;
  • VariableElement:表示一个字段、enum 常量、方法或构造方法参数、局部变量或异常参数

举个??那就走一个!生成如下的方法:

/**
 * author: liumengqiang
 * Date : 2019/10/27
 * Description :
 */
public class MakeMainJava {
    public static void main(String[] args) {
        //第一步 生成main函数
        MethodSpec main = MethodSpec.methodBuilder("main")
                .addModifiers(Modifier.PUBLIC, Modifier.STATIC)
                .returns(void.class)
                .addParameter(String[].class, "args")
                .addStatement("$T.out.println($S)", System.class, "Hello, JavaPoet!")
                .build();
        //第二步 生成类
        TypeSpec helloWorld = TypeSpec.classBuilder("HelloWorld")
                .addModifiers(Modifier.PUBLIC, Modifier.FINAL)
                .addMethod(main)
                .build();
        //第三步 生成java文件对象
        JavaFile javaFile = JavaFile.builder("com.liumengqiang.helloworld", helloWorld).build();
        //第四步 输出到控制台
        try {
            javaFile.writeTo(System.out);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

然后打印在控制台:
Android进阶9:手写Bufferknife(编译时注解)_第2张图片
好了,基本知识点完了之后,叉腰歇会儿。。。

Android进阶9:手写Bufferknife(编译时注解)_第3张图片

开始撸代码

想一下,我们使用BufferKnife的时候,肯定用到了@BindView注解,首先我们需要定义@BindView注解,那么我们新建一个Javalibrary专门存放注解; 然后是不是还得有个注解处理器?我们在新建一个JavaLibrary专门用于存放注解处理器。

再看一下Bufferknife生成的Java代码是不是继承自一个接口,也就是说所有生成的Java代码都得实现该接口,也就是说规范生成的代码。所以我们也需要约束我们生成的Java类规范。 创建一个apiLibrary, 这是个Android的library,然后定义接口规范。

总体的项目结构如下:

Android进阶9:手写Bufferknife(编译时注解)_第4张图片

app : 测试module
annotations:存放注解
handleAnnotations:存放注解处理器
applibrary: 存放约束规范接口

他们的依赖关系:

Android进阶9:手写Bufferknife(编译时注解)_第5张图片

整体项目架构搭建好了,那么先定义注解:

/**
 * FileName:BindView
 * Create By:liumengqiang
 * Description:TODO
 */
@Retention(RetentionPolicy.CLASS)
@Target(ElementType.FIELD)
public @interface BindView {
    int value() default -1;
}

注意是:作用在字段上 & 编译时。注解定义好了 我们需要创建注解处理器。在书写注解处理器之前,我们先理一下思路: 一个类中可能有多个注解字段;每个类都是不相关的。基于这两点,我们得出结论:在注解处理器中,我们可以创建一个HashMap,然后key:类;value:注解字段。

/**
 * FileName:BindViewHandle
 * Create By:liumengqiang
 * Description:TODO
 */
@AutoService(Processor.class)
public class BindViewHandle extends AbstractProcessor {

    private Elements elementUtils; //
    private Filer filer; //
    //key:包含注解的类; value:注解的字段对象
    private HashMap annotationClassHashMap;

    @Override
    public synchronized void init(ProcessingEnvironment processingEnvironment) {
        super.init(processingEnvironment);
        elementUtils = processingEnvironment.getElementUtils();
        filer = processingEnvironment.getFiler();
        annotationClassHashMap = new HashMap<>();
    }

    @Override
    public boolean process(Set set, RoundEnvironment roundEnvironment) {

		//需要每次都清空map集合,因为process在扫描过程中可能进入多次,然后多次生成Java类
        annotationClassHashMap.clear();
		//创建AnnotationClass对象,该对象代表一个类。这个类对象塞入注解的字段信息
        for (Element element : roundEnvironment.getElementsAnnotatedWith(BindView.class)) {
            TypeElement typeElement = (TypeElement) element.getEnclosingElement();
            AnnotationClass annotationClass = getAnnotationClass(typeElement);
            BindViewField bindViewField = new BindViewField(elementUtils.getPackageOf(typeElement).getQualifiedName().toString(), element, element.getAnnotation(BindView.class).value());
            annotationClass.addField(bindViewField);
        }
		//再过滤onClick注解,塞入上面创建的AnnotationClass对象中
        for(Element element : roundEnvironment.getElementsAnnotatedWith(onClick.class)) {
            TypeElement typeElement = (TypeElement) element.getEnclosingElement();
            ExecutableElement executableElement = (ExecutableElement) element;
            AnnotationClass annotationClass = annotationClassHashMap.get(typeElement.getQualifiedName().toString());
            if(annotationClass != null) {
                annotationClass.setMethodAnnotation((ExecutableElement) element, executableElement.getAnnotation(onClick.class).value());
            } else {
                throw new IllegalArgumentException("需要设置BindView注释!");
            }
        }

		//根据创建的AnnotationClass生成Java类
        for (AnnotationClass annotationClass : annotationClassHashMap.values()) {
            try {
                annotationClass.generateJavaFile().writeTo(filer);
            } catch (IOException e) {
                e.printStackTrace();
            }
        }

        return false;
    }

    private AnnotationClass getAnnotationClass(TypeElement typeElement) {
        String qualifiedName = typeElement.getQualifiedName().toString();
        AnnotationClass annotationClass = annotationClassHashMap.get(qualifiedName);
        if (annotationClass == null) {
            annotationClass = new AnnotationClass(elementUtils.getPackageOf(typeElement).getQualifiedName().toString(), typeElement);
            annotationClassHashMap.put(qualifiedName, annotationClass);
        }
        return annotationClass;
    }

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

    @Override
    public Set getSupportedAnnotationTypes() {
        Set set = new LinkedHashSet();
        //扫描处理BindView和onClick注解
        set.add(BindView.class.getCanonicalName());
        set.add(onClick.class.getCanonicalName());
        return set;
    }
}

生成的最终Java类:


public class MainActivity$ViewBinder implements ViewBinder, OnClickListener {
  MainActivity host;

  @Override
  public void bindView(MainActivity host, Object o, ViewFinder viewFinder) {
    this.host = host;
    host.textView = (TextView)(viewFinder.findView(o, 2131165265));
    host.textView.setOnClickListener(this);
    host.button = (Button)(viewFinder.findView(o, 2131165264));
    host.button.setOnClickListener(this);
  }

  @Override
  public void unBindView(MainActivity host) {
    host.textView = null;
    host.button = null;
    host = null;
  }

  @Override
  public void onClick(View v) {
    switch (v.getId()) {
      case 2131165265:
      host.onClick(v);
      break;
      case 2131165264:
      host.onClick(v);
      break;
      default:
    }
  }
}

至于具体的代码demo我会附上地址。
现在注解处理器我们有了,接下来是不是该使用注解处理器?

把我们的app module 依赖 handleAnnotations:

annotationProcessor project(':handleAnnotations')

然后二者是关联了,在MainActivity中使用@BindView也能生成相应的Java代码。但是,但是,但是二者有关联么? 答案是肯定的,我们根本都没用到生成的MainActivity$ViewBinder类。怎么用呢? 我们需要用到反射!

首先类是有了,但是我们可以根据我们生成该 Java类的命名规则反射获得该类。

/**
 * author: liumengqiang
 * Date : 2019/10/26
 * Description :
 */
public class MyButterKnife {

    private static final  ActivityViewFinder activityViewFinder = new ActivityViewFinder();


    private static final FragmentViewFinder fragmentViewFinder = new FragmentViewFinder();

    public static final HashMap map = new HashMap<>();

    /**
     * Activity
     * @param activity
     */
    public static void init(Activity activity) {
        init(activity, activity, activityViewFinder);
    }

    /**
     * Fragment
     * @param fragment
     * @param view
     */
    public static void init(Fragment fragment, View view) {
        init(fragment, view, fragmentViewFinder);
    }

    public static void init(Object host, Object o, ViewFinder viewFinder) {
        String hostName = host.getClass().getName();
        ViewBinder hostViewBinder = map.get(hostName);
        if(hostViewBinder == null) {
            try {
            //通过反射获取到该class
                Class aClass = Class.forName(hostName + "$ViewBinder");
                然后创建该类对象
                hostViewBinder = (ViewBinder) aClass.newInstance();
            } catch (ClassNotFoundException e) {
                e.printStackTrace();
            } catch (IllegalAccessException e) {
                e.printStackTrace();
            } catch (InstantiationException e) {
                e.printStackTrace();
            }
            //将对象存入map中
            map.put(hostName, hostViewBinder);
        }
        //执行该类的bindView方法,完成绑定。
        hostViewBinder.bindView(host, o, viewFinder);
    }

    public static void unBind(Object host) {
        String hostName = host.getClass().getName();
        ViewBinder viewBinder = map.get(hostName);
        viewBinder.unBindView(host);
        map.remove(hostName);
    }
}

首先我们创建Map的构想是:希望第一次我们反射获取该类,但是之后不再通过反射,而是通过从map中获取,达到节约资源的目的。

上述代码的过程就是我们通过反射创建xxx$ViewBinder,然后根据IOC编程思想,直接调用接口的方法即可达到调用实现类的方法。

看下效果?行,看下效果。

Android进阶9:手写Bufferknife(编译时注解)_第6张图片

至此我们的BufferKnife就写完了,实际上这篇博客不是跟你讲解代码,而是说一下怎么写出Bufferknife,重点是思路,以及那些知识点构成了BufferKnife。

对了得感谢一下:感谢享学Zero大佬指导。

纯手打,如果有错,希望小伙伴指正。

附上GitHub Demo :Demo 传送门
附上GitHub Demo :Demo 传送门

你可能感兴趣的:(android开发,android进阶)