Android ButterKnife框架实现原理

2024-08-21 10:08

本文主要是介绍Android ButterKnife框架实现原理,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

介绍

ButterKnife相信大家都很熟悉了,网上介绍其使用方法的文章很多,还不知道ButterKnife是啥的小伙伴可以先去了解一下。
ButterKnife用一个注解就替代了findViewById方法。用起来非常方便,但是你有没有想过为啥就不用写findViewById方法了呢,难道代码就真的没有跑findViewById了吗。
来来来,我们来自己手写一个ButterKnife,来学习一下他的技术。

效果

先看一下Demo的效果吧,先展示出来效果大家才有看下去的动力,毕竟光说不练假把式。

在这里插入图片描述
上面代码就是在activity中的使用,是不是跟ButterKnife一样。

@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);MyButterKnife.bind(this);}

当然同样需要在onCreate中bind一下。
下面是实际的运行效果。
在这里插入图片描述
△开始时候TextView显示的是Text1,当点击第一个Button后改变TextView的text。
在这里插入图片描述
△点击按钮后的结果,字符串发生的改变,Button,TextView,String都是通过我们自己实现的ButterKnife绑定的。

在这里插入图片描述

实现

话不多说,动手开干。
个人精力有限仅实现了BindString、BindView、OnClick,其他的小伙伴们可以自己试着实现,原理都是一样的。
首先看一下Demo的目录结构。

目录结构
先说明一下各个module的作用:

  • annotation:声明的注解,java library。
  • annotation_processor:注解处理器,java library。
  • app:测试用的app,application。
  • butterknife:调用findViewById方法,android library。

annotation module

第一步先声明我们自己的注解,这里声明了三个

在这里插入图片描述
以BingString为例,举个栗子。

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;@Retention(RetentionPolicy.CLASS)
@Target(ElementType.FIELD)
public @interface BindString {int value();
}

@interface就是声明注解,这个注解有value,如果需要多个value就将int改成数组。
@Retention(RetentionPolicy.CLASS) 可以理解成注解存在的生命周期。有三种:

  1. @Retention(RetentionPolicy.SOURCE) 注解只存在源码中,编译时将被编译器丢弃,比如我们常见的@Override
  2. @Retention(RetentionPolicy.CLASS) 编译器将注解记录在类文件中,但不会加载到JVM中。
  3. @Retention(RetentionPolicy.RUNTIME) 注解信息会保留在源文件、类文件中,在执行的时也加载到Java的JVM中,因此可以反射性的读取。

@Target(ElementType.FIELD)是表明这个注解用来修饰属性,像我们自定义的OnClick注解就要用@Target(ElementType.METHOD)来修饰,因为OnClick是添加到方法上面的。

annotation_processor module

这个模块是重点,主要的操作都是放到了这个模块里面。
它的作用就是在编译的时候根据标签,为activity生成一个activity$$BindView文件,并在构造方法中调用findViewById方法。所以ButterKnife并不是说就不会调用findViewById方法,而是它替我们写好了这些代码。

在这里插入图片描述
当新建好这个module后第一步修改build.gradle文件,添加相关依赖。
Android Studio 3.4+的版本是以下这种写法。

dependencies {implementation fileTree(dir: 'libs', include: ['*.jar'])implementation project(path: ':annotation')annotationProcessor'com.google.auto.service:auto-service:1.0-rc4'compileOnly 'com.google.auto.service:auto-service:1.0-rc3'
}

然后需要封装一个ElementClassify ,作用是将同一个类里面的注解放到一起。然后跟这个类对应起来,方便我们生成新的类时候使用。

import java.util.List;
import javax.lang.model.element.ExecutableElement;
import javax.lang.model.element.VariableElement;/*** 内部包含一个类中的所有带注解的元素*/
public class ElementClassify {//view的节点listpublic List<VariableElement> viewElements;//onclick的节点listpublic List<ExecutableElement> methodElements;//string的节点listpublic List<VariableElement> stringElements;public List<VariableElement> getStringElements() {return stringElements;}public void setStringElements(List<VariableElement> stringElements) {this.stringElements = stringElements;}public List<VariableElement> getViewElements() {return viewElements;}public void setViewElements(List<VariableElement> viewElements) {this.viewElements = viewElements;}public List<ExecutableElement> getMethodElements() {return methodElements;}public void setMethodElements(List<ExecutableElement> methodElements) {this.methodElements = methodElements;}
}

