ButterKnife源码梳理

写代码的时候,总会要不厌其烦的写findViewById,butterknife这个框架的作用就是利用注解来简化view查找过程。
先了解下注解的几个关键词:
Target:限定使用范围
1.CONSTRUCTOR:用于描述构造器
2.FIELD:用于描述域
  3.LOCAL_VARIABLE:用于描述局部变量
  4.METHOD:用于描述方法
  5.PACKAGE:用于描述包
  6.PARAMETER:用于描述参数
  7.TYPE:用于描述类、接口(包括注解类型) 或enum声明
Retention:被保留的时间长短
1.SOURCE:在源文件中有效(即源文件保留)
  2.CLASS:在class文件中有效(即class保留)
  3.RUNTIME:在运行时有效(即运行时保留)
JDK 5中引入了源代码中的注解(annotation)这一机制。注解使得Java源代码中不但可以包含功能性的实现代码,还可以添加元数据。注解的功能类似于代码中的注释,所不同的是注解不是提供代码功能的说明,而是实现程序功能的重要组成部分。
只要知道注解大概就是可以方便的进行程序配置。
具体参考:
https://docs.oracle.com/javase/tutorial/java/annotations/index.html
http://computerdragon.blog.51cto.com/6235984/1210969
http://www.infoq.com/cn/articles/cf-java-annotation

