专栏首页刘望舒自定义注解和解析器实现ButterKnife

自定义注解和解析器实现ButterKnife

相信绝大部分的Android开发者都曾使用过ButterKnife, 利用ButterKnife开发者可以快速的实现实体view与xml的绑定,此外还能绑定各种资源、动画、字符串甚至是点击事件等。ButterKnife内部的原理就是通过自定义注解+自定义注解解析器来动态生成代码并为我们的view绑定id的。本文通过实现一个demo性质的ButterKnife项目来展示如何自定义注解+注解解析器。

关于注解本身本文不多做介绍,这里给出一篇讲解注解的文章一小时搞明白自定义注解(Annotation),对注解还比较陌生的读者可以先看一下注解的知识。

新建一个Android Studio Project,名字就叫MyButterKnife好了。MainActivity、layout都直接使用自动生成的,在activity_main.xml中给TextView添加一个id。

接下来新建一个module用于实现我们的自定义注解以及自定义注解解析器,注意这个module必须是java library,因为在java library中我们才可以继承解析器AbstractProcessor,android library是无法访问的。

新建一个java library取名为processor.

然后自定义注解(Annotation),我们只是做一个demo性质的实验,因此只实现View与id的绑定功能。这里我定义了两个注解NeedBindBindView:

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.CLASS)
public @interface NeedBind {
 
}
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.CLASS)
public @interface BindView {
   int value() default -1;
}

NeedBind的Target是TYPE说明这是一个用于修饰类和接口的注解,这里NeedBind的作用是帮助我们快速筛选出需要处理自定义注解的类。BindView的Target是FIELD也就是成员变量,即需要绑定资源id的view成员。

这两个注解的Retention都是CLASS级别,表示注解会被编译保留到.class文件但是运行时(RUNTIME)不保留,因此不影响代码运行时的性能。有一个小技巧就是将注解的变量取名为value(只有一个变量时)可以在声明注解变量时省略变量名,即可以这样使用:

@BindView(R.id.my_tv)
TextView mTV;

如果我们取名为别的比如id,那么注解必须向下面这样使用:

@BindView(id = R.id.my_tv)
TextView mTv;

注解定义好后就可以在项目里使用了:

@NeedBind
public class MainActivity extends AppCompatActivity {

   @BindView(R.id.my_tv)
   TextView mTv;   

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

注意这里我加了两个注解:用于修饰MainActivity的NeedBind和用于修饰mTv的BindView。另外很重要的一点就是mTv变量不能用private修饰,因为我们是通过在生成的代理类中调用MainActivity.view=(View)MainActivity.findViewById()来实现为view绑定id的,所以mTv至少需要是package可见级别的。现在还没有解析我们自定义的注解,因此现在加的注解是没有任何作用的,那么接下来就开始实现我们的注解解析器吧。

还是在processor module下,新建类MyButterKnifeProcessor,继承自AbstractProcessor.这个就是用于解析自定义注解的解析器了。不过要想让它生效还必须在processor下新建如下的目录结构:

并新建名为javax.annotation.processing.Processor的文本文件,内容就一行:

me.mrrobot97.lib.MyButterKnifeProcessor

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

compile project(path: ':processor')
annotationProcessor project(path: ':processor')

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

后面的工作都是在MyButterKnifeProcessor类里实现了。我们的目的是通过读取类中的自定义注解,生成相应的绑定视图的代码,这就需要一个生成java代码的库javapoet, squre出品,质量绝对上乘。在processor的build.gradle里加入如下一行:

compile 'com.squareup:javapoet:1.9.0'

ps:这么实用的开源项目在github上居然才4500start,还没有最近火的微信跳一跳小游戏辅助脚本的star多,我也是醉了。可见github的star还是很水的,看看就好,千万别用star数目判断一个项目是否牛逼……

MyButterKnifeProcessor里需要重写方法process()和方法getSupportedAnnotationTypes():

public class MyButterKnifeProcessor extends AbstractProcessor{
   @Override
   public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) { 
       for(Element element:roundEnvironment.getElementsAnnotatedWith(NeedBind.class)){
           generateBinderClass((TypeElement) element); 
       }
       return true;
   }

   @Override
   public Set<String> getSupportedAnnotationTypes() {
       return Collections.singleton(NeedBind.class.getCanonicalName());
   }
}

然后就到了本文的关键:处理注解并生成辅助类。强烈建议读者先阅读javapoet的简单使用, 不然可能难以读懂接下来的代码。

先展示一下最终生成代码的效果,这是准备本文时练习的一个demo:

package guru.mrrobot97.customannotationprocessor;
import android.view.View;
import android.widget.TextView;

