手写ButterKnife

了解了ButterKnife的原理后,今天我就带领大家手写一个简易的ButterKnife。因为ButterKnife使用到了编译时注解+反射,所以在手写项目之前,小伙伴们需要先熟悉下注解(Annotation)、反射和AbstractProcessor相关的知识。

话不多说,我们新建一个项目,命名为ButterknifeViewBind。这里先梳理下我们的逻辑,首先我们需要新建一个annotation库,专门用来存放我们的注解代码,其次还需要创建一个compiler库,用来放置编译的相关逻辑,最后我们还需要新建一个util工具库,提供注册的入口,供我们的业务使用。整个项目的目录结构如下:
工程结构

首先看下butterknife-annotations库,很简单,我们只声明了BindView这个注解类,相关代码如下:

package com.demo.butterknife_annotations;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

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

好了,注解有了,现在我们需要做的就是针对我们特定的注解进行处理,这个时候我们的ButterKnifeProcessor就登场了:

package com.demo.butterknife_compiler;

import com.demo.butterknife_annotations.BindView;
import com.google.auto.service.AutoService;
import com.squareup.javapoet.ClassName;
import com.squareup.javapoet.CodeBlock;
import com.squareup.javapoet.JavaFile;
import com.squareup.javapoet.MethodSpec;
import com.squareup.javapoet.TypeName;
import com.squareup.javapoet.TypeSpec;

import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.Filer;
import javax.annotation.processing.Messager;
import javax.annotation.processing.ProcessingEnvironment;
import javax.annotation.processing.Processor;
import javax.annotation.processing.RoundEnvironment;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.Element;
import javax.lang.model.element.ElementKind;
import javax.lang.model.element.Modifier;
import javax.lang.model.element.PackageElement;
import javax.lang.model.element.TypeElement;
import javax.lang.model.type.TypeMirror;
import javax.lang.model.util.Elements;
import javax.lang.model.util.Types;
import javax.tools.Diagnostic;


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

    private static final ClassName UI_THREAD =
            ClassName.get("androidx.annotation", "UiThread");
    Map<TypeElement, List<FieldBinding>> map = new HashMap<>();

    private Types mTypeUtils;
    private Elements mElementUtils;
    private Filer mFiler;
    private Messager mMessager;

    @Override
    public synchronized void init(ProcessingEnvironment env) {
        super.init(env);

        //初始化我们需要的基础工具
        mTypeUtils = env.getTypeUtils();
        mElementUtils = env.getElementUtils();
        mFiler = env.getFiler();
        mMessager = env.getMessager();
    }

    @Override
    public SourceVersion getSupportedSourceVersion() {
        //支持的Java版本
        return SourceVersion.latestSupported();
    }

    @Override
    public Set<String> getSupportedAnnotationTypes() {
        //支持的注解
        Set<String> annotations = new LinkedHashSet<>();
        annotations.add(BindView.class.getCanonicalName());
        return annotations;
    }

    @Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {

        //解析特定注解,生成Java文件
        for (Element annotatedElement : roundEnvironment.getElementsAnnotatedWith(BindView.class)) {
            if (annotatedElement.getKind() != ElementKind.FIELD) {
                error(annotatedElement, "Only field can be annotated with @%s",
                        BindView.class.getSimpleName());
                return true;
            }

            analysisAnnotated(annotatedElement);
        }

        for (Map.Entry<TypeElement, List<FieldBinding>> item :
                map.entrySet()) {

            TypeElement enclosingElement = item.getKey();
            String packageName = ((PackageElement) enclosingElement.getEnclosingElement()).getQualifiedName().toString();
            String className = enclosingElement.getSimpleName().toString();
            ClassName originClassName = ClassName.get(packageName, className);
            ClassName bindingClassName = ClassName.get(packageName, className + "_ViewBinding");

            //System.out.println("packageName -------------------" + packageName);
            //System.out.println("className -------------------" + className);
            //System.out.println("originClassName -------------------" + originClassName.toString());
            //System.out.println("bindingClassName -------------------" + bindingClassName.toString());

            //构造方法中拼接代码
            CodeBlock.Builder codeBuilder = CodeBlock.builder();
            for (FieldBinding fieldBinding : item.getValue()) {
                codeBuilder.addStatement("target.$L=($T)target.findViewById($L)", fieldBinding.getFieldName(),
                        fieldBinding.getType(), fieldBinding.getFieldId());
            }

            //创建构造方法
            MethodSpec.Builder constructor = MethodSpec.constructorBuilder()
                    .addModifiers(Modifier.PUBLIC)
                    .addAnnotation(UI_THREAD)
                    .addParameter(originClassName, "target")
                    .addCode(codeBuilder.build());

            //创建类
            TypeSpec typeSpec = TypeSpec.classBuilder(bindingClassName)
                    .addModifiers(Modifier.PUBLIC)
                    .addMethod(constructor.build())
                    .build();

            try {
                //生成java文件
                JavaFile.builder(packageName, typeSpec).build().writeTo(mFiler);
            } catch (IOException e) {
                e.printStackTrace();
            }


        }
        return false;
    }

    private void error(Element e, String msg, Object... args) {
        mMessager.printMessage(
                Diagnostic.Kind.ERROR,
                String.format(msg, args),
                e);
    }

    private void analysisAnnotated(Element element) {
        TypeElement activityElement = (TypeElement) element.getEnclosingElement();
        List<FieldBinding> activityBinding = map.get(activityElement);
        if (activityBinding == null) {
            activityBinding = new ArrayList<>();
            map.put(activityElement, activityBinding);
        }

        // Verify that the target type extends from View.
        TypeMirror elementType = element.asType();
        TypeName type = TypeName.get(elementType);
        int fieldId = element.getAnnotation(BindView.class).value();
        String fieldName = element.getSimpleName().toString();

        FieldBinding fieldBinding = new FieldBinding(fieldId, fieldName, type);
        activityBinding.add(fieldBinding);
    }
}

