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

相关推荐
石山岭7 小时前
自己动手写了一个 Android 虚拟定位 App:GPSSimulate 技术实
android·前端
杉氧10 小时前
副作用 (Side Effects) 全攻略:如何像大师一样掌控 Composable 的生命周期?
android·架构·android jetpack
Kapaseker14 小时前
Kotlin Toolchain 0.11 发布:主要是把 Amper 干没了
android·kotlin
三少爷的鞋15 小时前
Android 现代架构不需要事件总线进阶篇
android
杉氧1 天前
深入理解 Compose 重组机制:快照系统如何驱动 UI 精准刷新?
android·架构·android jetpack
召钱熏1 天前
状态枚举正确≠渲染正确:一个语音按钮的状态机边界修复实录
android·前端
杉氧1 天前
深度解析:Jetpack Compose 核心架构与底层原理 —— 十年安卓老兵的“破茧重生”
android·架构·android jetpack
通玄1 天前
Jetpack Compose 入门系列(七):ViewModel 与界面状态管理
android
落魄Android在线炒饭1 天前
Android Framework 开发技巧:android.jar 生成与系统快速编译验证
android
如此风景1 天前
Kotlin Flow操作符学习
android·kotlin