ButterKnife源码剖析
前言
ButterKnife是一个Android视图快速注入库,它通过给view字段添加注解的方式,让我们省去了findViewById()和setOnClick等方法,从而简化了代码。
注意:本文是基于7.0.0版本的剖析。
注解
关于Java中注解的相关知识,建议阅读:注解(张孝祥Java视频笔记)
这里直接介绍ButterKnife中的相关注解,Bind和OnClick。
1 | @Retention(CLASS) @Target(FIELD) |
Bind注解是编译时注解,修饰的对象是字段。
1 | @Target(METHOD) |
OnClick注解也是编译时注解,修饰的对象是方法。
这些注解都是编译时注解,在编译的时候由apt(Annotation Processing Tool) 解析自动解析。ButterKnife便是用了Java Annotation Processing技术,就是在Java代码编译成Java字节码的时候就已经处理了@Bind、@OnClick(ButterKnife还支持很多其他的注解)这些注解了。
开发者可以自定义注解,并且自己定义注解解析器来处理它们。Annotation processing是在编译阶段执行的,它的原理就是读入Java源代码,解析注解,然后生成新的Java代码。新生成的Java代码最后被编译成Java字节码,注解解析器(Annotation Processor)不能改变读入的Java 类,比如不能加入或删除Java方法。
例子
例子:
1 | public class MainActivity extends AppCompatActivity { |
代码编译完成后,会在图示目录下生成MainActivity$$ViewBinder类。
MainActivity$$ViewBinder的源码后续会进行分析,这里我们只需要知道使用了ButterKnife的名为ClassName的类,在编译后会在相应的目录下生成类名为ClassName$$ViewBinder类即可。
关于ButterKnife是如何生成ClassName$$ViewBinder类的建议阅读:Butterknife 源码剖析(二)
流程分析
借助上面的例子分析ButterKnife的流程,在onCreate()方法中调用了ButterKnife.bind(this),bind方法的源码如下所示:
1 | public static void bind(Activity target) { |
bind方法中又调用了bind方法,参数分别为Activity和Finder,我们看到这里的第三个参数是Finder.ACTIVITY,先简单看下Finder的源码:
1 | public enum Finder { |
Finder是ButterKnife的一个内部枚举类,这里只贴出了后面分析会用到的代码,其他的由于篇幅的原因省略了。Finder内部定义了两个抽象方法,同时在Finder类的内部又有三个成员枚举类分别实现了这两个抽象方法,这两个抽象方法很简单,第一个方法findView就是调用对应类的findViewById方法来查找对应的对象,第二个方法getContext就是调用对应类的getContext方法来获得对应类型的context。再来看下findRequiredView方法,findRequiredView方法内部调用了findOptionalView方法,而findOptionalView方法内部调用findView方法去查找View,然后强制转换成对应的View类型。
继续看bind方法的源码,源码如下所示:
1 | static void bind(Object target, Object source, Finder finder) { |
bind方法主要做了两件事情,一是根据目标类,查找编译时候生成生成的目标类的对应的名为ClassName$$ViewBinder的类,二是调用编译时生成的类的bind方法完成相应的设置。
首先看下findViewBinderForClass(targetClass)方法,源码如下所示:
1 | private static ViewBinder<Object> findViewBinderForClass(Class<?> cls) |
首先从内存中查找,BINDERS的定义:
1 | static final Map<Class<?>, ViewBinder<Object>> BINDERS = new LinkedHashMap<Class<?>, ViewBinder<Object>>(); |
内存中没有,执行后面的代码,判断如果是framework class,就放弃查找并返回ViewBinder的空实现实例。
1 | public static final String ANDROID_PREFIX = "android."; |
如果不是framework class的话,会去实例化“MainActivity$$ViewBinder”这样的类,如果viewBinder不为空,放入缓存并返回;如果ClassNotFoundException异常则去父类查找。
1 | public static final String SUFFIX = "$$ViewBinder"; |
分析完findViewBinderForClass(Class<?> cls)方法后,我们继续回到上面的bind()方法,查找到的ViewBinder不为空,则执行viewBinder.bind(finder,target,source)方法,我们知道查找到的ViewBinder就是我们在前面提到的MainActivity$$ViewBinder类,接下来我们看看MainActivity$$ViewBinder类的源码。
1 | public class MainActivity$$ViewBinder<T extends tk.thinkerzhangyan.myapplication.MainActivity> implements ViewBinder<T> { |
可以看到MainActivity$$ViewBinder的bind方法中调用了Finder的findRequiredView来查找Activity中的对应id的View,通过前面的分析,我们可以知道findRequiredView方法最终是通过调用Activity的findViewById来查找对应id的View的,查找到对应的View后,给对应的View设置响应的点击事件。至此我们就知道,ButterKnifede是如何工作的了。
通过上面的分析我们可以明白为什么我们用@Bind、@OnClick等注解标注的属性或方法必须是public或protected的,因为ButterKnife是通过MainActivity.this.button来注入View的。
为什么要这样呢?有些注入框架比如RoboGuice你是可以把View设置成private的,答案就是性能。如果你把View设置成private,那么框架必须通过反射来注入View,一个很大的缺点就是在Activity运行时大量使用反射会影响App的运行性能,造成卡顿以及生成很多临时Java对象更容易触发GC,不管现在手机的CPU处理器变得多快,如果有些操作会影响性能,那么是肯定要避免的,这就是ButterKnife与其他注入框架的不同。
通过反射来实现ButterKnife的原理:
当执行绑定的时候,通过反射获取当前类的所有成员变量,然后检查这些成员变量上是否有自己定义的ButterKnife注解,如果有的话获取注解的值,然后调用当前类的findViewById方法来获取View,然后通过反射的方式,将通过findViewById查找到的View,设置给注解修饰的成员变量,点击事件的处理同理。
通过反射的方式,来实现ButterKnife效率不高,因为反射的过程中会产生很多的临时变量,临时变量过多可能会导致内存抖动,内存抖动可能会引起卡顿。
总结
Butterknife是基于Java注解解析技术,自己定义注解,同时通过继承AbstractProcessor自定义注解解析器,编译java源代码的时候,自定义的注解的解析器的process会被调用,查询类中是否含有自己定义的注解,如果含有自己定义的注解的话,就生成相应一个新的java类,新类的名字是ClassName$$ViewBinder,新生成的类实现了ViewBinder接口,实现了bind跟unBind方法,新生成的类也会被编译生成字节码,当程序运行的时候调用Butterknife.bind(this)执行动态绑定的时候,会去查找当前类对应的ClassName$$ViewBinder类,通过反射创建这个类的实例,然后调用其bind方法来完成view的查找和点击事件的设置。
注意由于Butterknife中自定义的注解的是存在与字节码阶段的,所以添加的注解,当加载进JVM的时候,会被忽略,所以不会影响程序的性能。另外注解是在编译阶段处理的,只是消耗编译的时间,不会影响程序的运行时间。
ButterKnife通过注解的方式来绑定View的属性或方法。减少代码的书写,使代码更为简洁明了,同时不消耗额外的性能,当然这样也有个缺点,就是可读性会差一些,需要开发者自己做出取舍。
扩展,如何自定义Android中的注解库
创建一个Module,在这个Module里面自定义注解,然后创建一个Module,在这个Modlue里面自定义注解处理器放。
自定义注解处理器的时候,常用到两个库:auto-service和javapoet。
auto-service自动用于在META-INF/services目录文件夹下创建javax.annotation.processing.Processor文件;
javapoet用于产生 .java 源文件的辅助库,它可以很方便地帮助我们生成需要的.java 源文件。
auto-service使用很简单,只需要在定义的注解处理器上添加@AutoService(Processor.class)即可,示例如下:
1 | @AutoService(Processor.class) |
如果不使用auto-service,需要我们自己手动的在相应的目录下创建相关的文件。步骤如下:
1、在 processors 库的 main 目录下新建 resources 资源文件夹;
2、在 resources文件夹下建立 META-INF/services 目录文件夹;
3、在 META-INF/services 目录文件夹下创建 javax.annotation.processing.Processor 文件;
4、在 javax.annotation.processing.Processor 文件写入注解处理器的全称,包括包路径;
这个文件的作用,就是告诉jvm(或者是编译器)我们自己定义的注解处理器的位置。如果没有这个文件的话,我们的注解处理器是根本不会起作用的。
关于javapoet的使用,请查阅API。
前面我们提到,需要将自定义的注解和注解解析器放在两个不同的Module里面,之所以将注解和注解处理器放在两个不同的Module里面,主要是因为,只有在编译的时候需要这个注解处理器,而在打包APK的时候这个注解处理器是不需要打包进APK里面的,所以我们单独给注解处理器建了一个Module。
注解处理器只在编译处理期间需要用到,编译处理完后就没有实际作用了,而主项目添加了这个库会引入很多不必要的文件,为了处理这个问题我们需要引入个插件android-apt,它能很好地处理这个问题。
它有两个目的:
- 允许配置只在编译时作为注解处理器的依赖,而不添加到最后的APK或library
- 设置源路径,使注解处理器生成的代码能被Android Studio正确的引用
1 | 伴随着 Android Gradle 插件 2.2 版本的发布,android-apt 作者在官网发表声明证实了后续将不会继续维护 android-apt,并推荐大家使用 Android 官方插件提供的相同能力。也就是说,大约三年前推出的 android-apt 即将告别开发者,退出历史舞台,Android Gradle 插件提供了名为 annotationProcessor 的功能来完全代替 android-apt。 |
annotationProcessor的功能和android-apt的功能是一样的。这里我们只介绍annotationProcessor的使用。
在工程中使用我们自定义的注解,需要在工程中依赖包含我们自定义注解的Module,为了使用我们自定义的注解解析器处理我们自定义的注解,需要借助于annotationProcessor。这些都可以通过配置gradle文件来完成,附录使用eventbus的gradle文件。
1 | dependencies { |
依赖自定义的注解库:
1 | compile 'org.greenrobot:eventbus:3.0.0' |
借助于annotationProcessor依赖,自定义的注解处理处理器:
1 | annotationProcessor 'org.greenrobot:eventbus-annotation-processor:3.0.1' |
自定义注解之编译时注解(RetentionPolicy.CLASS)(一)(建议先看这个系列)
自定义注解之编译时注解(RetentionPolicy.CLASS)(二)——JavaPoet
自定义注解之编译时注解(RetentionPolicy.CLASS)(三)—— 常用接口介绍
android-apt切换为官方annotationProcessor
Android」Android开发你需要知道的注解(Annotation)
Android进阶之自定义注解(建议看这个)
如何自定义 Android 注解?
Android,几分钟教你怎么应用自定义注解
参考资料:
Butterknife 源码剖析(一) 关于Butterknife的工作流程推荐看这篇
ButterKnife – 源码分析 – 在‘编译期’间生成findViewById等代码
android-open-source-project-cracking github上的博客
Android Studio上方便使用butterknife注解框架的偷懒插件Android Butterknife Zelezny