可以看到ButterKnifeProcessor使用了@AutoService注解,这个是用来做什么的呢?AutoService是Google开发的注解处理器,用来生成 META-INF/services/javax.annotation.processing.Processor 文件的,简化了我们创建注解处理器的流程。

AutoService的使用方式很简单,第一步,在我们butterknife_compiler库的buid.gradle文件中添加相关依赖:

implementation 'com.google.auto.service:auto-service:1.0-rc3'
annotationProcessor 'com.google.auto.service:auto-service:1.0-rc3'  

接着在我们创建的注解处理器上添加@AutoService(Processor.class)注解就OK了,有没有很简单哈哈。

我们接着看下ButterKnifeProcessor的process方法,该方法在项目中的作用就是解析@ BindView注解,生成“ _ViewBinding.java”文件。在生成“ _ViewBinding.java”文件的过程中,使用到了JavaPoet开源库,what? 这又是什么?其实JavaPoet 就是用来辅助我们生成 Java 代码的一个 Java Library。同样我们需要在buid.gradle文件中添加依赖:

implementation 'com.squareup:javapoet:1.8.0'

对JavaPoet 的API操作不太了解的童鞋可以查下相关博客,笔者这里也是一边查博客一边写的,一把辛酸泪啊~

好了到现在为止ButterKnifeProcessor也被我们搞定了,接下来就只剩下util工具库,提供注册的入口,这个就easy多了,只涉及到反射操作,我们首先新建butterknifeinject library,接着创建ButterknifeInject类,该类只有一个bind方法,代码如下:

package com.demo.butterknifeinject;

import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;

public final class ButterknifeInject {

    public static void bind(Object target) {

        try {
            String clsName = target.getClass().getName();
            Class<?> bindingClass = Class.forName(clsName + "_ViewBinding");
            try {
                Constructor constructor = bindingClass.getConstructor(target.getClass());
                try {
                    System.out.println("constructor-----------newInstance- before");
                    constructor.newInstance(target);
                } catch (InstantiationException e) {
                    e.printStackTrace();
                } catch (IllegalAccessException e) {
                    e.printStackTrace();
                } catch (InvocationTargetException e) {
                    e.printStackTrace();
                }
            } catch (NoSuchMethodException e) {
                e.printStackTrace();
            }
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
    }
}

大功告成,最后我们测试一下。在app的build.gradle文件中添加我们的library依赖:

annotationProcessor project(':butterknife-compiler')
implementation project(':butterknife-annotations')
implementation project(':butterknifeInject')

ok,在MainActivity中我们现在就可以这么写啦:

public class MainActivity extends AppCompatActivity {

    @BindView(R.id.tv_test)
    TextView mTvTest;

    @BindView(R.id.btn_test)
    Button mBtnTest;

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

        ButterknifeInject.bind(this);

        initData();
    }

    private void initData(){
        mBtnTest.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {

                mTvTest.setText("哈哈哈哈哈嗝");
            }
        });
    }
}

最后我们run一把,一切正常,不出意外的话我们的“ _ViewBinding.java”文件已经生成了,我们看下:
1577184824451.png

至此为止,一个简易的ButterKnife就生成了。

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