注解框架Butterknife解析

1.什么是注解
2.注解的分类
3.编译时注解的原理
4.APT
5.创建项目及依赖
6.编码实现
7.总结

我们首先了解一下什么是注解以及注解的核心原理,在掌握原理的前提下自己动手实现一个注解框架。通过代码的编写能够对Butterknife底层实现有更加清楚的认识。

注解

注解在Java文档中定义如下:

An annotation is a form of metadata, that can be added to Java source code. Classes, methods, variables, parameters and packages may be annotated. Annotations have no direct effect on the operation of the code they annotate.

翻译一下,大概的意思是:

注解是一种元数据, 可以添加到java代码中. 类、方法、变量、参数、包都可以被注解,注解对注解的代码没有直接影响。

注解的分类

  • <b>运行时注解:</b>指的是运行阶段利用反射,动态获取被标记的方法、变量等,如EvenBus。
  • <b>编译时注解:</b>指的是程序在编译阶段会根据注解进行一些额外的处理,如ButterKnife。

运行时注解和编译时注解,都可以理解为通过注解标识,然后进行相应处理。两者的区别是:前者是运行时执行的,反射的使用会降低性能;后者是编译阶段执行的,通过生成辅助类实现效果。

运行时注解由于性能问题被一些人所诟病,所以本文主要讲解编译时注解的原理,并实现自己的Butterknife框架。

编译时注解的原理

编译时注解的核心原理依赖APT(Annotation Processing Tools)实现:

编译时Annotation解析的基本原理是,在某些代码元素上(如类型、函数、字段等)添加注解,在编译时javac编译器会检查AbstractProcessor的子类,并且调用该类型的process函数,然后将添加了注解的所有元素都传递到process函数中,使得开发人员可以在编译器进行相应的处理,例如,根据注解生成新的Java类,这也就是ButterKnife Dragger等开源库的基本原理

那么APT又是什么呢?

APT(Annotation Processing Tool)是一种处理注解的工具,它对源代码文件进行检测找出其中的Annotation,使用Annotation进行额外的处理。 Annotation处理器在处理Annotation时可以根据源文件中的Annotation生成额外的源文件和其它的文件(文件具体内容由Annotation处理器的编写者决定),APT还会编译生成的源文件和原来的源文件,将它们一起生成class文件。

下面以Butterknife为例:

public class MainActivity extends AppCompatActivity {

  @BindView(R.id.tv_main)
  TextView tvMain;

  @Override
  protected void onCreate(Bundle savedInstanceState) {
      super.onCreate(savedInstanceState);
      setContentView(R.layout.activity_main);
      ButterKnife.bind(this);
  }
}

这是Butterknife最简单的用法,我们只需要加上一个注解 @BindView并指定对应的Id就可以了,从而避免了findViewById(),那么它底层是怎么实现的呢?本篇文章重点不是介绍Butterknife的实现原理,所以这里只是简单的说一下它底层的实现。这里我们只写了一个MainActivity.java文件,编译后我们查看一下class文件,我们会发现在MainActivity中多了一个内部类ViewBinder,其实Butterknife就是在这个内部类中关联对应控件的,下面以伪代码的形式简单说明一个它底层实现的原理。

字节码文件
public class MainActivity extends AppCompatActivity {

  @BindView(R.id.tv_main)
  TextView tvMain;

  @Override
  protected void onCreate(Bundle savedInstanceState) {
      super.onCreate(savedInstanceState);
      setContentView(R.layout.activity_main);
      ButterKnife.bind(this);
  }

  /*
  * 当bind()方法被调用之后,tvMain就有对应的值了
  * */
  private static class ViewBinder{
       public static void bind(MainActivity activity){
           activity.tvMain = (TextView) activity.findViewById(R.id.tv_main);
       }
   }
}

大概的原理是这样的:在编译的时候如果某个类中使用了注解,Butterknife就会在其中“添加”一个内部类,在内部类中实现控件的关联。我们知道编译java源文件的工具是javac,其实在javac中有一个注解处理工具(依赖APT)用来编译时扫描和处理的注解的工具。我们可以为特定的注解,注册你自己的注解处理器,来实现自己的处理逻辑。

