注解(Annotation)自定义注解(四)--编译期注解解析讲解和手写ButterKnife

来源:互联网 发布:excel数据分类 编辑:程序博客网 时间:2024/05/15 10:38

注解(Annotation)自定义注解(四)–编译期注解解析讲解和手写ButterKnife

前言

前面两篇讲解了运行期注解的使用和xutils源码的解析,以及手动打造自己的IOC框架。但是运行期注解由于性能问题被一些人所诟病,所以这里我们讲下编译器注解的使用和实战。

介绍

编译器注解的核心原理依赖APT(Annotation Processing Tolls)实现,例如,我们常用的ButterKnife,Dagger,Retrofit等开源库都是基于APT的。那么编译器注解是如何工作的呢?这就是我们要讨论的内容。

编译时Annotation注解的基本原理是,在某些代码元素上(比如类型,函数,字段)添加注解,然后在编译时编译器会检查AbstractProcessor的子类,并且调用该类型的process函数,然后将添加了注解的元素传递给process中,使得开发者可以在编译期可以进行处理,比如,根据注解生成新的ava类,这也是BufferKnife等库的基本原理。

这里写图片描述

编译处理时,是分开进行的。如果在某个处理中产生的新的JAVA文件,那么就需要另外一个处理来处理新生成的源文件,反复进行,知道没有新的文件产生。在处理完文件后,再进行编译。JDK5提供了APT工具来对注解进行处理。编写注解处理器和核心是:AnnotationProcessorFactory和AnnotationProcessor两个接口,前者是为某些注解类型创建注解处理器工厂,后者是表示注解处理器。

对于编译器,代码中元素的结构是不会变的。JDK中为这些元素定义了一个基本类型Element类,它有几个子类:

PackageElement--包元素,包含包的信息TypeElement-------类型元素,描述字段信息ExecutableElement--可执行元素,代表函数类型的元素VariableElement------变量元素TypeParameterElement--类型参数元素

使用上面的抽象来对于代码中的基本元素。

我们先来分析BuffKnife使用以及源码,然后在手动打造我们自己的框架。
BuffKnife使用

BuffKnife项目

public class MainActivity extends AppCompatActivity {    @Bind(R.id.icon)    ImageView icon;    @Override    protected void onCreate(Bundle savedInstanceState) {        super.onCreate(savedInstanceState);        setContentView(R.layout.activity_main);        ButterKnife.bind(this);    }    @OnClick(R.id.icon)    public void onClick() {        Toast.makeText(this, "图片点击了", Toast.LENGTH_LONG).show();    }}

使用非常简单,主要注意:属性是不能private就行。

butterknife的源码解析

按照逻辑,我们应该先看ButterKnife.bind(this),下面是源码:

static void bind(Object target, Object source, Finder finder) {    Class<?> targetClass = target.getClass();    try {      if (debug) Log.d(TAG, "Looking up view binder for " + targetClass.getName());      // 找到viewBinder      ViewBinder<Object> viewBinder = findViewBinderForClass(targetClass);      if (viewBinder != null) {        // 直接执行方法        viewBinder.bind(finder, target, source);      }    } catch (Exception e) {      throw new RuntimeException("Unable to bind views for " +           targetClass.getName(), e);    }}

如果从这里看我们好像看不到任何的东西,其实工作流程是怎样的呢?从上面介绍我们知道,编译器会先调用AbstractProcessor的子类的process函数,我们可以看一下Bind这个Annotation注解类和ButterKnifeProcessor这两个类:

ButterKnife 工作流程

@Retention(CLASS) @Target(FIELD)public @interface Bind {  /** View ID to which the field will be bound. */  int[] value();}

当你编译你的Android工程时,ButterKnife工程中ButterKnifeProcessor类的process()方法会执行以下操作:

1.开始它会扫描Java代码中所有的ButterKnife注解@Bind、@OnClick、@OnItemClicked等。
2.当它发现一个类中含有任何一个注解时,ButterKnifeProcessor会帮你生成一个Java类,名字类似$$ViewBinder,这个新生成的类实现了ViewBinder接口。
3.这个ViewBinder类中包含了所有对应的代码,比如@Bind注解对应findViewById(), @OnClick对应了view.setOnClickListener()等等。
4.最后当Activity启动ButterKnife.bind(this)执行时,ButterKnife会去加载对应的ViewBinder类调用它们的bind()方法。


现在我们总该明白为什么我们的生成的属性和方法不能私有了吧?我们最后看一下编译时生成的class类吧

public class MainActivity$$ViewBinder<T extends MainActivity> implements ViewBinder<T> {  @Override public void bind(final Finder finder, final T target, Object source) {    View view;    view = finder.findRequiredView(source, 2131427372, "field 'icon' and method 'onClick'");    target.icon = finder.castView(view, 2131427372, "field 'icon'");    view.setOnClickListener(      new butterknife.internal.DebouncingOnClickListener() {        @Override public void doClick(View p0) {           target.onClick();        }      });  }  @Override public void unbind(T target) {    target.icon = null;  }}

这里没有详细分析ButterKnifeProcessor类,有兴趣的同学可以自己看下。


手写BuffKnife

必备知识点:

- 基本的注解知识
- 反射
- 编译期注释处理API
- JavaPoet(用来生成java源文件,不是必须,可自己手动拼接)
- Auto (自动配置META-INF,不是必须,可以手动配置)

上面后面两个工具不是必须的,如果感兴趣,可以自己手动拼接java源文件和手动配置Auto。

这里写图片描述

基本的配置:

这边要注意几点:

aptlib和annotationlib是java库,不用android库是因为androidSDK中没有javax.annotation.*android程序调用java库时,需要在java库中引入com.google.android:android:XXXX和com.android.support:support-annotations:XXXXjar包

想使用APT,项目要应用APT插件:

dependencies {    classpath 'com.android.tools.build:gradle:2.3.1'    classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8'}apply plugin: 'com.android.application'apply plugin: 'com.neenbedankt.android-apt'

然后引入javajar包:

apt  project(':aptlib')compile project(':annotationlib')

知道上面的知识,就可以开始写我们的代码了。

让我们先来看看注解类吧

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

//自动配置META-INF

@AutoService(Processor.class)
public class ViewInjectProcessor extends AbstractProcessor {

//在元素上进行操作的某些实用工具方法private Elements elementUtils;

//用来创建新源、类或辅助文件的 Filer
private Filer filer;
//用来在类型上进行操作的某些实用工具
private Types typeUtils;

@Overridepublic synchronized void init(ProcessingEnvironment processingEnv) {    super.init(processingEnv);    elementUtils = processingEnv.getElementUtils();    filer = processingEnv.getFiler();    typeUtils = processingEnv.getTypeUtils();}//返回支持的注解类型@Overridepublic Set<String> getSupportedAnnotationTypes() {    return Collections.singleton(ViewInjector.class.getCanonicalName());}//类型与字段的关联表,用于在写入Java文件时按类型来写不同的文件和字段final Map<String, List<VariableElement>> map = new HashMap<String, List<VariableElement>>();//注解处理的核心方法@Overridepublic boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {    //获取使用ViewInjector注解的所有元素     Set<? extends Element> elementSet = roundEnv.getElementsAnnotatedWith(ViewInjector.class);    for (Element element:elementSet) {        checkAnnotationValid(element,ViewInjector.class);        //注解的字段        VariableElement varElement= (VariableElement)element;        //类型的完整路径名,比如某个Activity的完整路径       String className = getParentClassName(varElement);        //获取这个类型的所有注解,例如某个Activity中的所有View的注解对象         List<VariableElement> cacheElements = map.get(className);        if(cacheElements==null){            cacheElements = new LinkedList<VariableElement>();        }        //将元素添加到该类型对应的字段列表中        cacheElements.add(varElement);        //以类的路径为key,字段列表为value,存入map.这里是将所在字段按所属的类型进行分       map.put(className,cacheElements);    }    //生成java源文件    generate();    return false;} //生成java源文件private void generate() {          Iterator<Map.Entry<String, List<VariableElement>>> iterator = map.entrySet().iterator();          while (iterator.hasNext()){             Map.Entry<String, List<VariableElement>> entry = iterator.next();               List<VariableElement> cacheElements = entry.getValue();               if (cacheElements == null || cacheElements.size() == 0) {                      continue;                     }             InjectorInfo info = InjectorInfoUtil.createInjectorInfo(processingEnv,cacheElements.get(0));          //下面全是JavaPoet的基本使用,不明白的可以点击上面的链接去看           final ClassName className = ClassName.get(info.packageName,info.classlName);           final ClassName InterfaceName=ClassName.get(InjectAdapter.class);             MethodSpec.Builder injectsBuilder = MethodSpec.methodBuilder("inject")                .addModifiers(Modifier.PUBLIC)                .addAnnotation(Override.class)                .returns(void.class)                .addParameter(className, "target");            for (VariableElement element:cacheElements) {                InjectorInfo temInfo = InjectorInfoUtil.createInjectorInfo(processingEnv,element);                ViewInjector annotation = element.getAnnotation(ViewInjector.class);                int value = annotation.value();                String fieldName = element.getSimpleName().toString();                String type = element.asType().toString();                injectsBuilder.addStatement("target." + fieldName + " = ("+type+")(target).findViewById(" + value + ")");             }            MethodSpec injects = injectsBuilder.build();            TypeSpec typeSpec = TypeSpec.classBuilder(info.newClassName)                    .addSuperinterface(ParameterizedTypeName.get(InterfaceName, className))                    .addModifiers(Modifier.PUBLIC)                    .addMethod(injects)                    .build();           JavaFile javaFile = JavaFile.builder(info.packageName, typeSpec).build();           try {                   javaFile.writeTo(processingEnv.getFiler());               } catch (IOException e) {                     e.printStackTrace(); }                }        } /*打印错误的方法 */ protected void error(Element element, String message, Object... args) {            if (args.length > 0) { message = String.format(message, args);              }           processingEnv.getMessager().printMessage(ERROR, message, element); }//类型的完整路径名,比如某个Activity的完整路径 private String getParentClassName(VariableElement varElement) {               TypeElement typeElement = (TypeElement) varElement.getEnclosingElement();               String packageName = AnnotationUtil.getPackageName(processingEnv,typeElement);             return packageName + "." + typeElement.getSimpleName().toString();   }

}

测试:

public class MainActivity extends AppCompatActivity {    @ViewInjector(R.id.tv)    TextView tv;    @Override    protected void onCreate(Bundle savedInstanceState) {        super.onCreate(savedInstanceState);        setContentView(R.layout.activity_main);        ViewInjectUtil.injectView(this);        tv.setText("fddfdfdfdfdfdfdfdfdfdf");    }}

重新编译一下,到当前目录app\build\generated\source\apt\debug\com\github\essayjoke\可以看到MainActivity$InjectAdapter.java文件,我们打开看一下:

package github.com.stoneviewinject;import com.example.InjectAdapter;import java.lang.Override;public class MainActivity$$InjectAdapter implements InjectAdapter<MainActivity> {  @Override  public void inject(MainActivity target) {    target.tv = (android.widget.TextView)(target).findViewById(2131492945);  }}

我们再来看下是否真的可以使用:

这里写图片描述

看到这里我就放心了,哈哈。

下面很简单了,有兴趣,可以自己根据BuffKnife的bind来实现。

注解的知识已经讲完了,自己也学到很多东西,希望一直能够这样坚持下去。

下面给出下载地址:StoneViewInject

原创粉丝点击