Android反射与注解探究
一、 Android中的反射
1.1 反射基本概念
1.1.1什么是反射?
JAVA反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意一个方法和属性(包括私有方法和属性);这种动态获取的信息以及动态调用对象的方法的功能称为java语言的反射机制(注意关键词:运行状态)换句话说,Java程序可以加载一个运行时才得知名称的class,获悉其完整构造(但不包括methods定义),并生成其对象实体、或对其fields设值、或唤起其methods。
1.1.2 反射可以提供哪些功能
- 运行时判断对象所属类
- 运行时构造任何一个类对象
- 运行时调用对象的任意方法
- 运行时设置对象的任意属性值
1.2 反射类和方法
java为Reflection机制提供了特定的类和方法,主要是五个类:Class、Method、Field、Constractor和Array。要想掌握反射的使用,必须掌握这五个类的使用方法。下面简要介绍下它们的用途和常用方法。
1.2.1 Class:
在程序运行期间,Java运行时系统始终为所有的对象维护一个被称为运行时的类型标识。这个信息跟踪着每个对象所属的类。JVM利用运行时信息选择相应的方法执行。而保存这些信息的类称为Class。可能容易产生混淆,容易想到class。不过二者没什么关系,class不过是描述类的一个关键字。而Class却是保存着运行时信息的类。
Class是反射的起源也是入口,唯有获取到Class对象才能进行后续反射操作。那么如何获取这个对象呢,查看源码可以得知Class类没有公共构造方法,也就是说不能通过显示方法构造Class对象,而是由jvm在第一次加载类时由类加载器生成。因此我们通常只用关心Class对象的获取和使用即可。Class对象获取主要有三种方式
- Class.forName(“完整类名”)
- 实例对象.getClass()
- 类名.class
Class主要有以下方法可供调用:
方法名 | 说明 |
forName() | (1)获取Class对象的一个引用,但引用的类还没有加载(该类的第一个对象没有生成)就加载了这个类。(2)为了产生Class引用,forName()立即就进行了初始化 |
Object.getClass() | 获取Class对象的一个引用 |
getName() | 获取Class对象的全限定类名,也就是包名+类名 |
getSimpleName | 单纯获取Class对象类名 |
isInterface() | 判断类是否接口 |
getInterfaces() | 获取类的全部实现接口 |
getSuperClass() | 获取类的直接基类,也就是父类 |
newInstance() | 生成类的无参构造函数生成类对象,使用该方法时,类必须带有无参构造器,且不可为私有 |
getDeclaredMethod(String name,Class[] parms) | 返回对象Method,获取类中自己声明的所有方法,通过此方法可获取类中所有声明方法.类似还getDeclaredField(),getConstructor() |
getMethods() | 返回对象Array 获取类(包含父类)中的所有公共方法,类似还有getFields(),getConstructor()。 |
1.2.2 Method:
Method是一个类,位于java.lang.reflect包下,继承自Executable类。在Java反射中 Method类描述的是 类的方法信息。Method的获取方法都在前面介绍的Class类中,主要方法:
方法名 | 说明 |
getName() | 获取方法名 |
getReturnType() | 返回Class对象,表示返回值类型 |
getParameters() | 返回Parameter数组,表示参数序列 |
getTypeParameters() | 返回一个TypeVariable对象数组,表示该方法对象声明列表上的类型变量数组 |
invole(Object obj,Objece[] parms) | 调用method方法,第一个为调用对象,后面是方法参数值(private 方法设置accessible为true时才可正常访问,否则会报错) |
isAccessible() | 返回对象可访问标识 |
setAccessible() | 设置可访问标识,设置为true时,可访问对象的private方法 |
getAnnotations() | 返回方法上的所有注解 |
getAnnotationByType(Class clz) | 如果该方法对象存在指定类型的注解,则返回该注解数组,否则返回null |
1.2.3 Field:
Field类同样位于java.lang.reflect包下,为我们提供了获取当前对象成员变量类型和重新设值的方法。获取方式同样在Class类中。主要方法:
方法 | 说明 |
getType() | 获取当前变量类型:基本类型或引用类型 |
getModifier() | 获取当前变量修饰符 |
setAccessible() | 设置可访问标识 |
set(Object obj,Object value) | 为对象设置当前属性值,类似方法还有setInt(),setShort()… |
1.2.4 Constructor:
类的构造方法,继承自Executable类,同样在Class中通过getConstructors()和getDeclaredConstructors()获得,特定构造方法可通过getDeclaredConstructor(Class[] parms)获取,parms代表参数类型。借助Constructor可直接生成类实例。
方法 | 说 |
newInstance(Object … initargs) | 返回对应构造方法对象,参数为可变参数 |
setAccessible() | 设置可访问标识 |
getAnnotations() | 获取构造方法上的所有注解 |
1.2.5 Array:
Array提供了动态创建和访问数组元素的各种静态方法,位于java.lang.reflect包下。注意和Arrays进行区分,Arrays主要对数组进行复制,排序、搜索等操作;而Array主要供反射调用,动态创建数组,并进行访问和修改,用于反射时参数的读取和录入。主要方法:
方法 | 说明 |
newInstance(Class clz,int length) | 构建clz类型,容量为length的数组返回类型为Object |
get(Object obj,int index) | 访问obj数组的index下标的对应元素,类似的还有getBoolean()等… |
getLength() | 返回数组容量(注意是总容量,不是当前长度) |
set(Object array,Object item,int index) | 设置array中index对应值为item,同样还有setInt()等… |
1.3 反射在Android中的应用
1.3.1 简单使用
通过反射,我们可以借助类名直接生成类的实例,而不需要进行导包操作,即我们可以通过类名来进行其他包内类实例的生成和方法的调用,只要对应类是存在的。使用Person类作为反射对象,进行反射的常规操作。
package com.example.beans;
/**
* @author Mr.m
* @date 2019/8/30
**/
public class Person {
String name;
int age;
private Person() {
}
private Person(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
@Override
public String toString() {
return "Person{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
可以看到Person类只提供了私有构造方法,因此通过常规方式,我们无法从外部得到一个Person对象,而借助反射我们可以生成一个Person类的实例,还能进行成员方法的调用,并且是发生在不导包的情况下。
public static void main(String[] args) throws ClassNotFoundException, InstantiationException, IllegalAccessException {
//通过完整类名获取Class对象
Class c = Class.forName("com.example.beans.Person");
try {
//获取构造函数对象
Constructor constructor=c.getDeclaredConstructor(String.class,int.class);
//由于构造方法是私有的因此需要将可访问标识设为true
constructor.setAccessible(true);
//通过构造函数生成Person实例
Object bean = constructor.newInstance("张三", 32);
//输出Person{name='张三', age=32}
System.out.println(bean.toString())
//获取无参构造函数对象
Constructor constructor1=c.getDeclaredConstructor();
constructor1.setAccessible(true);
//构造无参实例
Object bean2 = constructor1.newInstance();
//获取setName方法对象
Method setName = c.getDeclaredMethod("setName", String.class);
//通过setName方法设置name值
setName.invoke(bean2, "李四");
//获取setAge方法
Method setAge = c.getDeclaredMethod("setAge", int.class);
//设置age值
setAge.invoke(bean2, 45);
//输出Person{name='李四', age=45}
System.out.println(bean2.toString());
//获取name属性对象
Field nameField = c.getDeclaredField("name");
//设置name属性可访问
nameField.setAccessible(true);
//设置name值
nameField.set(bean2, "王五");
//输出Person{name='王五', age=45}
System.out.println(bean2.toString());
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
} catch (NoSuchFieldException e) {
e.printStackTrace();
}
}
通过上面代码,我们借助类名获取了Person的Class类对象c,并借助此对象进一步获取到了构造方法、成员方法、成员属性等一系列对象,从而进行Person实例的构造以及方法的调用和属性的设置等操作。
1.3.2 全局View点击监听
试想某天产品提出了全局防止多次频繁的需求,界面稍微多点,逐个设置监听就是一场灾难。然而借助反射,我们可以轻松实现,只需一行代码即可搞定一个Activity(如果有BaseActivity的话就更方便了)。话不多少,先贴代码:
package com.example.reflectpractice;
import android.util.Log;
import android.view.View;
import android.view.ViewGroup;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
/**
* @User Created By Mr.m
* @Date 2019/8/2
**/
public class HookClickListenerUtils {
/**
*通过递归对每子View执行hook函数
**/
public static void hookViewGroup(View view) {
if (view instanceof ViewGroup) {
//遍历子View
for (int i = 0; i < ((ViewGroup) view).getChildCount(); i++) {
if (((ViewGroup) view).getChildAt(i) instanceof ViewGroup) {
//递归调用
hockViewGroup(((ViewGroup) view).getChildAt(i));
} else {
hockView(((ViewGroup) view).getChildAt(i));
}
}
} else {
hockView(view);
}
}
/**
*对View进行hook
**/
public static void hookView(View view) {
try {
//获取View的Class对象
Class cls = Class.forName("android.view.View");
//获取getListenerInfo方法
Method getListenerInfoMethod = cls.getDeclaredMethod("getListenerInfo");
//方法不可访问时设置可访问标识
if (!getListenerInfoMethod.isAccessible()) {
getListenerInfoMethod.setAccessible(true);
}
//获取具体View的ListenerInfo对象
Object listenerInfoObject = getListenerInfoMethod.invoke(view);
//获取ListenerInfo内部类对象
Class listenerInfoCls = Class.forName("android.view.View$ListenerInfo");
//获取onClickListener属性
Field onClickListerField=listenerInfoCls
.getDeclaredField("mOnClickListener");
//设置onClickListener属性可访问
onClickListerField.setAccessible(true);
//为listenerInfo对象设置代理监听
onClickListerField.set(listenerInfoObject,new HookListener((View.OnClickListener)onClickListerField.get(listenerInfoObject)) );
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
} catch (NoSuchFieldException e) {
e.printStackTrace();
}
}
/**
*View 代理监听接口
**/
private static class HookListener implements View.OnClickListener {
//View原监听对象
private View.OnClickListener onClickListener;
//两次点击最少间隔时间
private int MIN_CLICK_DELAY_TIME = 3000;
//上次点击时间
private long lastClickTime = 0;
@Override
public void onClick(View view) {
//当前时间
long curTime = System.currentTimeMillis();
//判断是否大于最小间隔时间
if (curTime - lastClickTime > MIN_CLICK_DELAY_TIME) {
onClickListener.onClick(view);
lastClickTime = curTime;
}else{
Log.i("频繁点击",""+view.getTag());
}
}
HookListener(View.OnClickListener listener) {
this.onClickListener = listener;
}
}
}
HookListenerUtils主要做了三件事:
- 对ViewGroup进行递归调用hookView()方法
- 通过hookView()方法,借助反射为View的onClickListener设置代理对象
- 定义HookListener静态内部类,用作代理对象,执行点击时的具体策略,唤起view的onClick还是另作频繁点击处理。
通过调用HookViewGroup()将对应的ViewGroup作为参数传入即可实现所有子View的频繁点击处理,因此我们自然而然的想到使用DecorView来进行当前Activity下所有View的处理,如此全局监听也就完成了。
1.3.3 调用其他jar包或者apk中的方法
我们知道所有的java类都运行在java虚拟机中,而要运行在虚拟机中,必须经过类加载器加载的过程。虽说在Android5.0以后使用ART作为虚拟机,但是类加载的过程是无可避免的。而android中的类加载器主要分为以下三类:
- BootClassLoader:主要用于加载系统的类,包括java和android系统的类库,和JVM中不同。BootClassLoader是ClassLoader内部类,是由Java实现的,它也是所有系统ClassLoader的父ClassLoader
- PathClassLoader:用于加载Android系统类和开发编写应用的类,只能加载已经安装应用的 dex 或 apk 文件,也是getSystemClassLoader的返回对象
- DexClassLoader:可以用于加载任意路径的zip,jar或者apk文件,也是进行安卓动态加载的基础
android中所有的类都是通过以上三种类加载器加载到虚拟机中的,同时由以上简介可知,我们要想调用其他jar包或者apk中的方法时,必须要通过PathClassLoader或者DexClassLoader进行相应类的加载,由于PathClassLoader存在的加载路径的限制,我们选用DexClassLoader进行类的加载。首先新建一个项目作为被调用apk,向外提供调用方法。
package com.example.hhh;
/**
* @User Created By Mr.m
* @Date 2019/8/2
**/
public class Share{
public void display(String xxx){
System.out.println(xxx);
}
}
代码及其简单,仅提供一个display方法供调用尝试,但是要注意在Manifest中启动Activity中加入exported=true,否则外界无法找到包内的类。
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme"
tools:ignore="AllowBackup,GoogleAppIndexingWarning">
<activity android:name="com.example.hhh.MainActivity"
android:exported="true">
<intent-filter>
<action android:name="com.hhh"/>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
<category android:name="android.intent.category.DEFAULT"/>
</intent-filter>
</activity>
</application>
将生成的apk放在调用者assert目录下。当然,在实际应用中,我们可以放在任何地方(包含从网络加载),这也是的动态加载成为可能。但是为了调用方便,我们通常会将assert中的文件复制到sd卡中以供调用,因为assert仅提供了文件流的方式对文件进行读取。通过前面的叙述我们知道要想调用类方法,必须将类加载到虚拟机,而我们选择了DexClassLoader作为累加器,那么这个类加载器如何获取呢。
/**
* Creates a {@code DexClassLoader} that finds interpreted and native
* code. Interpreted classes are found in a set of DEX files contained
* in Jar or APK files.
*
* <p>The path lists are separated using the character specified by the
* {@code path.separator} system property, which defaults to {@code :}.
*
* @param dexPath the list of jar/apk files containing classes and
* resources, delimited by {@code File.pathSeparator}, which
* defaults to {@code ":"} on Android
* @param optimizedDirectory this parameter is deprecated and has no effect since API level 26.
* @param librarySearchPath the list of directories containing native
* libraries, delimited by {@code File.pathSeparator}; may be
* {@code null}
* @param parent the parent class loader
*/
public DexClassLoader(String dexPath, String optimizedDirectory,
String librarySearchPath, ClassLoader parent) {
super(dexPath, null, librarySearchPath, parent);
}
通过源码,我们可以知道DexClassLoader的唯一构造方法,且在api26以上,optimizedDirectory是无效的,因此我们只需得到三个参数dexPath,librarySearchPath以及parent即可,显而易见的,三个参数分别为apk路径,apk中c语言或c++库路径以及父加载器,具体实现如下:
fun classLoaderTest() {
try {
//将assert路径下的apk复制到sd卡中,方便调用,返回为复制后apk的路径
val dexPath = Utils.copyFiles(baseContext, "hhh.apk")
//构造类加载器,未使用c或者c++库,librarySearchPath传空,parent为本应用加载器经过查询得知为系统类加载器BootClassLoader
val dexClassLoader = DexClassLoader(dexPath, null, null, javaClass.classLoader)
//根据dex路径获取PackageInfo对象,进一步获取包名
val loadPackageInfo =
packageManager.getPackageArchiveInfo(dexPath, PackageManager.GET_ACTIVITIES)
//通过DexClassLoader进行类的加载
val cls = dexClassLoader.loadClass(loadPackageInfo.packageName + ".Share")
//构建Share对象
val obj = cls.newInstance()
//获取display方法
val diaplayMethod = cls.getDeclaredMethod("display", String::class.java)
//调用obj的display方法,传递参数为“hhh”
diaplayMethod.invoke(obj, "hhh")
} catch (e: Exception) {
e.printStackTrace()
}
}
前面说过Class对象的获取可通过Class.forName()的方式,这里同样适用,只不过需要指定相应的ClassLoader,Class.forName提供了重载方法如下:
public static Class<?> forName(String name, boolean initialize,ClassLoader loader)
传入对应的类名及loader同样可以获得具体的Class对象。获取到具体对象之后的操作就跟之前反射代码没什么区别了。到此,我们的jar包或者apk方法调用就完成了。
二、Android中的注解
2.1 注解基本概念
2.1.1 什么是注解
Annotation(注解)就是Java提供了一种源程序中的元素关联任何信息或者任何元数据(metadata)的途径和方法。Annotation是被动的元数据,永远不会有主动行为,对程序的逻辑没有任何影响。也就是说注解只是一种外在的标识,并没有实际的处理操作,只有对这种标识进行识别,并做出相应的处理,注解才会有意义。
2.1.2 什么是元注解
元注解就是对注解进行标注的注解。通常情况下我们使用的JDK定义好的注解,比如@NonNull、@Override等,但是要用到自定义注解的话就要了解元注解的用法了。JDK1.5提供了四种元注解分别是:
- Retention(定义注解的保留策略):
- @Retention(RetentionPolicy.SOURCE)
源代码时期的注解,仅存在于.java文件中,编译后生成的class字节码文件中不包含。对这种注解的方式分为两种:
1.被IDEA读取,实时提醒开发者代码中的错误,例如@Override
2.在编译期间处理,.java文件编译生成class文件时期,通过开发者注册的注解处理器(AnnotationProcessor)对注解进行处理,借助JavaPoet自动生成模板java文件,用生成的java文件进行编译,避免编写重复代码,提升开发效率,同时由于是在编译期处理注解,对系统运行时的开销几乎是没有影响的。 - @Retention(RetentionPolicy.CLASS)//注解存在于class字节码文件中,运行时不存在,注解默认保留策略。对于这种注解一般是通过修改字节码进行处理,根据处理时机的不同分为两种:
- 源代码编译后:在编译完成生成.class后,通过调用asm编写的三方工具或者手动使用命令行对.class文件中的虚拟机指令进行修改,注解生成的.class不会被加载到虚拟机中。
- 类加载时期:在类加载时进行.class文件加载前,通过代理程序,对内存中的.class字节码进行修改,这种修改不会被保存到.class文件中,同样.class文件中注解对应的字节码不会被加载到虚拟机中。
- @Retention(RetentionPolicy.RUNTIME)// 注解会在class字节码文件中存在,能够像其他类对象一样被调用,通过反射进行相关属性的读取。这种处理方式采用的是直接编码方式,由于被加载到虚拟机中,因此这种方式是运行效率最低的,好处就是方便且容易实现。
综上,生命周期长度 SOURCE < CLASS < RUNTIME ,所以前者能作用的地方后者一定也能作用。一般如果需要在运行时去动态获取注解信息,那只能用 RUNTIME 注解;如果要在编译时进行一些预处理操作,比如生成一些辅助代码(如 ButterKnife),就用 CLASS注解;如果只是做一些检查性的操作,比如 @Override 和@SuppressWarnings,则可选用 SOURCE 注解。
- Target(定义注解的目标类型)
- @Target(ElementType.TYPE) //接口、类、枚举、注解
- @Target(ElementType.FIELD) //字段、枚举的常量
- @Target(ElementType.METHOD) //方法
- @Target(ElementType.PARAMETER) //方法参数
- @Target(ElementType.CONSTRUCTOR) //构造函数
- @Target(ElementType.LOCAL_VARIABLE)//局部变量
- @Target(ElementType.ANNOTATION_TYPE)//注解
- @Target(ElementType.PACKAGE) ///包
- Document(说明该注解将被保存在doc中)
- Inherited(说明子类可以继承父类中的该注解)
2.1.3 自定义注解
当jdk提供的注解无法满足我们自身的需求时就要考虑使用自定义注解,自定义注解通过元注解来标识注解的目标类型,保留策略等,注解的编写要遵守以下规则:
- Annotation 型定义为@interface, 所有的Annotation 会自动继承java.lang.Annotation这一接口,并且不能再去继承别的类或是接口
- 注解的成员参数只能是public和default,使用default关键字对属性值赋予默认值
- 当定义的注解中只有一个属性value的时候,在使用此注解时,对其value属性赋值可以不必明确写上value而是直接使用属性值即可。
- 当定义的注解中有value属性,同时也包含有其他属性时候,那么在对属性赋值时候,必须明确的以name = value的形式赋值。
- 如果注解中存在数组属性,那么在对其赋值的时候,如果是单个值,可不使用“{}”的形式,如果是多个值,必须使用“{}”。
2.2 注解处理器
2.2.1 什么是注解处理器?
注解处理器是Java1.5引入的工具,它提供了在程序编译期间扫描和处理注解的能力。我们可以借助自定义注解处理器去处理我们自己定义的注解。注解处理器通常是通过获取我们的注解信息,进行处理,生成java文件,这些java文件是在编译阶段前期生成的,无法被修改,最终和其他手动编写的文件一起被javac编译运行。
2.2.2 如何自定义注解处理器
- 继承AbstractProcessor并重写其中的方法,核心方法为process。AbstractProcessor定义如下:
public abstract class AbstractProcessor implements Processor {
protected ProcessingEnvironment processingEnv;
private boolean initialized = false;
protected AbstractProcessor() {
}
//获取通过注解@SupportedOptions设置的可支持的输入选项值(-A参数
public Set<String> getSupportedOptions() {
SupportedOptions var1 = (SupportedOptions)this.getClass().getAnnotation(SupportedOptions.class);
return var1 == null ? Collections.emptySet() : arrayToSet(var1.value());
}
/**
*指定注解处理器可解析的注解类型,结果元素可能是某一受支持注释类型的规范(完全限定)名称。它也可能 是“name.*”形
*式的名称,表示所有以name开头的注解
**/
public Set<String> getSupportedAnnotationTypes() {
SupportedAnnotationTypes var1 = (SupportedAnnotationTypes)this.getClass().getAnnotation(SupportedAnnotationTypes.class);
if (var1 == null) {
if (this.isInitialized()) {
this.processingEnv.getMessager().printMessage(Kind.WARNING, "No SupportedAnnotationTypes annotation found on " + this.getClass().getName() + ", returning an empty set.");
}
return Collections.emptySet();
} else {
return arrayToSet(var1.value());
}
}
/**
*获取支持的JDK版本,通常这里返回SourceVersion.latestSupported(),默认返回SourceVersion.RELEASE_6
* @return
*/
public SourceVersion getSupportedSourceVersion() {
SupportedSourceVersion var1 = (SupportedSourceVersion)this.getClass().getAnnotation(SupportedSourceVersion.class);
SourceVersion var2 = null;
if (var1 == null) {
var2 = SourceVersion.RELEASE_6;
if (this.isInitialized()) {
this.processingEnv.getMessager().printMessage(Kind.WARNING, "No SupportedSourceVersion annotation found on " + this.getClass().getName() + ", returning " + var2 + ".");
}
} else {
var2 = var1.value();
}
return var2;
}
//使用处理环境类初始化处理器类,将ProcessingEnvironment环境存入成员变量processingEnv中,可供子类使用。
public synchronized void init(ProcessingEnvironment var1) {
if (this.initialized) {
throw new IllegalStateException("Cannot call init more than once.");
} else {
Objects.requireNonNull(var1, "Tool provided null ProcessingEnvironment");
this.processingEnv = var1;
this.initialized = true;
}
}
//注解处理器核心方法,用于对先前的注解进行读取和处理,生成java文件
public abstract boolean process(Set<? extends TypeElement> var1, RoundEnvironment var2);
...
}
2.3 注解案例
2.3.1 BindView手写
/**
* @author Mr.m
* @date 2019/9/3
**/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface RequestAnnotation {
String path() default "";
String data();
int code() default -1;
}
2.3.2 BindView解析
相信大家一定用过或听过ButterKnife这款知名的老牌Android注解框架,通过BindView注解的方式避免的繁多的findViewById的操作,深受开发者好评。BindView注解定义如下:
/**
* Bind a field to the view for the specified ID. The view will automatically be cast to the field
* type.
* <pre><code>
* {@literal @}BindView(R.id.title) TextView title;
* </code></pre>
*/
@Retention(RUNTIME) @Target(FIELD)
public @interface BindView {
/** View ID to which the field will be bound. */
@IdRes int value();
}
可以看到BindView注解使用了两个元注解Retention和Target,在使用的同时你是否会想过为什么只用一个简短的注解就能实现这样的操作呢,框架是如何通过@BindView()进行id的绑定的呢?使用过的都知道每个界面都需要进行ButterKnife的绑定操作,不绑定的话@BindView()是不会生效的,那么我们就要从ButterKnife.bind()方法作为入口探讨了。
public static Unbinder bind(@NonNull Object target, @NonNull View source) {
Class<?> targetClass = target.getClass();
if (debug) Log.d(TAG, "Looking up binding for " + targetClass.getName());
//查找继承自Unbinde类的s构造函数
Constructor<? extends Unbinder> constructor = findBindingConstructorForClass(targetClass);
if (constructor == null) {
return Unbinder.EMPTY;
}
//noinspection TryWithIdenticalCatches Resolves to API 19+ only type.
try {
//通过构造函数生成继承自Unbinder的实例
return constructor.newInstance(target, source);
...
}
}
可以看到bind函数的主要工作就是查找一个继承自Unbinder类的构造函数对象,并通过这个构造函数对象生成一个实例。而主要方法就是 findBindingConstructorForClass(targetClass);
@Nullable @CheckResult @UiThread
private static Constructor<? extends Unbinder> findBindingConstructorForClass(Class<?> cls) {
Constructor<? extends Unbinder> bindingCtor = BINDINGS.get(cls);
if (bindingCtor != null || BINDINGS.containsKey(cls)) {
if (debug) Log.d(TAG, "HIT: Cached in binding map.");
return bindingCtor;
}
String clsName = cls.getName();
if (clsName.startsWith("android.") || clsName.startsWith("java.")
|| clsName.startsWith("androidx.")) {
if (debug) Log.d(TAG, "MISS: Reached framework class. Abandoning search.");
return null;
}
try {
Class<?> bindingClass = cls.getClassLoader().loadClass(clsName + "_ViewBinding");
//noinspection unchecked
bindingCtor = (Constructor<? extends Unbinder>) bindingClass.getConstructor(cls, View.class);
if (debug) Log.d(TAG, "HIT: Loaded binding class and constructor.");
} catch (ClassNotFoundException e) {
if (debug) Log.d(TAG, "Not found. Trying superclass " + cls.getSuperclass().getName());
bindingCtor = findBindingConstructorForClass(cls.getSuperclass());
} catch (NoSuchMethodException e) {
throw new RuntimeException("Unable to find binding constructor for " + clsName, e);
}
BINDINGS.put(cls, bindingCtor);
return bindingCtor;
}
方法首先从BINDINGS中查找cls对应的构造函数,若不为空则直接返回,经过查看得知BINDINGS为LinkedHashMap<Class<?>, Constructor<? extends Unbinder>>,维持了一个Constructor对象hash链表。若不存在则通过类加载器进行类的加载而类名为当前调用者类名加后缀_ViewBinding,这时候问题就来了,我们没有写过这个类,那么这里不会崩嘛。当然,既然这样写,那么这个时候这个类是定然存在的,至于这个类是如何凭空出现的,那就用到我们前面说的annotationProcessor和JavaPoet技术了,然后对注解进行解析处理,进而得到要生成的类的必要信息,然后根据这些信息动态生成对应的 java 类。首先我们看看生成的java类。
public class HomeCompanyInfoFragment_ViewBinding implements Unbinder {
private HomeCompanyInfoFragment target;
@UiThread
public HomeCompanyInfoFragment_ViewBinding(HomeCompanyInfoFragment target, View source) {
this.target = target;
target.mTvCompanyMemberCount = Utils.findRequiredViewAsType(source, R.id.company_member_count, "field 'mTvCompanyMemberCount'", TextView.class);
...
}
@Override
@CallSuper
public void unbind() {
HomeCompanyInfoFragment target = this.target;
if (target == null) throw new IllegalStateException("Bindings already cleared.");
this.target = null;
target.mTvCompanyMemberCount = null;
target.mTvDeviceCount = null;
target.mTvAdminCount = null;
target.mAdminLayout = null;
}
}
很明显,构造函数里借助Utils.findRequireViewAsType进行了id绑定操作,而这里的id必然是通过解析BindView注解获得的信息。正如我们之前说过的,注解只是一个标签,传递一些基本信息。没有具体的处理过程,信息是不会产生价值的。而具体的处理过程就是解析注解,获得注解信息,并根据信息作出后续处理。如此一来问题就集中在了BindView注解的解析工作上,负责这项工作的则是ButterKnifeProcesser
private boolean collectBindViewAnnotations(RoundEnvironment roundEnvironment){
//查找所有添加了注解BindView的元素
Set<? extends Element> elements = roundEnvironment.getElementsAnnotatedWith(BindView.class);
if(elements == null || elements.isEmpty()){
return false;
}
for(Element element : elements){
//注解BindView必须添加在属性上
if(element.getKind() != ElementKind.FIELD){
error(element, "只有类的属性可以添加@%s注解", BindView.class.getCanonicalName());
return false;
}
//获取注解的值
int viewId = element.getAnnotation(BindView.class).value();
//这个元素是属性类型的元素
VariableElement viewElement = (VariableElement) element;
//获取直接包含属性元素的元素,即类元素
TypeElement typeElement = (TypeElement) viewElement.getEnclosingElement();
//将类型元素作为key,保存到bindMap暂存
List<ViewBindInfo> viewBindInfoList = bindMap.get(typeElement);
if(viewBindInfoList == null){
viewBindInfoList = new ArrayList<>();
bindMap.put(typeElement, viewBindInfoList);
}
info("注解信息:viewId=%d, name=%s, type=%s", viewId, viewElement.getSimpleName().toString(), viewElement.asType().toString());
viewBindInfoList.add(new ViewBindInfo(viewId, viewElement.getSimpleName().toString(), viewElement.asType()));
}
return true;
}