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

注解(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;

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

//返回支持的注解类型
@Override
public Set<String> getSupportedAnnotationTypes() {
    return Collections.singleton(ViewInjector.class.getCanonicalName());
}
//类型与字段的关联表,用于在写入Java文件时按类型来写不同的文件和字段
final Map<String, List<VariableElement>> map = new HashMap<String, List<VariableElement>>();

//注解处理的核心方法
@Override
public 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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值