探索Android开源框架 - 6. ButterKnife使用及源码解析
相关知识ButterKnife中最重要的就是先自定义注解,再通过APT在编译期解析注解,解析器中又会用到反射,然后通过javapoet库来生成模板代码,要想学习其源码,首先要基本的了解注解和反射的知识;注解Java 注解(Annotation)又称 Java 标注,是 JDK5.0 引入的一种注释机制;Java中的类、方法、变量、参数和包等都可以被标注,Java 标注可以通过反射获取标注内容;在编
·
相关知识
- ButterKnife中最重要的就是先自定义注解,再通过APT在编译期解析注解,解析器中又会用到反射,然后通过javapoet库来生成模板代码,
要想学习其源码,首先要基本的了解注解和反射的知识;
注解
- Java 注解(Annotation)又称 Java 标注,是 JDK5.0 引入的一种注释机制;
- Java中的类、方法、变量、参数和包等都可以被标注,Java 标注可以通过反射获取标注内容;
在编译器生成类文件时,标注可以被嵌入到字节码中,Java 虚拟机可以保留标注内容,在运行时可以获取到标注内容; - 注解以@注解名的形式存在于代码中,也支持自定义 Java 注解;
- 注解相当于一种标记,在程序中加了注解就等于为程序打上了某种标记。程序可以利用ava的反射机制来了解你的类及各种元素上有无何种标记,针对不同的标记,就去做相应的事件。
内置注解
普通注解
- 作用在代码的注解,在java.lang中
@Override
- 检查该方法是否是重写方法。如果发现其父类,或者是引用的接口中并没有该方法时,会报编译错误。
@Deprecated
- 标记表示过时的,不推荐使用。可以用于修饰方法,属性,类。如果使用被此注解修饰的方法,属性或类,会报编译警告。
@SuppressWarnings
- 指示编译器去忽略注解中声明的警告
新增的普通注解
@SafeVarargs
- Java7开始支持,忽略任何使用参数为泛型变量的方法或构造函数调用产生的警告;
@FunctionalInterface
- Java8开始支持,标识一个匿名函数或函数式接口;
@Repeatable
- Java8开始支持,标识某注解可以在同一个声明上使用多次;
元注解
- 作用在其他注解的注解,在java.lang.annotation中
@Retention
- 标识这个注解怎么保存,是只在代码中,还是编入class文件中,或者是在运行时可以通过反射访问
- 其可选策略如下:
- RetentionPolicy.SOURCE: 注解仅存在于源码中,在class字节码文件中不包含;
- RetentionPolicy.CLASS: 默认的保留策略,注解会在class字节码文件中存在,但运行时无法获得;
- RetentionPolicy.RUNTIME: 注解会在class字节码文件中存在,在运行时可以通过反射获取到;
- 如果自定义注解,只存在于源码或字节码中无法发挥作用,在运行期间能获取到注解才能实现我们目的,所以自定义注解中是使用 @Retention(RetentionPolicy.RUNTIME);
@Target
- 标记这个注解作用的范围
- 其可选类型如下
- ElementType.TYPE: 接口、类、枚举、注解
- ElementType.FIELD:属性字段、枚举的常量
- ElementType.METHOD:方法
- ElementType.PARAMETER: 方法参数
- ElementType.CONSTRUCTOR: 构造函数
- ElementType.LOCAL_VARIABLE: 局部变量
- ElementType.ANNOTATION_TYPE: 注解(@Retention注解中就使用该属性)
- ElementType.PACKAGE: 包
- ElementType.TYPE_PARAMETER:类型泛型,即泛型方法、泛型类、泛型接口 (jdk1.8加入)
- ElementType.TYPE_USE:类型使用.可以用于标注任意类型除了 class (jdk1.8加入)
@Documented
- 标记这些注解是否包含在用户文档中
@Inherited
- 如果一个类用上了@Inherited修饰的注解,那么其子类也会继承这个注解
自定义注解
- 创建自定义注解和创建一个接口相似,但是注解的interface关键字需要以@符号开头,可以为注解声明方法,示例如下
//创建:
@Documented // 此注解包含在用户文档中
@Target(ElementType.METHOD)//此注解只能用在方法上
@Inherited//子类可以继承父类中的此注解
@Retention(RetentionPolicy.RUNTIME)// 此注解保存在运行时,可以通过反射访问
public @interface MyAnnotation {
//注解方法不能带有参数,可以有默认值,
//返回值类型限定为:基本类型、String、Enums、Annotation或者是这些类型的数组
String name() ;//没有默认值,使用时需要显示赋值
int age() default -1;//有默认值,使用时可以不赋值
}
//使用:
@MyAnnotation(name = "ljy",age = 18)
private fun initView() {
}
实战:使用自定义注解+AOP实现打印方法耗时
- 上面说了自定义注解如何定义,那么我们来结合实际业务需求写一个实战的例子:打印方法耗时,当然也可以用来做埋点统计
- 创建自定义注解
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface GetTime {
String tag() default "";
}
- 通过AOP解析注解,我这里用的aspectj框架,关于AOP并不是本篇相关的技术就不做展开说明了;
@Around("execution(@GetTime * *(..))")
public void getTime(ProceedingJoinPoint joinPoint) throws Throwable {
MethodSignature joinPointObject = (MethodSignature) joinPoint.getSignature();
Method method = joinPointObject.getMethod();
boolean flag = method.isAnnotationPresent(GetTime.class);
LjyLogUtil.d("flag:"+flag);
String tag = null;
if (flag) {
GetTime getTime = method.getAnnotation(GetTime.class);
tag = getTime.tag();
}
if (TextUtils.isEmpty(tag)) {
Signature signature = joinPoint.getSignature();
tag = signature.toShortString();
}
long time = System.currentTimeMillis();
try {
joinPoint.proceed();
} catch (Throwable throwable) {
throwable.printStackTrace();
}
LjyLogUtil.d( tag+" get time: " + (System.currentTimeMillis() - time));
}
- 使用注解:给需要统计的方法添加注解,例如Application.onCreate方法里面经常会做许多初始化操作,我们可能很关系它的耗时过长,影响应用的启动时长
@GetTime(tag = "MyApplication.onCreate() 耗时")
@Override
public void onCreate() {
super.onCreate();
ProcessLifecycleOwner.get().getLifecycle().addObserver(new ApplicationLifecycleObserver());
MMKV.initialize(this);
GreenDaoHelper.getInstance().init(getApplicationContext());
ARouter.init(MyApplication.this);
// Application的onCreate方法中开启卡顿监控
BlockCanary.install(this, new AppBlockCanaryContext()).start();
//Application的onCreate方法中初始化ANR-WatchDog
new ANRWatchDog().start();
NetworkListener.getInstance().init(this);
}
反射
- 在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意一个方法和属性;
- 反射就是把java类中的各种成分映射成一个个的Java对象;
- 反射有两个作用:1.反编译:.class->.java; 2.通过反射机制访问java对象中的属性,方法,构造器等;
- 实现反射,实际上是得到Class对象
常用类
java.lang.Class //类的创建
java.lang.reflect.Constructor //反射构造方法
java.lang.reflect.Field //反射属性
java.lang.reflect.Method //反射方法
java.lang.reflect.Modifier //访问修饰符的信息
获取类(Class)对象
- 三种获取Class对象的方法
//1.会让ClassLoader装载类,并进行类的初始化
val c1 = Class.forName("com.ljy.demo.Person")
//2.会让ClassLoader装载类,不进行类的初始化操作
val c2 = Person::class.java
//3.在实例中获取,返回类对象运行时真正所指的对象
val c3 = Person("bob").javaClass
反射构造方法
- 无参数创建对象
val person = c1.newInstance() as Person
LjyLogUtil.d(person.toString())
//new Person()是直接创建一个实列,同时完成类的装载和连接
//newInstance是使用类加载机制,可以灵活的创建类的实例,更换类的时候无需修改之前的代码
- 有参数的创建对象: 使用Constructor
val constructor = c1.getConstructor(String::class.java)
val person2 = constructor.newInstance("Emily") as Person
LjyLogUtil.d(person2.toString())
- 遍历构造器
for (it in c1.declaredConstructors) {
LjyLogUtil.d("declaredConstructors.it:")
for (i in it.parameterTypes) {
LjyLogUtil.d("it.parameterTypes.i:${i.name}")
}
}
反射属性
- 获取属性(Field)
val fieldAge = c1.getDeclaredField("age")
val fieldName = c1.getDeclaredField("name")
- 遍历属性
for (it in c1.declaredFields) {
LjyLogUtil.d("declaredFields.it:${it.name}")
}
- 修改属性
//取消封装,特别是取消私有字段的访问限制,并不是将方法的权限设置为public,而是取消java的权限控制检查,
// 所以即使是public方法,其isAccessible默认也是false
fieldAge.isAccessible = true
fieldName.isAccessible = true
fieldAge.set(person2, 18)
LjyLogUtil.d("fieldAge.name=${fieldAge.name}")
反射方法
- 获取方法
val method = c1.getDeclaredMethod("setAge", Int::class.java)//获取类中的方法
method.invoke(person2, 22)//通过反射调用方法
LjyLogUtil.d(person2.toString())
- 遍历方法
for (it in c1.declaredMethods) {
LjyLogUtil.d("declaredMethods.it:${it.name}")
}
反射静态方法
val clz = Class.forName("com.ljy.publicdemo.util.LjyLogUtil")
val m = clz.getDeclaredMethod("d", CharSequence::class.java)
m.invoke(LjyLogUtil::class.java, "log.d")
反射泛型参数方法
//class Test<T> {
// public void test(T t){
// LjyLogUtil.d("Test.test(),t:"+t);
// }
//}
val clz2 = Class.forName("com.ljy.publicdemo.activity.Test")
//注意这里有个泛型的基础--泛型擦除,编译器会自动类型向上转型,T向上转型是Object,所以下面第二个参数是Object.class
val tm = clz2.getDeclaredMethod("test", Object::class.java)
tm.isAccessible = true
tm.invoke(Test<Int>(), 666)
访问修饰符的信息
val modifierField = Modifier.toString(fieldName.modifiers)
val modifierMethod = Modifier.toString(method.modifiers)
LjyLogUtil.d("modifierField=$modifierField, modifierMethod=$modifierMethod")
实践:通过反射获取运行时注解
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface GetTime {
String tag() default "";
}
public class Test {
@GetTime(tag = "做啥子")
private static void doSomething(String info) {
//todo...
}
public static void main(String[] args) {
Test test = new Test();
GetTime getTime ;
Class clazz = test.getClass();
try {
Method method = clazz.getMethod("doSomething", String.class);
getTime = method.getAnnotation(GetTime.class);
System.out.print("needLogin:"+getTime.tag());
} catch (NoSuchMethodException e) {
System.out.print("NoSuchMethod");
}
}
}
实践:通过反射越过泛型检查
- 泛型用在编译期,编译过后泛型擦除(消失掉)。所以是可以通过反射越过泛型检查的
public static void main(String[] args) throws Exception{
ArrayList<String> strList = new ArrayList<>();
strList.add("aaa");
strList.add("bbb");
// strList.add(100);
//获取ArrayList的Class对象,反向的调用add()方法,添加数据
Class listClass = strList.getClass(); //得到 strList 对象的字节码 对象
//获取add()方法
Method m = listClass.getMethod("add", Object.class);
//调用add()方法
m.invoke(strList, 100);
//遍历集合
for(Object obj : strList){
System.out.println(obj);
}
}
实践:通过反射运行配置文件内容
- 我们利用反射和配置文件,可以使:应用程序更新时,对源码无需进行任何修改,我们只需要将新类发送给客户端,并修改配置文件即可
//student类:
public class Student {
public void show(){
System.out.println("is show()");
}
}
// 配置文件以txt文件为例子(pro.txt):
className = cm.ljy.Student
methodName = show
//测试类
public class Demo {
public static void main(String[] args) throws Exception {
//通过反射获取Class对象
Class stuClass = Class.forName(getValue("className"));//"cn.fanshe.Student"
//2获取show()方法
Method m = stuClass.getMethod(getValue("methodName"));//show
//3.调用show()方法
m.invoke(stuClass.getConstructor().newInstance());
}
//此方法接收一个key,在配置文件中获取相应的value
public static String getValue(String key) throws IOException{
Properties pro = new Properties();//获取配置文件的对象
FileReader in = new FileReader("pro.txt");//获取输入流
pro.load(in);//将流加载到配置文件对象中
in.close();
return pro.getProperty(key);//返回根据key获取的value值
}
}
实践:动态代理
- java的反射机制提供了动态代理模式实现,代理模式的作用是为其他对象提供一种代理,以控制对这个对象的访问
interface Subject {
fun doSomething()
}
class Test : Subject {
override fun doSomething() {
LjyLogUtil.d("Test.doSomething")
}
}
class DynamicProxy(private val target: Subject) : InvocationHandler {
override fun invoke(proxy: Any?, method: Method?, args: Array<out Any>?): Any? {
LjyLogUtil.d("Proxy:${proxy?.javaClass?.name}")
LjyLogUtil.d("before target")
//Kotlin中数组转为可变长参数,通过前面加*符号
val invoke = method!!.invoke(target, *(args ?: emptyArray()))
LjyLogUtil.d("after target")
return invoke
}
}
//使用:
val test = Test()
val myProxy = DynamicProxy(test)
val subject: Subject =
Proxy.newProxyInstance(
test.javaClass.classLoader,
test.javaClass.interfaces,
myProxy
) as Subject
subject.doSomething()
LjyLogUtil.d("subject.className:" + subject.javaClass.name)
实践:动态创建fragment
//管理fragment标题及路径的类
public class PageConfig {
public static List<String> pageTitles = new ArrayList<String>();
public static List<String> getPageTitles(Context context) {
pageTitles.clear();
pageTitles.add("Fragment1");
pageTitles.add("Fragment2");
pageTitles.add("Fragment3");
return pageTitles;
}
private static final String PATH_FRAGMENT1 = "com.ljy.publicdemo.activity.fragment.Fragment1";
private static final String PATH_FRAGMENT2 = "com.ljy.publicdemo.activity.fragment.Fragment2";
private static final String PATH_FRAGMENT3 = "com.ljy.publicdemo.activity.fragment.Fragment3";
public static String[] fragmentNames = {
PATH_FRAGMENT1,
PATH_FRAGMENT2,
PATH_FRAGMENT3,
};
}
//通过反射遍历添加
class FragmentActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_frag)
try {
//遍历Fragment地址
for (address in PageConfig.fragmentNames) {
//反射获得Class
val clazz = Class.forName(address)
//创建类
val fragment = clazz.newInstance() as Fragment
//添加到viewPagerAdapter的资源
supportFragmentManager
.beginTransaction()
.add(R.id.frame_layout, fragment)
.commit()
}
} catch (e: ClassNotFoundException) {
} catch (e: IllegalAccessException) {
} catch (e: InstantiationException) {
}
}
}
反射简化: jOOR
- 当预期工程非常多的使用到反射时,我们需要更加简化的工具来优化开发流程,一个很棒的反射框架jOOR,非常轻量,让代码更加优雅
- 项目地址 https://github.com/jOOQ/jOOR
//1. 添加依赖
implementation 'org.jooq:joor:0.9.13'
//2. 使用
val helloStr: String? = Reflect.onClass("java.lang.String")//类似于Class.forName
.create("Hello jOOR")//调用类中构造方法
.call("substring", 8)//调用类中方法
.call("toString")
.get()//获取包装好的对象
LjyLogUtil.d("helloStr=$helloStr")
//3. 也支持动态代理
Reflect.onClass(RealSubject::class.java).create().`as`(Subject::class.java).doSomething()
注意
- 由于反射会额外消耗一定的系统资源,因此如果不需要动态地创建一个对象,那么就不需要用反射;
- jvm无法对反射部分的代码进行优化,反射生成大量的临时对象,造成大量GC,导致UI卡顿;
- 另外,反射调用方法时可以忽略权限检查,因此可能会破坏封装性而导致安全问题;
ButterKnife使用
- ButterKnife使用还是非常简单的,示例如下
class ButterKnifeActivity : AppCompatActivity() {
@BindView(R.id.et_name)
lateinit var etName: EditText
@BindView(R.id.et_pwd)
lateinit var etPwd: EditText
@BindString(R.string.login_error)
lateinit var loginErrorMessage: String
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_butter_knife)
ButterKnife.bind(this)
}
@OnClick(R.id.btn_submit)
fun submit() {
LjyLogUtil.d("etName=${etName.text}")
LjyLogUtil.d("etPwd=${etPwd.text}")
if ("ljy".equals(etName.text) && "123".equals(etPwd.text)) {
LjyLogUtil.d("登录成功")
} else {
LjyLogUtil.d(loginErrorMessage)
}
}
}
ButterKnife源码解析
- 主要有下面两步:
- 编译的时候通过APT扫描注解,并进行相应处理,通过 javapoet库生成Java代码;
- 调研ButterKnife.bind(this)方法时通过反射将id与相应的上下文绑定;
ButterKnifeProcessor类:通过APT扫描解析注解并生成代码
- ButterKnife的APT注解解析类是ButterKnifeProcessor,继承了AbstractProcessor,我们先来看看他是如何实现的
init方法
- 重写init方法初始化工具类, 注意该方法添加了synchronized关键字
@Override public synchronized void init(ProcessingEnvironment env) {
super.init(env);
String sdk = env.getOptions().get(OPTION_SDK_INT);
。。。
debuggable = !"false".equals(env.getOptions().get(OPTION_DEBUGGABLE));
typeUtils = env.getTypeUtils();
filer = env.getFiler();
。。。
}
getSupportedAnnotationTypes方法
- 该方法返回支持的注解类型,用于指定定义的注解
@Override public Set<String> getSupportedAnnotationTypes() {
Set<String> types = new LinkedHashSet<>();
for (Class<? extends Annotation> annotation : getSupportedAnnotations()) {
types.add(annotation.getCanonicalName());
}
return types;
}
//指定定义的注解,注册了一系列的Bindxxx注解类和监听列表LISTENERS
private Set<Class<? extends Annotation>> getSupportedAnnotations() {
Set<Class<? extends Annotation>> annotations = new LinkedHashSet<>();
annotations.add(BindAnim.class);
annotations.add(BindArray.class);
annotations.add(BindBitmap.class);
annotations.add(BindBool.class);
annotations.add(BindColor.class);
annotations.add(BindDimen.class);
annotations.add(BindDrawable.class);
annotations.add(BindFloat.class);
annotations.add(BindFont.class);
annotations.add(BindInt.class);
annotations.add(BindString.class);
annotations.add(BindView.class);
annotations.add(BindViews.class);
annotations.addAll(LISTENERS);
return annotations;
}
private static final List<Class<? extends Annotation>> LISTENERS = Arrays.asList(
OnCheckedChanged.class, //
OnClick.class, //
OnEditorAction.class, //
OnFocusChange.class, //
OnItemClick.class, //
OnItemLongClick.class, //
OnItemSelected.class, //
OnLongClick.class, //
OnPageChange.class, //
OnTextChanged.class, //
OnTouch.class //
);
process方法
- 最核心的方法, 注解的处理和生成代码都是在这个方法中完成
@Override public boolean process(Set<? extends TypeElement> elements, RoundEnvironment env) {
//查询并解析注解,保存到bindingMap中
Map<TypeElement, BindingSet> bindingMap = findAndParseTargets(env);
for (Map.Entry<TypeElement, BindingSet> entry : bindingMap.entrySet()) {
TypeElement typeElement = entry.getKey();
BindingSet binding = entry.getValue();
JavaFile javaFile = binding.brewJava(sdk, debuggable);
try {
//通过javaFile.writeTo(filer)生成java源文件
javaFile.writeTo(filer);
} catch (IOException e) {
error(typeElement, "Unable to write binding for type %s: %s", typeElement, e.getMessage());
}
}
return false;
}
findAndParseTargets方法
- 上面的process第一行就调用了findAndParseTargets方法,他主要是用来扫描注解,并将注解信息保存到map
private Map<TypeElement, BindingSet> findAndParseTargets(RoundEnvironment env) {
Map<TypeElement, BindingSet.Builder> builderMap = new LinkedHashMap<>();
Set<TypeElement> erasedTargetNames = new LinkedHashSet<>();
// 扫描@BindAnim注解并调用parseResourceAnimation方法解析处理,保存到上面两个集合中
for (Element element : env.getElementsAnnotatedWith(BindAnim.class)) {
if (!SuperficialValidation.validateElement(element)) continue;
try {
parseResourceAnimation(element, builderMap, erasedTargetNames);
} catch (Exception e) {
logParsingError(element, BindAnim.class, e);
}
}
。。。//省略了其他的一系列注解扫描
//同样的扫描并解析监听注解,保存到集合中
for (Class<? extends Annotation> listener : LISTENERS) {
findAndParseListener(env, listener, builderMap, erasedTargetNames);
}
Map<TypeElement, ClasspathBindingSet> classpathBindings =
findAllSupertypeBindings(builderMap, erasedTargetNames);
// 创建一个双向队列entries,放入builderMap.entrySet()
Deque<Map.Entry<TypeElement, BindingSet.Builder>> entries =
new ArrayDeque<>(builderMap.entrySet());
//
Map<TypeElement, BindingSet> bindingMap = new LinkedHashMap<>();
//遍历entries中每个
while (!entries.isEmpty()) {
Map.Entry<TypeElement, BindingSet.Builder> entry = entries.removeFirst();
TypeElement type = entry.getKey();
BindingSet.Builder builder = entry.getValue();
TypeElement parentType = findParentType(type, erasedTargetNames, classpathBindings.keySet());
//判断是否有父类
if (parentType == null) {
bindingMap.put(type, builder.build());
} else {
BindingInformationProvider parentBinding = bindingMap.get(parentType);
if (parentBinding == null) {
parentBinding = classpathBindings.get(parentType);
}
if (parentBinding != null) {
builder.setParent(parentBinding);
bindingMap.put(type, builder.build());
} else {
// Has a superclass binding but we haven't built it yet. Re-enqueue for later.
entries.addLast(entry);
}
}
}
return bindingMap;
}
BindingSet.brewJava方法
- 再回到process方法,接下来的循环中,通过brewJava返回javapoet的JavaFile对象,用来生成java文件,brewJava代码如下
JavaFile brewJava(int sdk, boolean debuggable) {
TypeSpec bindingConfiguration = createType(sdk, debuggable);
//通过javapoet的builder构造器将上面得到的bindingConfiguration对象构建生成一个JavaFile对象
return JavaFile.builder(bindingClassName.packageName(), bindingConfiguration)
.addFileComment("Generated code from Butter Knife. Do not modify!")
.build();
}
BindingSet.createType方法
- createType中使用javapoet库生成TypeSpec对象,里面保存各种的绑定配置信息
private TypeSpec createType(int sdk, boolean debuggable) {
TypeSpec.Builder result = TypeSpec.classBuilder(bindingClassName.simpleName())
.addModifiers(PUBLIC)
.addOriginatingElement(enclosingElement);
if (isFinal) {
result.addModifiers(FINAL);
}
if (parentBinding != null) {
result.superclass(parentBinding.getBindingClassName());
} else {
result.addSuperinterface(UNBINDER);
}
if (hasTargetField()) {
result.addField(targetTypeName, "target", PRIVATE);
}
if (isView) {
result.addMethod(createBindingConstructorForView());
} else if (isActivity) {
result.addMethod(createBindingConstructorForActivity());
} else if (isDialog) {
result.addMethod(createBindingConstructorForDialog());
}
if (!constructorNeedsView()) {
result.addMethod(createBindingViewDelegateConstructor());
}
result.addMethod(createBindingConstructor(sdk, debuggable));
if (hasViewBindings() || parentBinding == null) {
result.addMethod(createBindingUnbindMethod(result));
}
return result.build();
}
ButterKnife类:通过反射调用APT生成的java文件的方法,完成绑定
1. bind
- 代码如下:
@NonNull @UiThread
public static Unbinder bind(@NonNull Activity target) {
//sourceView:当前界面的顶级父View
View sourceView = target.getWindow().getDecorView();
return bind(target, sourceView);
}
@NonNull @UiThread
public static Unbinder bind(@NonNull View target) {
return bind(target, target);
}
@NonNull @UiThread
public static Unbinder bind(@NonNull Dialog target) {
View sourceView = target.getWindow().getDecorView();
return bind(target, sourceView);
}
2. createBinding
- 可以看到单参的bind有几个重载方法,但最终都是调用了两个参数的bind方法,其实这个方法早期版本的名字可能更容易理解:createBinding,代码如下:
public static Unbinder bind(@NonNull Object target, @NonNull View source) {
Class<?> targetClass = target.getClass();
//这里调用findBindingConstructorForClass获取APT生成文件的构造器
Constructor<? extends Unbinder> constructor = findBindingConstructorForClass(targetClass);
//判空
if (constructor == null) {
return Unbinder.EMPTY;
}
//通过反射创建其对象,也就是createBinding
try {
return constructor.newInstance(target, source);
} catch 。。。//省略异常处理代码
}
3. findBindingConstructorForClass
- 再看一下上面代码中调用的findBindingConstructorForClass方法,代码如下:
static final Map<Class<?>, Constructor<? extends Unbinder>> BINDINGS = new LinkedHashMap<>();
@Nullable @CheckResult @UiThread
private static Constructor<? extends Unbinder> findBindingConstructorForClass(Class<?> cls) {
//1. 先判断binding map中有没有缓存过cls,有则直接取用
Constructor<? extends Unbinder> bindingCtor = BINDINGS.get(cls);
if (bindingCtor != null || BINDINGS.containsKey(cls)) {
return bindingCtor;
}
//2. 判断类路径是否以android.或androidx.或java.开头,是则返回null
String clsName = cls.getName();
if (clsName.startsWith("android.") || clsName.startsWith("java.")
|| clsName.startsWith("androidx.")) {
return null;
}
try {
//使用classLoader加载cls对应的绑定类(APT生成的),获取其class对象
Class<?> bindingClass = cls.getClassLoader().loadClass(clsName + "_ViewBinding");
//通过反射调用其构造器创建绑定类的对象
bindingCtor = (Constructor<? extends Unbinder>) bindingClass.getConstructor(cls, View.class);
} catch (ClassNotFoundException e) {
//没找到则递归调用找其父类的
bindingCtor = findBindingConstructorForClass(cls.getSuperclass());
} catch (NoSuchMethodException e) {
//没有找到构造函数则抛出异常
throw new RuntimeException("Unable to find binding constructor for " + clsName, e);
}
BINDINGS.put(cls, bindingCtor);
return bindingCtor;
}
番外:使用APT自定义注解处理器
- 即Annotation Processing Tool,编译时注解处理器, APT可以用来在编译时扫描和处理注解
- APT技术被广泛的运用在Java框架中,ButterKnife,EventBus,Dagger2以及ARouter等都运用到APT;
- 新版的ButterKnife是使用APT编译时注解处理器在编译时扫描注解,并做相应处理,生成java代码,生成Java代码是调用javapoet库生成的;
- 那么让我们尝试使用APT实现一个最简版的ButterKnife吧
使用流程
1. 新建Java Module(我的命名是"annotation")用于存放自定义注解,gradle配置文件如下:
apply plugin: 'java-library'
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
}
sourceCompatibility = "1.8"
targetCompatibility = "1.8"
- 如果创建Android Library模块会发现不能找到AbstractProcessor这个类,这是因为Android平台是基于OpenJDK的,
而OpenJDK中不包含APT的相关代码。因此,在使用APT时,必须在Java Library中进行,如果不知道如何创建,直接修改为上面的配置即可;
2. 新建Java Module(我的命名是"processor")用于存放自定义apt注解处理器,并依赖annotation module, gradle配置文件如下:
apply plugin: 'java-library'
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
//AutoService是Google开发的一个库,用来生成META-INF/services/javax.annotation.processing.Processor文件
implementation 'com.google.auto.service:auto-service:1.0-rc6'
annotationProcessor 'com.google.auto.service:auto-service:1.0-rc6'
//square的自动生成代码库
implementation 'com.squareup:javapoet:1.11.1'
implementation project(path: ':annotation', configuration:'default')
}
sourceCompatibility = "1.8"
targetCompatibility = "1.8"
3. 将前两部创建的module添加到主工程app module下
//导入自定义注解
implementation project(path: ':annotation')
// 指定自定义注释处理器 ,如果使用kotlin, replace annotationProcessor with kapt
kapt project(path: ':processor')
4. 自定义注解
@Target(ElementType.FIELD) // 作用于成员属性上
@Retention(RetentionPolicy.CLASS)
public @interface BindView {
int value();
}
5. Element元素
- Java中Element是一个接口,表示一个程序元素,它可以指代包、类、方法或者一个变量
PackageElement: 表示一个包程序元素。提供对有关包及其成员的信息的访问。
ExecutableElement: 表示某个类或接口的方法、构造方法或初始化程序(静态或实例),包括注释类型元素。
TypeElement: 表示一个类或接口程序元素。提供对有关类型及其成员的信息的访问。注意,枚举类型是一种类,而注解类型是一种接口。
VariableElement: 表示一个字段、enum 常量、方法或构造方法参数、局部变量或异常参数。
- 自定义一个ElementInfo类,用于存储解析Element元素的信息
internal class ElementInfo(
/**
* 包路径
*/
var packageName: String,
/**
* 节点所在类名称
*/
var className: String,
/**
* 节点类型名称
*/
var typeName: String,
/**
* 节点名称
*/
var nodeName: String,
/**
* 注解的value
*/
var value: Int
)
6. 自定义注解处理器,需继承AbstractProcessor
/**
* @Author: LiuJinYang
* @CreateDate: 2021/11/9
* AutoService注解作用是用来生成META-INF/services/javax.annotation.processing.Processor文件的,
*/
@AutoService(Processor.class)
public class MyProcessor extends AbstractProcessor {
private Types mTypeUtils;
private Messager mMessagerUtils;
private Filer mFilerUtils;
private Elements mElementUtils;
/**
* 节点信息缓存
*/
private Map<String, List<ElementInfo>> mCache = new HashMap<>();
/**
* 用于初始化处理器,可以通过ProcessingEnvironment来获取一些帮助我们来处理注解的工具类
*/
@Override
public synchronized void init(ProcessingEnvironment processingEnv) {
super.init(processingEnv);
//类信息工具类
mTypeUtils = processingEnv.getTypeUtils();
// Messager可以用来打印错误信息;
mMessagerUtils = processingEnv.getMessager();
// Filer可以用来编写新文件;
mFilerUtils = processingEnv.getFiler();
// Elements是一个可以处理Element的工具类。
mElementUtils = processingEnv.getElementUtils();
}
/**
* 只有一个返回值,用来指定当前正在使用的Java版本,
* 通常return SourceVersion.latestSupported()即可
*/
@Override
public SourceVersion getSupportedSourceVersion() {
return SourceVersion.latestSupported();
// return super.getSupportedSourceVersion();
}
/**
* 指明有哪些注解需要被扫描到,返回注解的全路径(包名+类名)
*/
@Override
public Set<String> getSupportedAnnotationTypes() {
Set<String> types = new LinkedHashSet<>();
for (Class<? extends Annotation> annotation : getSupportedAnnotations()) {
types.add(annotation.getCanonicalName());
}
return types;
// return super.getSupportedAnnotationTypes();
}
private Set<Class<? extends Annotation>> getSupportedAnnotations() {
Set<Class<? extends Annotation>> annotations = new LinkedHashSet<>();
annotations.add(LjyBindView.class);
annotations.add(ClickGap.class);
annotations.add(GetTime.class);
annotations.add(NeedLogin.class);
return annotations;
}
/**
* 核心方法,注解的处理和生成代码都是在这个方法中完成
*/
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
if (annotations == null || annotations.isEmpty()) {
return false;
}
//扫描所有被@GetTime注解的元素
Set<? extends Element> elements = roundEnv.getElementsAnnotatedWith(LjyBindView.class);
if (elements == null || elements.isEmpty()) {
return false;
}
//遍历被@GetTime注解的元素
for (Element element : elements) {
if (element.getKind() != ElementKind.FIELD) {
throw new RuntimeException("Only classes can be annotated with " + element.getSimpleName());
}
// 获取节点包信息
String packageName = mElementUtils.getPackageOf(element).getQualifiedName().toString();
// 获取节点类信息,由于 @NeedLogin 作用方法上,所以这里使用 getEnclosingElement() 获取父节点信息
String className = element.getEnclosingElement().getSimpleName().toString();
// 获取节点类型
String typeName = element.asType().toString();
// 获取节点标记的属性名称
String nodeName = element.getSimpleName().toString();
// 获取注解的值
int value = element.getAnnotation(LjyBindView.class).value();
// 打印
mMessagerUtils.printMessage(Diagnostic.Kind.NOTE, "packageName:" + packageName);
mMessagerUtils.printMessage(Diagnostic.Kind.NOTE, "className:" + className);
mMessagerUtils.printMessage(Diagnostic.Kind.NOTE, "typeName:" + typeName);
mMessagerUtils.printMessage(Diagnostic.Kind.NOTE, "nodeName:" + nodeName);
mMessagerUtils.printMessage(Diagnostic.Kind.NOTE, "value:" + value);
// 缓存KEY
String key = packageName + "." + className;
// 缓存节点信息
List<ElementInfo> nodeInfos = mCache.get(key);
if (nodeInfos == null) {
nodeInfos = new ArrayList<>();
nodeInfos.add(new ElementInfo(packageName, className, typeName, nodeName, value));
// 缓存
mCache.put(key, nodeInfos);
} else {
nodeInfos.add(new ElementInfo(packageName, className, typeName, nodeName, value));
}
}
// 判断临时缓存是否不为空
if (!mCache.isEmpty()) {
// 遍历临时缓存文件
for (Map.Entry<String, List<ElementInfo>> stringListEntry : mCache.entrySet()) {
try {
// 创建文件
createFile(stringListEntry.getValue());
} catch (Exception e) {
e.printStackTrace();
}
}
}
return false;
}
/**
* 创建文件,自动生成代码
* javapoet的具体使用教程请参考javapoet官方文档
*/
private void createFile(List<ElementInfo> infos) throws IOException {
ElementInfo info = infos.get(0);
// 生成的文件名(类名)
String className = info.getClassName() + "__ViewBinding";
// 方法参数
ParameterSpec parameterSpec = ParameterSpec.builder(
ClassName.get(info.getPackageName(), info.getClassName()), "target")
.build();
// 方法
MethodSpec.Builder methodSpecBuilder = MethodSpec.methodBuilder("bind")
.addModifiers(Modifier.PUBLIC, Modifier.STATIC)
.addParameter(parameterSpec)
.returns(void.class);
// 给方法添加代码块
for (ElementInfo nodeInfo : infos) {
// target.textView = (TextView) target.findViewByID(R.id.text_view);
methodSpecBuilder.addStatement("target.$L = ($L)target.findViewById($L)",
nodeInfo.getNodeName(),
nodeInfo.getTypeName(),
nodeInfo.getValue());
}
// 类
TypeSpec typeSpec = TypeSpec.classBuilder(className)
.addModifiers(Modifier.PUBLIC)
.addMethod(methodSpecBuilder.build())
.build();
// 生成文件
JavaFile.builder(info.getPackageName(), typeSpec)
.build()
.writeTo(mFilerUtils);
}
}
- reBuild项目之后,就可以|在项目的app\build\generated\source\kapt\debug\包名 路径下找到自动生成的文件ButterKnifeActivity__ViewBinding,其内容如下:
public class ButterKnifeActivity__ViewBinding {
public static void bind(ButterKnifeActivity target) {
target.etName = (android.widget.EditText)target.findViewById(2131296530);
}
}
7. 自定义LjyButterKnife类
- 然后就可以仿照ButterKnife在App模块下新建一个LjyButterKnife类,利用反射来调用上面方法
public class LjyButterKnife {
public static void bind(Activity target) {
try {
Class<?> clazz = target.getClass();
// 反射获取apt生成的指定类
Class<?> bindViewClass = Class.forName(clazz.getName() + "__ViewBinding");
// 获取它的方法
Method method = bindViewClass.getMethod("bind", clazz);
// 执行方法
method.invoke(bindViewClass.newInstance(), target);
} catch (Exception e) {
e.printStackTrace();
}
}
}
8. 在Activity中使用
- 可以看到我是和ButterKnife同时使用的,也不会有任何冲突问题
class ButterKnifeActivity : AppCompatActivity() {
@LjyBindView(R.id.et_name)
lateinit var etName: EditText
@BindView(R.id.et_pwd)
lateinit var etPwd: EditText
@BindString(R.string.login_error)
lateinit var loginErrorMessage: String
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_butter_knife)
ButterKnife.bind(this)
LjyButterKnife.bind(this)
}
@OnClick(R.id.btn_submit)
fun submit() {
LjyLogUtil.d("etName=${etName.text}")
LjyLogUtil.d("etPwd=${etPwd.text}")
if ("ljy".equals(etName.text) && "123".equals(etPwd.text)) {
LjyLogUtil.d("登录成功")
} else {
LjyLogUtil.d(loginErrorMessage)
}
}
}
参考
我是今阳,如果想要进阶和了解更多的干货,欢迎关注微信公众号 “今阳说” 接收我的最新文章
更多推荐
已为社区贡献4条内容
所有评论(0)