重点来了,下面就是注解处理器,这里主要是进行分类,把注解的元素和包含它的类用Map对应起来,然后生成一个名叫类名$$ViewBinder的文件。

import com.google.auto.service.AutoService;
import com.honeywell.annotation.BindString;
import com.honeywell.annotation.BindView;
import com.honeywell.annotation.OnClick;import java.io.IOException;
import java.io.Writer;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
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.ExecutableElement;
import javax.lang.model.element.Name;
import javax.lang.model.element.PackageElement;
import javax.lang.model.element.TypeElement;
import javax.lang.model.element.VariableElement;
import javax.lang.model.type.TypeMirror;
import javax.tools.Diagnostic;
import javax.tools.JavaFileObject;@AutoService(Processor.class)
public class AnnotationProcessor extends AbstractProcessor {private Filer filer;public void logUtil(String message) {Messager messager = processingEnv.getMessager();messager.printMessage(Diagnostic.Kind.NOTE, message);}@Overridepublic synchronized void init(ProcessingEnvironment processingEnvironment) {super.init(processingEnvironment);logUtil("processor init ============================");filer = processingEnvironment.getFiler();}@Overridepublic boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {logUtil("processor process ============================");Map<TypeElement, ElementClassify> parseTargets = classifyElementWithClass(roundEnvironment);//如果map为空说明没有注解 什么都不用做if (parseTargets.size() <= 0) {return false;}Iterator<TypeElement> iterator = parseTargets.keySet().iterator();String newClassName;String packageName;Writer writer = null;//遍历map  为每一个acitvity生成$$ViewBinder文件while (iterator.hasNext()) {TypeElement classElement = iterator.next();ElementClassify elementClassify = parseTargets.get(classElement);newClassName = classElement.getSimpleName().toString();newClassName = newClassName + "$$ViewBinder";packageName = getPackageName(classElement);try {JavaFileObject javaFileObject = filer.createSourceFile(packageName + "." + newClassName);writer = javaFileObject.openWriter();StringBuffer stringBuffer = getStringBuffer(packageName, newClassName, classElement, elementClassify);writer.write(stringBuffer.toString());} catch (IOException e) {e.printStackTrace();} finally {if (writer != null) {try {writer.close();} catch (IOException e) {e.printStackTrace();}}}}return false;}/*** 生成新的class文件* @param packageName* @param newClassName* @param classElement* @param elementClassify* @return*/private StringBuffer getStringBuffer(String packageName, String newClassName, TypeElement classElement,ElementClassify elementClassify) {StringBuffer stringBuffer = new StringBuffer();stringBuffer.append("package " + packageName + ";\n");stringBuffer.append("import android.view.View;\n");stringBuffer.append("import android.util.Log;\n");stringBuffer.append("public class " + newClassName + "{\n");stringBuffer.append("\tpublic " + newClassName + "(final " + classElement.getQualifiedName() + " target){\n");stringBuffer.append("\t\tLog.d(\"gsy\",\"constractor\");\n");if (elementClassify != null && elementClassify.getViewElements() != null && elementClassify.getViewElements().size() > 0) {List<VariableElement> viewElements = elementClassify.getViewElements();for (VariableElement variableElement : viewElements) {TypeMirror typeMirror = variableElement.asType();Name name = variableElement.getSimpleName();int resId = variableElement.getAnnotation(BindView.class).value();stringBuffer.append("\t\ttarget." + name + " =(" + typeMirror + ")target.findViewById(" + resId + ");\n");}}if (elementClassify != null && elementClassify.getMethodElements() != null && elementClassify.getMethodElements().size() > 0) {List<ExecutableElement> methodElements = elementClassify.getMethodElements();for (ExecutableElement executableElement : methodElements) {int[] resIds = executableElement.getAnnotation(OnClick.class).value();String methodName = executableElement.getSimpleName().toString();for (int id : resIds) {stringBuffer.append("\t\t(target.findViewById(" + id + ")).setOnClickListener(new View.OnClickListener() {\n");stringBuffer.append("\t\t\tpublic void onClick(View p0) {\n");stringBuffer.append("\t\t\t\ttarget." + methodName + "(p0);\n");stringBuffer.append("\t\t\t}\n\t\t});\n");}}}if(elementClassify != null && elementClassify.getStringElements() != null && elementClassify.getStringElements().size() > 0){List<VariableElement> stringElements = elementClassify.getStringElements();for (VariableElement variableElement:stringElements){int id = variableElement.getAnnotation(BindString.class).value();Name name = variableElement.getSimpleName();stringBuffer.append("\t\ttarget."+name+" = target.getResources().getString("+id+");\n");}}stringBuffer.append("\t}\n}\n");return stringBuffer;}/*** 获取包名* @param classElement* @return*/private String getPackageName(Element classElement) {PackageElement packageElement = processingEnv.getElementUtils().getPackageOf(classElement);return packageElement.getQualifiedName().toString();}/*** 声明注解器支持的java版本* @return*/@Overridepublic SourceVersion getSupportedSourceVersion() {return processingEnv.getSourceVersion();}/*** 声明要处理的注解* @return*/@Overridepublic Set<String> getSupportedAnnotationTypes() {Set<String> annotationSet = new HashSet<>();annotationSet.add(BindView.class.getCanonicalName());annotationSet.add(OnClick.class.getCanonicalName());annotationSet.add(BindString.class.getCanonicalName());return annotationSet;}//TypeElement 对应class ElementClassify 对应class中的注解元素对应的方法或者变量private Map<TypeElement, ElementClassify> classifyElementWithClass(RoundEnvironment roundEnvironment) {//建立类和方法、变量的对应关系 便于生成新的class文件Map<TypeElement, ElementClassify> classElementMap = new HashMap<>();//通过roundEnvironment获取到添加了BindView注解的所有元素Set<? extends Element> viewElementsAnnotatedWith = roundEnvironment.getElementsAnnotatedWith(BindView.class);//通过roundEnvironment获取到添加了OnClick注解的所有方法Set<? extends Element> methodElementsAnnotatedWith = roundEnvironment.getElementsAnnotatedWith(OnClick.class);//通过roundEnvironment获取到添加了BindString注解的所有元素Set<? extends Element> stringElementsAnnotatedWith = roundEnvironment.getElementsAnnotatedWith(BindString.class);//处理view 首先遍历view元素 然后封装到ElementClassify中for (Element viewElement : viewElementsAnnotatedWith) {//VariableElement可以理解为一个变量元素VariableElement variableElement = (VariableElement) viewElement;//获取到变量所在的class节点TypeElement classElement = (TypeElement) variableElement.getEnclosingElement();//先从map中取ElementClassifyElementClassify elementClassify = classElementMap.get(classElement);//List用来存放注解的对象节点List<VariableElement> viewElements;if (elementClassify != null) {//取出viewElements = elementClassify.getViewElements();logUtil("view list size="+viewElements.size());//如果list为空 新建一个 并放入ElementClassifyif (viewElements == null) {viewElements = new ArrayList<>();elementClassify.setViewElements(viewElements);}} else {elementClassify = new ElementClassify();viewElements = new ArrayList<>();elementClassify.setViewElements(viewElements);if (!classElementMap.containsKey(classElement)) {//将activity节点和list对应起来logUtil("viewlist size ="+elementClassify.getViewElements().size());classElementMap.put(classElement, elementClassify);}}logUtil(variableElement.getSimpleName().toString());//将节点放入ListviewElements.add(variableElement);}//处理methodfor (Element methodElement : methodElementsAnnotatedWith) {//ExecutableElement元素对应methodExecutableElement executableElement = (ExecutableElement) methodElement;TypeElement typeElement = (TypeElement) methodElement.getEnclosingElement();List<ExecutableElement> methodList;ElementClassify elementClassify = classElementMap.get(typeElement);if (elementClassify != null) {methodList = elementClassify.getMethodElements();if (methodList == null) {methodList = new ArrayList<>();elementClassify.setMethodElements(methodList);}} else {elementClassify = new ElementClassify();methodList = new ArrayList<>();elementClassify.setMethodElements(methodList);if (!classElementMap.containsKey(typeElement)) {classElementMap.put(typeElement, elementClassify);}}methodList.add(executableElement);}//处理stringfor (Element stringElement : stringElementsAnnotatedWith) {//VariableElementVariableElement variableElement = (VariableElement) stringElement;//获得所在的类TypeElement typeElement = (TypeElement) stringElement.getEnclosingElement();List<VariableElement> stringList;ElementClassify elementClassify = classElementMap.get(typeElement);if (elementClassify != null) {stringList = elementClassify.getStringElements();if (stringList == null) {stringList = new ArrayList<>();elementClassify.setStringElements(stringList);}} else {stringList = new ArrayList<>();elementClassify = new ElementClassify();elementClassify.setStringElements(stringList);if(!classElementMap.containsKey(typeElement)){classElementMap.put(typeElement,elementClassify);}}stringList.add(variableElement);}return classElementMap;}}

代码中都添加了注释,大家自己看一下就行,最好是可以自己动手撸一遍。我自己写时候踩了个坑,这个类要添加注解@AutoService(Processor.class),因为自动补全的功能给我写成了@AutoService(Processo.class),找了好久才发现。

butterknife module

这个module作用就是调用上一步生成的$$ViewBinder中的构造方法。
在这里插入图片描述
MyButterKnife就一个方法,bind方法,通过反射机制运行ViewBind中的 构造方法

import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;public class MyButterKnife {/*** 通过反射的方式运行***$$ViewBinder* 达到运行findViewById方法* @param activity*/public static void bind(Object activity){String name = activity.getClass().getName();String bindName = name+"$$ViewBinder";try {Class<?> clazz = Class.forName(bindName);Constructor<?> constructor = clazz.getConstructor(activity.getClass());constructor.newInstance(activity);} catch (ClassNotFoundException e) {e.printStackTrace();} catch (NoSuchMethodException e) {e.printStackTrace();} catch (IllegalAccessException e) {e.printStackTrace();} catch (InstantiationException e) {e.printStackTrace();} catch (InvocationTargetException e) {e.printStackTrace();}}
}

app module

app就不用贴了吧,就是一些简单的布局,再啰嗦一下依赖方法,在build.gradle的dependencies下面添加上对刚才三个模块的引用,注意注解处理器模块要用annotationProcessor project。