最开始版本的butterknife使用反射来实现
<pre>
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface ViewInject {
int value();
}
</pre>
像这样定义一个注解,然后通过遍历Activity或者Fragment中的成员变量来实现findViewById的过程
<pre>
private static void injectView(Activity activity) {
Class<? extends Activity> clazz = activity.getClass();
//获得activity的所有成员变量
Field[] fields = clazz.getDeclaredFields();
for (Field field : fields) {
//获得每个成员变量上面的ViewInject注解,没有的话,就会返回null
ViewInject viewInject = field.getAnnotation(ViewInject.class);
if (viewInject != null) {
int viewId = viewInject.value();
View view = activity.findViewById(viewId);
try {
field.setAccessible(true);
field.set(activity, view);
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
}
}
</pre>

但是这种方式的性能很低,特别是对类似Android这样的系统,对性能要求很苛刻的情况,最好不要用反射。所以Jake大神进行了修改。
新版本的Butterknife用的APT(Annotation Processing Tool)编译时解析技术。
APT大概就是你声明的注解的生命周期为CLASS,然后继承AbstractProcessor类。继承这个类后,在编译的时候,编译器会扫描所有带有你要处理的注解的类,然后再调用AbstractProcessor的process方法,对注解进行处理,那么我们就可以在处理的时候,动态生成绑定事件或者控件的java代码,然后在运行的时候,直接调用bind方法完成绑定。
先说下butterknife的几个模块:
':butterknife' 静态绑定方法入口,查找生成类并存入缓存
':butterknife-annotations' 定义所有的注解类
':butterknife-compiler' 注解解析
':butterknife-gradle-plugin' 插件
':butterknife-lint'
':butterknife-integration-test' 测试模块

定义注解,注意,这儿Retention是CLASS
<pre>
@Retention(CLASS) @Target(FIELD)
public @interface BindView {
/** View ID to which the field will be bound. */
@IdRes int value();
}
</pre>
这个注解表明@BindView 是用来修饰 field 的,并且保留至编译时刻。内部有一个默认int类型的属性 value ,用来表示 View 的 id ,即平时程序中的 R.id.xxx

定义了注解之后,butterknife-compiler模块负责进行解析
核心类是ButterKnifeProcessor,它继承自AbstractProcessor,是专门处理annotation的类。
重写process方法后,在方法中butterknife进行的具体处理是
1.扫描所有具有注解的类,然后根据这些类的信息生成BindingClass,最后生成以TypeElement为键,BindingClass为值的键值对。
2.循环遍历这个键值对,根据TypeElement和BindingClass里面的信息生成对应的java类。例如AnnotationActivity生成的类即为Cliass$$ViewBinder类。
跟着代码具体看一下处理过程:
1.ButterKnifeProcessor类的init方法中主要根据 env 得到一些工具类。其中的 filter 主要是用来生成 Java 代码,而 elementUtils 和 typeUtils 会在下面源码中用到
2.process方法中首先调用了findAndParseTargets方法,在findAndParseTargets方法中调用scanForRClasses扫描所有的带有注解的类,然后遍历所有被 @BindView 注解的属性,最后调用 parseBindView 方法
<pre>
private void parseBindView(Element element, Map<TypeElement, BindingSet.Builder> builderMap,
Set<TypeElement> erasedTargetNames) {
// 得到注解 @BindView 元素所在的类元素
TypeElement enclosingElement = (TypeElement) element.getEnclosingElement();
// Start by verifying common generated code restrictions.
// ---------- 类型校验逻辑 start ---------------
// 判断是否被注解在属性上,如果该属性是被 private 或者 static 修饰的,则出错
// 判断是否被注解在错误的包中,若包名以“android”或者“java”开头,则出错
boolean hasError = isInaccessibleViaGeneratedCode(BindView.class, "fields", element)
|| isBindingInWrongPackage(BindView.class, element);
// Verify that the target type extends from View.
TypeMirror elementType = element.asType();
if (elementType.getKind() == TypeKind.TYPEVAR) {
TypeVariable typeVariable = (TypeVariable) elementType;
elementType = typeVariable.getUpperBound();
}
// 判断元素是不是View及其子类或者Interface
if (!isSubtypeOfType(elementType, VIEW_TYPE) && !isInterface(elementType)) {
if (elementType.getKind() == TypeKind.ERROR) {
note(element, "@%s field with unresolved type (%s) "+ "must elsewhere be generated as a View or interface. (%s.%s)",
BindView.class.getSimpleName(), elementType, enclosingElement.getQualifiedName(),
element.getSimpleName());
} else {
error(element, "@%s fields must extend from View or be an interface. (%s.%s)",
BindView.class.getSimpleName(), enclosingElement.getQualifiedName(),
element.getSimpleName());
hasError = true;
}
}
// 如果有错误 不执行下面代码
if (hasError) {
return;
}
//---------------- 类型校验逻辑 end -----------------
// Assemble information on the field. //得到被注解的注解值,即 R.id.xxx
int id = element.getAnnotation(BindView.class).value();
// 根据所在的类元素去查找 builder
BindingSet.Builder builder = builderMap.get(enclosingElement);
// 如果相应的 builder 已经存在
if (builder != null) {
// 得到相对应的 View 绑定的属性名
String existingBindingName = builder.findExistingBindingName(getId(id));
// 若该属性名已经存在,则说明之前已经绑定过,会报错
if (existingBindingName != null) {
error(element, "Attempt to use @%s for an already bound ID %d on '%s'. (%s.%s)",
BindView.class.getSimpleName(), id, existingBindingName,
enclosingElement.getQualifiedName(), element.getSimpleName());
return;
}
} else {
// 如果没有对应的 builder ,就通过 getOrCreateBindingBuilder 方法生成,并且放入 builderMap 中
builder = getOrCreateBindingBuilder(builderMap, enclosingElement);
}
// 得到注解名
String name = element.getSimpleName().toString();
// 得到注解元素的类型
TypeName type = TypeName.get(elementType);
boolean required = isFieldRequired(element);
// 根据 id ,添加相对应的 Field 的绑定信息
builder.addField(getId(id), new FieldViewBinding(name, type, required));
// Add the type-erased version to the valid binding targets set.
// 添加到待 unbind 的序列中
erasedTargetNames.add(enclosingElement);
}
</pre>
最后将 builderMap 转换为了 bindingMap 并返回。
到这儿准备工作已经完成,接下来就是根据准备好的bindingMap 生成辅助类,比如说
MainActivity_ViewBinding
<pre>
public class MainActivity_ViewBinding implements Unbinder {
private MainActivity target;
@UiThread
public MainActivity_ViewBinding(MainActivity target) {
this(target, target.getWindow().getDecorView());
}
@UiThread
public MainActivity_ViewBinding(MainActivity target, View source) {
this.target = target;
View view;
target.textView = Utils.findRequiredViewAsType(source, R.id.text_view, "field 'textView'", TextView.class);
target.toolbar = Utils.findRequiredViewAsType(source, R.id.toolbar, "field 'toolbar'", Toolbar.class);
}
@Override
@CallSuper
public void unbind() {
MainActivity target = this.target;
if (target == null) throw new IllegalStateException("Bindings already cleared.");
this.target = null;
target.textView = null;
target.toolbar = null;
}
}
</pre>
那这个是如何生成的呢,看process中的代码
<pre> JavaFile javaFile = binding.brewJava(sdk);</pre>
是通过https://github.com/square/javapoet 来生成代码的
核心方法在brewJava中调用了 createType(sdk),具体使用方法参考javapoet官网

生成类之后如何使用呢,butterknife调用了静态方法bind,在bind方法中调用了createBinding,这个方法是根据 target 创建对应的 targetClassName_ViewBinding对象 。在 targetClassName_ViewBinding 的构造器中会把对应的 View 进行绑定(具体可以查看上面的 MainActivity_ViewBinding )。而在 findBindingConstructorForClass(Class<?> cls) 方法中也使用了 Class.forName() 反射来查找 Class ,这也是无法避免的。但是仅限于一个类的第一次查找,之后都会从 BINDINGS 缓存中获取。

Demo地址:https://github.com/fanturbo/ButterKnifeStudy

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,445评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,889评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,047评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,760评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,745评论 5 367
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,638评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,011评论 3 398
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,669评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,923评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,655评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,740评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,406评论 4 320
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,995评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,961评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,197评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,023评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,483评论 2 342

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,594评论 18 139
  • 什么是注解(Annotation):Annotation(注解)就是Java提供了一种元程序中的元素关联任何信息和...
    九尾喵的薛定谔阅读 3,141评论 0 2
  • 本文章涉及代码已放到github上annotation-study 1.Annotation为何而来 What:A...
    zlcook阅读 29,119评论 15 116
  • 【婷婷诗词】 翡酒夜话 葡萄翡酒润身心, 举杯成歌邀知音。 良宵不付离人泪, 饮尽繁华主浮沉。
    单婷婷Shayne阅读 347评论 0 3
  • 武则天真的很不容易,不知为啥那些大男子主义的人要污蔑她? 一说起武则天,原本是朋友的人都有可能成为敌人,原因是武则...
    有梦真好阅读 1,026评论 12 12