上面我们了解了基本原理,接下来我们实战演练


项目结构

我们的项目结构如上图所示:每个库都有自己的实现功能,最中通过我们的项目依赖相应的库来使用。

创建App

我们新建一个工程,因为我们要处理注解需要用到APT,所以在app中需要使用apt的插件
<b>github:</b>https://github.com/Aexyn/android-apt

关联APT插件:
Step1: 在我们工程目录下的build.gradle文件中添加如下代码:
Step2: 在我们项目目录下的build.gradle文件中添加如下代码:

创建Java库(定义注解)

inject-annotion

创建Android库

inject

<b>关联Java库(inject-annotion)</b>

关联java库

创建Java库(处理注解库)

我们需要在编译的时候根据注解创建新的类并添加到源文件中,所有需要引用几个依赖。并且要关联上一个Java库

  • com.google.auto.service:auto-service:谷歌提供的Java 生成源代码库
  • com.squareup:javapoet:提供了各种 API 让你用各种姿势去生成 Java 代码文件
  • com.google.auto:auto-common:生成代码的库
依赖

<b>全部创建完毕后,我们的工程目录如下:</b>

项目目录

关联库

让我们的项目(app)去关联注解库

关联库

编写代码

1.定义注解:inject-annotion

/**
 * @Retention(RetentionPolicy.CLASS):编译时被保留,在class文件中存在,但JVM将会忽略
 * @Target(ElementType.FIELD) :出现的位置(字段、枚举的常量)
 */
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.CLASS)
public @interface BindView {
    int value();
}

2.定义方法:inject

  • <b>InjectView.java</b>

    public class InjectView {
      public  static  void bind(Activity activity)
      {
          String clsName=activity.getClass().getName();
          try {
              //获取内部类
              Class<?> viewBidClass=Class.forName(clsName+"$$ViewBinder");
              //创建内部类的实例
              ViewBinder viewBinder= (ViewBinder) viewBidClass.newInstance();
              viewBinder.bind(activity);//绑定页面
          } catch (ClassNotFoundException e) {
              e.printStackTrace();
          } catch (InstantiationException e) {
              e.printStackTrace();
          } catch (IllegalAccessException e) {
              e.printStackTrace();
          }
      }
    }
    
  • <b>ViewBinder.java</b>

    public interface ViewBinder <T>{
      void  bind(T tartget);
    }
    

3.处理注解:inject-compiler

  • <b>FieldViewBinding.java</b>

    /**
     * 注解信息封装类
     */
    public class FieldViewBinding {
    
        private String name;// 字段的名字 textview
        private TypeMirror type ;// 字段的类型 --->TextView
        private int resId;// 对应的id R.id.textiew
    
        public FieldViewBinding(String name, TypeMirror type, int resId) {
            this.name = name;
            this.type = type;
            this.resId = resId;
        }
    
        public String getName() {
            return name;
        }
    
        public TypeMirror getType() {
            return type;
        }
    
        public int getResId() {
            return resId;
        }
    }
    
  • <b>BindViewProcessor.java</b>


    /**
     * 注解处理类
     */
    @AutoService(Processor.class)
    public class BindViewProcessor extends AbstractProcessor {
    
        private Elements elementUtils;
        private Types typeUtils;
        private Filer filer;
    
        @Override
        public synchronized void init(ProcessingEnvironment processingEnvironment) {
            super.init(processingEnvironment);
            elementUtils = processingEnvironment.getElementUtils();
            typeUtils = processingEnvironment.getTypeUtils();
            filer = processingEnvironment.getFiler();
        }
    
        /* 设置处理那些注解 */
        @Override
        public Set<String> getSupportedAnnotationTypes() {
            Set<String> types = new LinkedHashSet<>();
            types.add(BindView.class.getCanonicalName());
            return types;
        }
    
        /* 设置支持的JDk版本 */
        @Override
        public SourceVersion getSupportedSourceVersion() {
            return SourceVersion.latestSupported();
        }
    
        @Override
        public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
            Map<TypeElement, List<FieldViewBinding>> targetMap = new HashMap<>();
            for (Element element : roundEnvironment.getElementsAnnotatedWith(BindView.class)) {
    
                TypeElement enClosingElement = (TypeElement) element.getEnclosingElement();
                List<FieldViewBinding> list = targetMap.get(enClosingElement);
                if (list == null) {
                    list = new ArrayList<>();
                    targetMap.put(enClosingElement, list);
                }
                int id = element.getAnnotation(BindView.class).value();
                String fieldName = element.getSimpleName().toString();
                TypeMirror typeMirror = element.asType();
                FieldViewBinding fieldViewBinding = new FieldViewBinding(fieldName, typeMirror, id);
                list.add(fieldViewBinding);
            }
            for (Map.Entry<TypeElement, List<FieldViewBinding>> item : targetMap.entrySet()) {
                List<FieldViewBinding> list = item.getValue();
    
                if (list == null || list.size() == 0) {
                    continue;
                }
                TypeElement enClosingElement = item.getKey();
                String packageName = getPackageName(enClosingElement);
                String complite = getClassName(enClosingElement, packageName);
                ClassName className = ClassName.bestGuess(complite);
                ClassName viewBinder = ClassName.get("com.example.inject", "ViewBinder");
                TypeSpec.Builder result = TypeSpec.classBuilder(complite + "$$ViewBinder")
                        .addModifiers(Modifier.PUBLIC)
                        .addTypeVariable(TypeVariableName.get("T", className))
                        .addSuperinterface(ParameterizedTypeName.get(viewBinder, className));
    
                MethodSpec.Builder methodBuilder = MethodSpec.methodBuilder("bind")
                        .addModifiers(Modifier.PUBLIC)
                        .returns(TypeName.VOID)
                        .addAnnotation(Override.class)
                        .addParameter(className, "target", Modifier.FINAL);
                for (int i = 0; i < list.size(); i++) {
                    FieldViewBinding fieldViewBinding = list.get(i);
                    String pacckageNameString = fieldViewBinding.getType().toString();
                    ClassName viewClass = ClassName.bestGuess(pacckageNameString);
                    methodBuilder.addStatement
                            ("target.$L=($T)target.findViewById($L)", fieldViewBinding.getName()
                                    , viewClass, fieldViewBinding.getResId());
                }
                result.addMethod(methodBuilder.build());
    
                try {
                    JavaFile.builder(packageName, result.build())
                            .addFileComment("auto create make")
                            .build().writeTo(filer);
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            return false;
        }
    
        /* 获取类名 */
        private String getClassName(TypeElement enClosingElement, String packageName) {
            int packageLength = packageName.length() + 1;
            return enClosingElement.getQualifiedName().toString().substring(packageLength).replace(".", "$");
        }
    
        /* 获取包名 */
        private String getPackageName(TypeElement enClosingElement) {
            return elementUtils.getPackageOf(enClosingElement).getQualifiedName().toString();
        }
    }
    

测试

public class MainActivity extends AppCompatActivity {

    @BindView(R.id.text)
    TextView textview;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        InjectView.bind(this);
        if(textview == null){
            Toast.makeText(this,"注解处理失败",Toast.LENGTH_SHORT).show();
        }else{
            textview.setText("世界你好!");
        }
    }
}

用法跟Butterknife一样,页面上有一个TextView,使用注解关联,如果关联失败,弹出提示信息。否则设置显示为“世界你好!”。

演示

总结

通过上述代码的编写,我们能更加对Butterknife的底层实现有更清楚的认识,虽然只是实现了绑定View。在编译时javac编译器会检查AbstractProcessor的子类,并且调用该类型的process函数,然后将添加了注解的所有元素都传递到process函数中,我们需要继承该类重写此方法我们就能获取我们想要处理的注解。在里面做具体的绑定逻辑。
AutoService注解处理器是Google开发的,用来生成META-INF/services/javax.annotation.processing.Processor文件的。我们可以在注解处理器中使用注解。非常方便。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,033评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,725评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,473评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,846评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,848评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,691评论 1 282
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,053评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,700评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,856评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,676评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,787评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,430评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,034评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,990评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,218评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,174评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,526评论 2 343

推荐阅读更多精彩内容