	implementation project(path: ':annotation')annotationProcessor project(path: ':annotation_processor')implementation project(path: ':butterknife')

总结

当所有代码写完后先build一下。如果代码没有问题会有中间文件$$ViewBinder.class文件生成。路径在build/intermediates/javac下面,举个栗子:
在这里插入图片描述
红框中就是注解处理器生成的中间文件,它们是在编译时生成的,我们看一下它的内容:
在这里插入图片描述
是不是恍然大悟,写了那么多就是为了生成这些findViewById、setOnClickListener代码。本文只是讲述了一下实现过程,对于注解、apt、反射等知识点没有过多讲解,小伙伴要是想学习可以网上搜一下,各种博客一大把。
最后奉上源码,github:源码路径
csdn下载:csdn下载路径
水平有限,如有错误欢迎指正讨论。

这篇关于Android ButterKnife框架实现原理的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1092859

相关文章

hdu1043(八数码问题,广搜 + hash(实现状态压缩) )

利用康拓展开将一个排列映射成一个自然数,然后就变成了普通的广搜题。 #include<iostream>#include<algorithm>#include<string>#include<stack>#include<queue>#include<map>#include<stdio.h>#include<stdlib.h>#include<ctype.h>#inclu

深入探索协同过滤:从原理到推荐模块案例

文章目录 前言一、协同过滤1. 基于用户的协同过滤(UserCF)2. 基于物品的协同过滤(ItemCF)3. 相似度计算方法 二、相似度计算方法1. 欧氏距离2. 皮尔逊相关系数3. 杰卡德相似系数4. 余弦相似度 三、推荐模块案例1.基于文章的协同过滤推荐功能2.基于用户的协同过滤推荐功能 前言     在信息过载的时代,推荐系统成为连接用户与内容的桥梁。本文聚焦于

【C++】_list常用方法解析及模拟实现

相信自己的力量,只要对自己始终保持信心,尽自己最大努力去完成任何事,就算事情最终结果是失败了,努力了也不留遗憾。💓💓💓 目录   ✨说在前面 🍋知识点一:什么是list? •🌰1.list的定义 •🌰2.list的基本特性 •🌰3.常用接口介绍 🍋知识点二:list常用接口 •🌰1.默认成员函数 🔥构造函数(⭐) 🔥析构函数 •🌰2.list对象

【Prometheus】PromQL向量匹配实现不同标签的向量数据进行运算

✨✨ 欢迎大家来到景天科技苑✨✨ 🎈🎈 养成好习惯,先赞后看哦~🎈🎈 🏆 作者简介:景天科技苑 🏆《头衔》:大厂架构师,华为云开发者社区专家博主,阿里云开发者社区专家博主,CSDN全栈领域优质创作者,掘金优秀博主,51CTO博客专家等。 🏆《博客》:Python全栈,前后端开发,小程序开发,人工智能,js逆向,App逆向,网络系统安全,数据分析,Django,fastapi

hdu4407(容斥原理)

题意:给一串数字1,2,......n,两个操作:1、修改第k个数字,2、查询区间[l,r]中与n互质的数之和。 解题思路:咱一看,像线段树,但是如果用线段树做,那么每个区间一定要记录所有的素因子,这样会超内存。然后我就做不来了。后来看了题解,原来是用容斥原理来做的。还记得这道题目吗?求区间[1,r]中与p互质的数的个数,如果不会的话就先去做那题吧。现在这题是求区间[l,r]中与n互质的数的和

让树莓派智能语音助手实现定时提醒功能

最初的时候是想直接在rasa 的chatbot上实现,因为rasa本身是带有remindschedule模块的。不过经过一番折腾后,忽然发现,chatbot上实现的定时,语音助手不一定会有响应。因为,我目前语音助手的代码设置了长时间无应答会结束对话,这样一来,chatbot定时提醒的触发就不会被语音助手获悉。那怎么让语音助手也具有定时提醒功能呢? 我最后选择的方法是用threading.Time

Android实现任意版本设置默认的锁屏壁纸和桌面壁纸(两张壁纸可不一致)

客户有些需求需要设置默认壁纸和锁屏壁纸  在默认情况下 这两个壁纸是相同的  如果需要默认的锁屏壁纸和桌面壁纸不一样 需要额外修改 Android13实现 替换默认桌面壁纸: 将图片文件替换frameworks/base/core/res/res/drawable-nodpi/default_wallpaper.*  (注意不能是bmp格式) 替换默认锁屏壁纸: 将图片资源放入vendo

C#实战|大乐透选号器[6]:实现实时显示已选择的红蓝球数量

哈喽,你好啊,我是雷工。 关于大乐透选号器在前面已经记录了5篇笔记,这是第6篇; 接下来实现实时显示当前选中红球数量,蓝球数量; 以下为练习笔记。 01 效果演示 当选择和取消选择红球或蓝球时,在对应的位置显示实时已选择的红球、蓝球的数量; 02 标签名称 分别设置Label标签名称为:lblRedCount、lblBlueCount

Android平台播放RTSP流的几种方案探究(VLC VS ExoPlayer VS SmartPlayer)

技术背景 好多开发者需要遴选Android平台RTSP直播播放器的时候,不知道如何选的好,本文针对常用的方案,做个大概的说明: 1. 使用VLC for Android VLC Media Player(VLC多媒体播放器),最初命名为VideoLAN客户端,是VideoLAN品牌产品,是VideoLAN计划的多媒体播放器。它支持众多音频与视频解码器及文件格式,并支持DVD影音光盘,VCD影

Kubernetes PodSecurityPolicy:PSP能实现的5种主要安全策略

Kubernetes PodSecurityPolicy:PSP能实现的5种主要安全策略 1. 特权模式限制2. 宿主机资源隔离3. 用户和组管理4. 权限提升控制5. SELinux配置 💖The Begin💖点点关注,收藏不迷路💖 Kubernetes的PodSecurityPolicy(PSP)是一个关键的安全特性,它在Pod创建之前实施安全策略,确保P