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等技术的应用,希望对大家有所帮助。

相关推荐
拭心10 小时前
Google 提供的 Android 端上大模型组件:MediaPipe LLM 介绍
android
带电的小王12 小时前
WhisperKit: Android 端测试 Whisper -- Android手机(Qualcomm GPU)部署音频大模型
android·智能手机·whisper·qualcomm
梦想平凡12 小时前
PHP 微信棋牌开发全解析:高级教程
android·数据库·oracle
元争栈道12 小时前
webview和H5来实现的android短视频(短剧)音视频播放依赖控件
android·音视频
阿甘知识库13 小时前
宝塔面板跨服务器数据同步教程:双机备份零停机
android·运维·服务器·备份·同步·宝塔面板·建站
元争栈道14 小时前
webview+H5来实现的android短视频(短剧)音视频播放依赖控件资源
android·音视频
MuYe14 小时前
Android Hook - 动态加载so库
android
居居飒15 小时前
Android学习(四)-Kotlin编程语言-for循环
android·学习·kotlin
Henry_He18 小时前
桌面列表小部件不能点击的问题分析
android
工程师老罗18 小时前
Android笔试面试题AI答之Android基础(1)
android