public class MainActivityDeleagteBinder {
 public MainActivityDeleagteBinder(final MainActivity activity) {
   bindView(activity);
   bindClick(activity);
 }

 private void bindView(final MainActivity activity) {
   activity.mTv=(TextView)activity.findViewById(2131165301);
   activity.mTv2=(TextView)activity.findViewById(2131165302);
 }

 private void bindClick(final MainActivity activity) {
   activity.findViewById(2131165301).setOnClickListener(new View.OnClickListener() {
                   @Override
                   public void onClick(View v) {
                         activity.sayHello();                
                   }
               });
 }
}

上面所有的内容都是javapoet生成的,下面就按照上面这个最终效果来一步一步分析要怎么生成我们的代理类。简单起见,就不生成bindClick相关代码了,毕竟我们也没定义相关注解。

我们要为所有标注了NeedBind注解的类生成名为*DeleagteBinder的类,同样为了简单起见我们只做了Activity中view的绑定。DeleagteBinder类要包含一个构造函数、一个bindView方法, bingView方法里要为Activity中绑定了BindView注解的view绑定id,此外构造函数和bindVIew方法还都有一个<? extends Activity>类型的参数。

我们从小到大一个一个生成,首先来构造我们的<? extends Activity>类型的方法参数:

ClassName activityClassName=ClassName.get(element);     
ParameterSpec activityParam=ParameterSpec.builder(activityClassName,"activity")
               .addModifiers(Modifier.FINAL)
               .build();

然后加入一个如下的方法,用于查找类中所有标注了某种注解的成员变量(VariableElement):

private List<VariableElement> getFieldElementsWithAnnotation(TypeElement typeElement,
Class clazz){
       List<VariableElement> elements=new ArrayList<>();
       for(Element element:typeElement.getEnclosedElements()){
           if(element.getAnnotation(clazz)!=null){        
               elements.add((VariableElement) element);
           }
       }
       return elements;
   }

然后是生成bindView方法内的方法体,就是真正实现view=activity.findViewById的java语句:

List<VariableElement> bindViewFieldList=getFieldElementsWithAnnotation(element,BindView.class);
       CodeBlock.Builder bindViewCodeBlockBuilder=CodeBlock.builder();
       for(VariableElement variableElement:bindViewFieldList){      
           String variableName=variableElement.getSimpleName().toString();
           TypeName viewType=ClassName.bestGuess(variableElement.asType().toString());
           
           int viewId=variableElement.getAnnotation(BindView.class).value();
           bindViewCodeBlockBuilder.addStatement
          ("activity.$L=($T)activity.findViewById($L)",variableName,viewType,viewId);
       }

有了bindView()的方法体,参数,该构造bindView()方法了:

MethodSpec bindViewMethod=MethodSpec.methodBuilder("bindView")
               .addModifiers(Modifier.PUBLIC)
               .addParameter(activityParam)
               .addCode(bindViewCodeBlockBuilder.build())
               .returns(void.class)
               .build();

构造函数:

MethodSpec constructorMethod=MethodSpec.constructorBuilder()
               .addModifiers(Modifier.PUBLIC)
               .addParameter(activityParam)
               .addStatement("$N($L)",bindViewMethod,activityParam.name)
               .build();

然后是生成*DelegateBinder这个类文件:

String binderClassName=element.getSimpleName().toString();
       TypeSpec delegateType=TypeSpec.classBuilder(binderClassName+"DelegateBinder")
               .addModifiers(Modifier.PUBLIC)
               .addMethod(bindViewMethod)
               .addMethod(constructorMethod)
               .build();

       JavaFile javaFile=JavaFile.builder(getPackage(element).getQualifiedName()
      .toString(),delegateType)
               .addFileComment("This file is generated by Binder, do not edit!")
               .build();
       try {
           javaFile.writeTo(processingEnv.getFiler());
       } catch (IOException e) {
           e.printStackTrace();
       }

注意这里的包名,生成的类的包名尽量与需要绑定的Activity所在的包名一致,这样BindView修饰的成员变量只需是包内可见就行,否则的话就必须是public的了。获取包名用如下方法:

public static PackageElement getPackage(Element element) {
       while (element.getKind() != PACKAGE) {
           element = element.getEnclosingElement();
       }
       return (PackageElement) element;
   }

写完上面所有这些,Make Project,你会发现app下的build/generated/source/apt/debug目录下生成了MainActivityDelegateBinder类:

到这里,已经距离成功很接近了,我们还需要做的就是在MainActivity的setContentView()调用之后,new出我们的MainActivityDelegateBinder类,即完成了MainActivity中带BindView标注的成员变量的id绑定。为了new一个MainActivityDelegateBinder,我们在app module中新建一个帮助类MyButterKnife:

public class MyButterKnife {
   public static final String ACTIVITY_DELEGATE_SUFFIX = "DelegateBinder";
   public static void bind(Activity activity){
       String activityName=activity.getClass().getName();
       String delegateName=activityName+ ACTIVITY_DELEGATE_SUFFIX;
       try {
           Class delegateClass=activity.getClass().getClassLoader()
          .loadClass(delegateName);
           Constructor constructor=delegateClass.getConstructor(activity.getClass());
           constructor.newInstance(activity);
       } catch (InstantiationException e) {
           e.printStackTrace();
       } catch (InvocationTargetException e) {
           e.printStackTrace();
       } catch (NoSuchMethodException e) {
           e.printStackTrace();
       } catch (IllegalAccessException e) {
           e.printStackTrace();
       } catch (ClassNotFoundException e) {
           e.printStackTrace();
       }
   }
}

在MyButterKnife里稍微利用了一点反射new了MainActivityDelegateBinder实体,然后MainActivityDelegateBinder的构造函数调用了bindView()最终实现了MainActivity中view的绑定。

最后在MainActivity中调用MyButterKnife.bind(this)即可:

@NeedBindpublic class MainActivity extends AppCompatActivity {
   @BindView(R.id.my_tv)
   TextView mTv;
   @Override
   protected void onCreate(Bundle savedInstanceState) {
       super.onCreate(savedInstanceState);
       setContentView(R.layout.activity_main);
       MyButterKnife.bind(this);
       mTv.setText("This is not hello world");
   }
}

编译运行,没有NullPointerException,而且mTv的内容也是我们设置的内容。至此,我们实现Demo版本ButterKnife的目的已经基本实现了!

ps:如果你在你的自定义Processor中用到Modifier的地方Android Studio报红时,请无视,这是Android Studio自身的bug,不影响编译.

再次强调,本文的目的是给读者对AnnotationProcessor一个入门的使用概念,最终实现的Demo也是一个十分拙劣的版本,只能说可以跑通,代码里没有做任何合法性、类型匹配、访问权限等相关的安全性检查,这在生产环境中是完全不可用的。真正的ButterKnife在这些可能发生异常的方面做了大量安全性检查。

github地址:https://github.com/mrrobot97/MyButterKnife

作者 | mrrobot97

地址 | https://www.jianshu.com/p/8c2173e0ea87

声明 | 本文是 mrrobot97 原创,已获授权发布,未经原作者允许请勿转载

本文分享自微信公众号 - 刘望舒(liuwangshuAndroid)

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2018-01-30

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • 【Go 语言社区】js 向服务器请求数据的五种技术

    Ajax,在它最基本的层面,是一种与服务器通讯而不重载当前页面的方法,数据可从服务器获得或发送给服务器。有多种不同的方法构造这种通讯通道,每种方法都有自己的优势...

    李海彬
  • Java面试系列21-xml

    xml方面面试题 1.xml有哪些解析技术?区别是什么? 有DOM,SAX,STAX等 DOM:处理大型文件时其性能下降的非常厉害。这个问题是由DOM的树...

    奋斗蒙
  • JSP简单入门(2)

    六、行为元素(JSP标签,简述) JSP提供了一种称之为Action的元素,在JSP页面中使用Action元素可以完成各种通用的JSP页面功能。Action元素...

    奋斗蒙
  • json解析-开发必会

    json解析 什么是JSON: JSON即JavaScript Object Natation, 它是一种轻量级的数据交换格式, 与XML一样, 是广泛被采用的...

    奋斗蒙
  • Java操作数据库Spring(1)

    首先是核心配置文件daoContext.xml <?xml version="1.0" encoding="UTF-8"?> <beans xmlns="h...

    奋斗蒙
  • 网页中显示xml,直接显示xml格式的文件

    第一种方法 使用<pre></pre>包围代码(在浏览器中测试不行啊,但是在富编辑器中又可以,怪); 使用<xmp></xmp>包围代码(官方不推荐,但是效果不...

    cloudskyme
  • Java操作数据库Spring(2)

    pom.xml <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www...

    奋斗蒙
  • 如何让spring自动扫描jar包中的类?

    在applicationContext.xml配置了 1 <context:component-scan base-package="com.demo"...

    cloudskyme
  • java读取xml文件

    xml文件:   Xml代码   <?xml version=”1.0” encoding=”GB2312”?>   <RESULT>   <VALUE>...

    奋斗蒙
  • android使用Activity

    第一个例子,显示网址 首先创建工程 ? 按照提示填入 我使用的是2.3版本,所以Min SDK Version填10 修改/res/layout/下main....

    cloudskyme

扫码关注云+社区

领取腾讯云代金券