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

本文同步发布于搁置了一年的个人博客http://mrrobot97.me

​ 相信绝大部分的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

​ 新建一个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;   //不能为private

    @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下新建如下的目录结构:

image

并新建名为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) {
        //为所有标注了NeedBind标注的类生成相应代理class
        for(Element element:roundEnvironment.getElementsAnnotatedWith(NeedBind.class)){
            generateBinderClass((TypeElement) element); //后面实现
        }
        //return true 表示该processor处理的注解是否只由该processor处理
        return true;
    }

    @Override
    public Set<String> getSupportedAnnotationTypes() {
      //表示该注解处理器需要处理带有这些注解的类、接口
        return Collections.singleton(NeedBind.class.getCanonicalName());
    }
}

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

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

// This file is generated by Binder, do not edit!
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>类型的方法参数:

//拿到Activity的类
ClassName activityClassName=ClassName.get(element);     
//构造activity类型的参数
ParameterSpec activityParam=ParameterSpec.builder(activityClassName,"activity")
                .addModifiers(Modifier.FINAL)
                .build();

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

    /**
     * 返回所有标注了clazz类型注解的成员变量
     * @param typeElement
     * @param clazz
     * @return
     */
    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());
            //注解的值,也就是view要绑定的id
            int viewId=variableElement.getAnnotation(BindView.class).value();
            bindViewCodeBlockBuilder.addStatement("activity.$L=($T)activity.findViewById($L)",variableName,viewType,viewId);
        }

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

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

构造函数:

        //构造函数,内部调用bindView方法
        MethodSpec constructorMethod=MethodSpec.constructorBuilder()
                .addModifiers(Modifier.PUBLIC)
                .addParameter(activityParam)
                .addStatement("$N($L)",bindViewMethod,activityParam.name)
                .build();

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

        //生成BinderDelegate类
        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的了。获取包名用如下方法:

    /**
     * 查找包名
     * @param element
     * @return
     */
    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类:

image

到这里,已经距离成功很接近了,我们还需要做的就是在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)即可:

@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);

        MyButterKnife.bind(this);

        mTv.setText("This is not hello world");
    }
}

​ 编译运行,没有NullPointerException,而且mTv的内容也是我们设置的内容:


image

​ 至此,我们实现Demo版本ButterKnife的目的已经基本实现了!

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

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

​ 另附demo源码地址

​ 以上。

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

推荐阅读更多精彩内容

  • 什么是注解注解分类注解作用分类 元注解 Java内置注解 自定义注解自定义注解实现及使用编译时注解注解处理器注解处...
    Mr槑阅读 1,029评论 0 3
  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 170,569评论 25 707
  • 上一篇我们讲解了ButterKnife的设计思想,理解了ButterKnife绑定相关源码的实现逻辑。但是它是怎么...
    Ihesong阅读 976评论 0 2
  • 俗话说的好“不想偷懒的程序员,不是好程序员”,我们在日常开发android的过程中,在前端activity或者fr...
    蛋西阅读 4,936评论 0 14
  • 1.需吞服的药物应用温开水送服,忌用茶叶水、酒水、牛奶服药。(因为会发生化学反应) 2.按规定时间服药。抗生素及磺...
    夏药阅读 338评论 0 0