当前位置: 首页 > 工具软件 > ButterKnife > 使用案例 >

手写ButterKnife

濮阳唯
2023-12-01

一、原理简介
ButterKnife框架原理的是采用APT编译时技术,主要运用到注解+注解处理器的方式动态地为添加了BindView等注解的成员或方法生成类文件,开发者无需自己手写findViewById等等重复的代码,简化了开发者的工作量。

二、手写ButterKnife
想要完全理解ButterKnife底层的APT技术,手写实现ButterKnife可以帮助更好地吸收这种技术。

2.1准备工作
(1)创建Android工程,并且在此项目中新建一个java Module取名为annotataion,用于存放注解。
注意:必须新建java library而不能是Android lib,为什么只能新建java工程,不能是Android工程后面讲
了解了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就登场了:

@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

 类似资料: