ButterKnife的最简单实现:SimpleButterKnife

ButterKnife的光辉历史

ButterKnife是JakeWharton大神开源的一个库,主要作用是把Android View和回调绑定到属性和方法上面,在ViewBinding出现之前是一个很好的库,在性能和易用性方面是一个毋庸置疑的库,应用范围比较广泛。虽然现在已经废弃,但是其中的技术和理念对我们的软件研发人员还是有借鉴意义。很多方面都是相通的,大道至简同样也是这个道理。

最简单实现View绑定的方法

findViewById是查找View最初的方法,可以使用运行时注解在Activity的onCreate方法使用反射来实现Android View和属性的绑定。运行时注解的定义如下:

java 复制代码
package com.example.simplebutterknife;

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

@Retention(RetentionPolicy.RUNTIME) //注解保留范围,SOURCE代表代码级别(Annotation Processor可以使用),CLASS代表编译器级别 , RUNTIME代码VM级别(反射可以使用)
@Target(ElementType.FIELD) //注解使用范围
public @interface ReflectBindView {
    int value();
}

注意三点:

  1. 因为使用反射实现,注解定义的Retention保留范围应该是RUNTIME,代表VM运行时可以得到相应的注解。
  2. 设置Target代表注解的使用范围,FIELD代表Java类的属性。
  3. 可以有一个value()方法,方便在注解中增加元数据。

反射相关的代码如下:

ini 复制代码
public static void bind(Activity activity) {
    Class clazz = activity.getClass();
    Field[] fields = clazz.getDeclaredFields();
    for (Field field : fields) {
        if (field.isAnnotationPresent(ReflectBindView.class)) {
            ReflectBindView annotation = field.getAnnotation(ReflectBindView.class);
            int resId = annotation.value();
            try {
                Method method = clazz.getMethod("findViewById", int.class);
                Object invoke = method.invoke(activity, resId);
                field.setAccessible(true);
                field.set(activity, invoke);
            } catch (NoSuchMethodException e) {
                e.printStackTrace();
            } catch (InvocationTargetException e) {
                e.printStackTrace();
            } catch (IllegalAccessException e) {
                e.printStackTrace();
            }
        }
    }
}

ReflectBinnView注解在相关的Activity中的定义,代码如下:

python 复制代码
@ReflectBindView(R.id.tv_hello)
TextView mHelloTv;
@ReflectBindView(R.id.tv_name)
TextView mNameTv;

大体思路是获取Activity的所有属性,然后逐个判断属性是否是ReflectBindView注解,如果是此注解就通过value()方法获取设定的值,然后通过反射findViewById获取View值,最后通过field设置刚刚获取的view值。

因为运行时注解须要在Activity初始化中进行绑定操作,调用了大量反射相关代码,在界面复杂的状况下,使用这种方法就会严重影响Activity初始化效率。

ButterKnife使用了更高效的方式------Annotation Processor来完成这一工作,Pluggable Annotation Processing API是JDK6新增的注解处理API。那么什么是Annotation Processor呢?

ButterKnife如何实现View绑定

Annotation Processor使用 javapoet生成Java文件。 Android整个编译过程就是 source(源代码) -> processor(处理器) -> generate (文件生成)-> javacompiler -> .class 文件 -> .dex(只针对安卓)。Annotation Processor执行时机在Android项目编译的第一步,这样生成的Java代码才最终会编译到Android apk软件中。 ButterKnife使用的是源码级注解,代码如下:

java 复制代码
package com.example.lib_annotation;

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

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

Annotation Processor处理流程如下图所示: 实现的AbstractProcessor代码如下:

java 复制代码
package com.example.lib;
import java.io.IOException;
import java.util.Collections;
import java.util.Set;

import com.example.lib_annotation.BindView;
import com.google.auto.service.AutoService;
import com.squareup.javapoet.ClassName;
import com.squareup.javapoet.JavaFile;
import com.squareup.javapoet.MethodSpec;
import com.squareup.javapoet.TypeSpec;

import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.Filer;
import javax.annotation.processing.ProcessingEnvironment;
import javax.annotation.processing.Processor;
import javax.annotation.processing.RoundEnvironment;
import javax.annotation.processing.SupportedSourceVersion;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.Element;
import javax.lang.model.element.Modifier;
import javax.lang.model.element.TypeElement;
import javax.tools.Diagnostic;

//@SupportedAnnotationTypes("com.example.lib_annotation.BindView")
@SupportedSourceVersion(SourceVersion.RELEASE_8)
@AutoService(Processor.class)
public class BindingProcessor extends AbstractProcessor {
    private Filer filer;
    @Override
    public synchronized void init(ProcessingEnvironment processingEnv) {
        super.init(processingEnv);
        filer = processingEnv.getFiler();
    }

    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        System.out.println("BindingProcessor start");
        for (Element element : roundEnv.getRootElements()) {
            String packageStr = element.getEnclosingElement().toString();
            String classStr = element.getSimpleName().toString();
            ClassName className = ClassName.get(packageStr, classStr + "$Binding");
            MethodSpec.Builder constructorBuilder = MethodSpec.constructorBuilder()
                    .addModifiers(Modifier.PUBLIC)
                    .addParameter(ClassName.get(packageStr, classStr), "activity");
            boolean hasBinding = false;
            for (Element e: element.getEnclosedElements()) {
                BindView bindView = e.getAnnotation(BindView.class);
                if (bindView != null) {
                    hasBinding = true;
                    constructorBuilder.addStatement
                            ("activity.$L = activity.findViewById($L)", e.getSimpleName()
                                    , bindView.value());
                }
            }
            TypeSpec buildClass = TypeSpec.classBuilder(className)
                    .addModifiers(Modifier.PUBLIC)
                    .addMethod(constructorBuilder.build()
                    )
                    .build();
            if (hasBinding) {
                try {
                    JavaFile.builder(packageStr, buildClass)
                            .build()
                            .writeTo(filer);
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
        processingEnv.getMessager().printMessage(Diagnostic.Kind.NOTE, "BindingProcessor start process annotation");
        return false;
    }

    @Override
    public Set<String> getSupportedAnnotationTypes() {
        System.out.println("BindingProcessor getSupportedAnnotationTypes");
        return Collections.singleton(BindView.class.getCanonicalName());
    }
}

需要注意几点:

  1. AbstractProcessor中需要重写的getSupportedAnnotationTypes方法定义的是需要此注解处理器处理的注解的名称。
  2. 生成的Java代码的类一般和我们手动创建的类的名称做区分,比如说添加$Binding作为类名称后缀。
  3. Javapoet的具体如何使用可以到它的网站进行了解熟悉。

在子项目中配置annotationProcessor,如:

java 复制代码
annotationProcessor project(':lib') //AbstractProcessor所在项目
implementation project(':lib-annotation') //BindView注解所在项目

在Activity中的属性设置注解的值,代码如下:

python 复制代码
@BindView(R.id.tv_hello)
TextView mHelloTv;
@BindView(R.id.tv_name)
TextView mNameTv;

重新编译项目,或者运行Android项目能够执行生成Java代码的逻辑。重新编译项目如下图所示:

生成的Java代码如下图所示:

Activity如何使用上面想成的Java代码呢,自然而然就是反射,代码如下:

java 复制代码
package com.example.simplebutterknife;

import android.app.Activity;

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

public class ProcessBindViewUtil {
    public static void bind(Activity activity) {
        try {
            Class bindClass = Class.forName(activity.getClass().getCanonicalName() + "$Binding");
            Class activityClass = activity.getClass();
            Constructor constructor = bindClass.getConstructor(activityClass);
            constructor.newInstance(activity);
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (NoSuchMethodException e) {
            e.printStackTrace();
        } catch (InvocationTargetException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (InstantiationException e) {
            e.printStackTrace();
        }


    }
}

在Activity中的onCreate方法中,在调用setContentView() 之后调用ProcessBindViewUtil.bind(this)方法就可以完成了。

参考资料:github.com/caiweihao/S...

致谢

希望文章对大家有所帮助,如果文章有所纰漏请不吝指教,大家共同进步。欢迎关注"技术蔡"的公众号,此公众号也是本作者的技术相关的公众号。之后会发布一系列Android相关的文章,可能会涉及到视频编解码,手机录屏,JNI,ASM字节码处理,hook等技术的应用,希望对大家有所帮助。

相关推荐
天空中的野鸟44 分钟前
Android音频采集
android·音视频
小白也想学C2 小时前
Android 功耗分析(底层篇)
android·功耗
曙曙学编程2 小时前
初级数据结构——树
android·java·数据结构
闲暇部落4 小时前
‌Kotlin中的?.和!!主要区别
android·开发语言·kotlin
诸神黄昏EX6 小时前
Android 分区相关介绍
android
大白要努力!7 小时前
android 使用SQLiteOpenHelper 如何优化数据库的性能
android·数据库·oracle
Estar.Lee7 小时前
时间操作[取当前北京时间]免费API接口教程
android·网络·后端·网络协议·tcp/ip
Winston Wood7 小时前
Perfetto学习大全
android·性能优化·perfetto
浩宇软件开发8 小时前
Android开发,使用TabLayout+ViewPager2实现校园健康安全宣传
android studio·android开发
Dnelic-10 小时前
【单元测试】【Android】JUnit 4 和 JUnit 5 的差异记录
android·junit·单元测试·android studio·自学笔记