前言

话说在android这座大山里,有一座庙(方块公司-square),庙里住着一个神-jake(我是这么叫的嘻嘻)。
不要小看这个小jake,这个神可是为android应用开发们提供了强有力的帮助。比如流行的开源库okhttp,leakcanary ,retrofit,butterknife 等等都是出于他之手。小弟佩服的不要不要的…,可以说是为android的应用开发效率和耦合性提高了一个台阶啊。
其它的大神我也是佩服的不要不要的…嘻嘻

声明

这一系列的文章是对ButterKnife的源码进行分析的,涉及的细节比较多,但也比较广。一次看不懂no 问题,慢慢来嘛,嘻嘻
如有不对的地方,还望指导。
最后会给出自己实现的一个demo,主要是原理和思想哦!!!

代码结构


我们这里对ButterKnife的最新版本8.4.0进行分析。
我们先down下来看下代码的结构,可以看到代码结构分的还是很好的。

  • butterknife ;android library model 提供android使用的API
  • butterknife-annotations; java-model,使用时的注解
  • butterknife-compiler;java-model,编译时用到的注解的处理器
  • butterknife-gradle-plugin;自定义的gradle插件,辅助生成有关代码
  • butterknife-integration-test;该项目的测试用例
  • butterknife-lint;该项目的lint检查
  • sample;demo

可以看到大神的代码很风骚的,很清晰啊。

这里重点分析butterknife-compiler及butterknife

原理图

你可以看完了再回来看这个图,会更明白。

使用方法

这里不再给出。不过会在原代码分析的时候给出一些注意的地方。
我们拿官方的demo-SimpleActivity
编译完后最后生成的文件为:SimpleActivity_ViewBinding
路径在:

内容:

// Generated code from Butter Knife. Do not modify!
package com.example.butterknife.library;import android.support.annotation.CallSuper;
import android.support.annotation.UiThread;
import android.view.View;
import android.widget.AdapterView;
import android.widget.Button;
import android.widget.ListView;
import android.widget.TextView;
import butterknife.Unbinder;
import butterknife.internal.DebouncingOnClickListener;
import butterknife.internal.Utils;
import java.lang.IllegalStateException;
import java.lang.Override;public class SimpleActivity_ViewBinding<T extends SimpleActivity> implements Unbinder {protected T target;private View view2130968578;private View view2130968579;@UiThreadpublic SimpleActivity_ViewBinding(final T target, View source) {this.target = target;View view;target.title = Utils.findRequiredViewAsType(source, R.id.title, "field 'title'", TextView.class);target.subtitle = Utils.findRequiredViewAsType(source, R.id.subtitle, "field 'subtitle'", TextView.class);view = Utils.findRequiredView(source, R.id.hello, "field 'hello', method 'sayHello', and method 'sayGetOffMe'");target.hello = Utils.castView(view, R.id.hello, "field 'hello'", Button.class);view2130968578 = view;view.setOnClickListener(new DebouncingOnClickListener() {@Overridepublic void doClick(View p0) {target.sayHello();}});view.setOnLongClickListener(new View.OnLongClickListener() {@Overridepublic boolean onLongClick(View p0) {return target.sayGetOffMe();}});view = Utils.findRequiredView(source, R.id.list_of_things, "field 'listOfThings' and method 'onItemClick'");target.listOfThings = Utils.castView(view, R.id.list_of_things, "field 'listOfThings'", ListView.class);view2130968579 = view;((AdapterView<?>) view).setOnItemClickListener(new AdapterView.OnItemClickListener() {@Overridepublic void onItemClick(AdapterView<?> p0, View p1, int p2, long p3) {target.onItemClick(p2);}});target.footer = Utils.findRequiredViewAsType(source, R.id.footer, "field 'footer'", TextView.class);target.headerViews = Utils.listOf(Utils.findRequiredView(source, R.id.title, "field 'headerViews'"), Utils.findRequiredView(source, R.id.subtitle, "field 'headerViews'"), Utils.findRequiredView(source, R.id.hello, "field 'headerViews'"));}@Override@CallSuperpublic void unbind() {T target = this.target;if (target == null) throw new IllegalStateException("Bindings already cleared.");target.title = null;target.subtitle = null;target.hello = null;target.listOfThings = null;target.footer = null;target.headerViews = null;view2130968578.setOnClickListener(null);view2130968578.setOnLongClickListener(null);view2130968578 = null;((AdapterView<?>) view2130968579).setOnItemClickListener(null);view2130968579 = null;this.target = null;}
}

我们看到了我们熟悉的代码,虽然比较乱(因为是生成的),
可以看出 在构造中findview 在unbind中进行置null处理,让告诉gc在合适的机会回收占用的内存 ;但是这是后面真正生成代码我们看不到的,没关系 嘻嘻。

整体的原理-(编译时期-注解处理器)

在java代码的编译时期,javac 会调用java注解处理器来进行处理。因此我们可以定义自己的注解处理器来干一些事情。一个特定注解的处理器以 java 源代码(或者已编译的字节码)作为输入,然后生成一些文件(通常是.java文件)作为输出。因此我们可以在用户已有的代码上添加一些方法,来帮我们做一些有用的事情。这些生成的 java 文件跟其他手动编写的 java 源代码一样,将会被 javac 编译。(个人参考及个人理解)

定义处理器 继承AbstractProcessor

在java中定义自己的处理器都是继承自AbstractProcessor
前3个方法都试固定写法,主要是process方法。

public class MyProcessor extends AbstractProcessor {//用来指定你使用的 java 版本。通常你应该返回                                SourceVersion.latestSupported()@Overridepublic SourceVersion getSupportedSourceVersion() {return SourceVersion.latestSupported();}//会被处理器调用,可以在这里获取Filer,Elements,Messager等辅助类,后面会解释@Overridepublic synchronized void init(ProcessingEnvironment processingEnv) {super.init(processingEnv);}//这个方法返回stirng类型的set集合,集合里包含了你需要处理的注解@Overridepublic Set<String> getSupportedAnnotationTypes() {Set<String> annotataions = new LinkedHashSet<String>();annotataions.add("com.example.MyAnnotation");return annotataions;}//核心方法,这个一般的流程就是先扫描查找注解,再生成 java 文件//这2个步骤设计的知识点细节很多。@Overridepublic boolean process(Set<? extends TypeElement> annoations,RoundEnvironment env) {return false;}
}

注册你的处理器

要像jvm调用你写的处理器,你必须先注册,让他知道。怎么让它知道呢,其实很简单,google 为我们提供了一个库,简单的一个注解就可以。
首先是依赖

  compile 'com.google.auto.service:auto-service:1.0-rc2'
@AutoService(Processor.class)
public class BindViewProcessor extends AbstractProcessor {//...省略非关键代码
}

基本概念

  • Elements:一个用来处理Element的工具类
  • Types:一个用来处理TypeMirror的工具类
  • Filer:你可以使用这个类来创建.java文件

源码分析

分析之前呢先要有写基本的概念

可以看到AutoService注解

@AutoService(Processor.class)
public final class ButterKnifeProcessor extends AbstractProcessor {//...
}
(1)init 方法,这个主要是获取一些辅助类
    private Filer mFiler; //文件相关的辅助类private Elements mElementUtils; //元素相关的辅助类private Messager mMessager; //日志相关的辅助类@Override public synchronized void init(ProcessingEnvironment env) {super.init(env);elementUtils = env.getElementUtils();typeUtils = env.getTypeUtils();filer = env.getFiler();try {trees = Trees.instance(processingEnv);} catch (IllegalArgumentException ignored) {}}
(2)getSupportedSourceVersion()方法就是默认的,获取你该处理器使用的java版本
 @Override public SourceVersion getSupportedSourceVersion() {return SourceVersion.latestSupported();}
(4)接下来就是process方法,这个方法是核心方法。
 @Override public boolean process(Set<? extends TypeElement> elements, RoundEnvironment env) {//1.查找所有的注解信息,并形成BindingClass(是什么 后面会讲) 保存到 map中Map<TypeElement, BindingClass> targetClassMap = findAndParseTargets(env);//2.遍历步骤1的map 的生成.java文件也就是上文的  类名_ViewBinding  的java文件for (Map.Entry<TypeElement, BindingClass> entry : targetClassMap.entrySet()) {TypeElement typeElement = entry.getKey();BindingClass bindingClass = entry.getValue();JavaFile javaFile = bindingClass.brewJava();try {javaFile.writeTo(filer);} catch (IOException e) {error(typeElement, "Unable to write binding for type %s: %s", typeElement, e.getMessage());}}return true;}

咦,很明显,2步走啊。是的没错, 满满套路啊!!大家都回农村吧,嘻嘻
下面我们仔细走一下该方法流程。

  1. 每个注解的查找与解析-findAndParseTargets
  private Map<TypeElement, BindingClass> findAndParseTargets(RoundEnvironment env) {Map<TypeElement, BindingClass> targetClassMap = new LinkedHashMap<>();Set<TypeElement> erasedTargetNames = new LinkedHashSet<>();scanForRClasses(env);//...下面是每个注解的解析// Process each @BindArray element.for (Element element : env.getElementsAnnotatedWith(BindArray.class)) {if (!SuperficialValidation.validateElement(element)) continue;try {parseResourceArray(element, targetClassMap, erasedTargetNames);} catch (Exception e) {logParsingError(element, BindArray.class, e);}}// Process each @BindView element.for (Element element : env.getElementsAnnotatedWith(BindView.class)) {// we don't SuperficialValidation.validateElement(element)// so that an unresolved View type can be generated by later processing roundstry {parseBindView(element, targetClassMap, erasedTargetNames);} catch (Exception e) {logParsingError(element, BindView.class, e);}}//....// Process each annotation that corresponds to a listener.for (Class<? extends Annotation> listener : LISTENERS) {findAndParseListener(env, listener, targetClassMap, erasedTargetNames);}// Try to find a parent binder for each.for (Map.Entry<TypeElement, BindingClass> entry : targetClassMap.entrySet()) {TypeElement parentType = findParentType(entry.getKey(), erasedTargetNames);if (parentType != null) {BindingClass bindingClass = entry.getValue();BindingClass parentBindingClass = targetClassMap.get(parentType);bindingClass.setParent(parentBindingClass);}}return targetClassMap;}

首先我们先看一下参数 RoundEnvironment 这个是什么呢?个人理解是注解框架里的一个工具什么工具呢?一个可以在处理器处理该处理器 用来查询注解信息的工具,当然包含你在getSupportedAnnotationTypes注册的注解。
接下来呢?
创建了一个LinkedHashMap保证了先后的顺序就是先注解的先生成java文件,其实也没有什么先后无所谓。最后将其返回。
我们这里只分析一个具有代表性的BindView注解,其它的都是一样的,连代码都一毛一样。
在这之前先看一下Element这个类我们看一下官方注释

 * Represents a program element such as a package, class, or method.* Each element represents a static, language-level construct* (and not, for example, a runtime construct of the virtual machine).

在注解处理器中,我们扫描 java 源文件,源代码中的每一部分都是Element的一个特定类型。换句话说:Element代表程序中的元素,比如说 包,类,方法。每一个元素代表一个静态的,语言级别的结构.
比如:

public class ClassA { // TypeElementprivate int var_0; // VariableElementpublic ClassA() {} // ExecuteableElementpublic void setA( // ExecuteableElementint newA // TypeElement) {}
}

可以看到类为TypeElement,变量为VariableElement,方法为ExecuteableElement
这些都是Element的子类,自己可以看下源码,的确如此的。

TypeElement aClass ;
for (Element e : aClass.getEnclosedElements()){ //获取所有的子节点Element parent = e.getEnclosingElement();  // 获取父节点}

Elements代表源代码,TypeElement代表源代码中的元素类型,例如类。然后,TypeElement并不包含类的相关信息。你可以从TypeElement获取类的名称,但你不能获取类的信息,比如说父类。这些信息可以通过TypeMirror获取。你可以通过调用element.asType()来获取一个Element的TypeMirror。

这个是对于理解源码的基础。
继续,我们看到了for循环查找所有包含BindView的注解。

 parseBindView(element, targetClassMap, erasedTargetNames);

把element和targetClassMap传入

  private void parseBindView(Element element, Map<TypeElement, BindingClass> targetClassMap,Set<TypeElement> erasedTargetNames) {TypeElement enclosingElement = (TypeElement) element.getEnclosingElement();//1.检查用户使用的合法性// Start by verifying common generated code restrictions.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();}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;}//2.获取id 值// Assemble information on the field.int id = element.getAnnotation(BindView.class).value();// 3.获取 BindingClass,有缓存机制, 没有则创建,下文会仔细分析BindingClass bindingClass = targetClassMap.get(enclosingElement);if (bindingClass != null) {ViewBindings viewBindings = bindingClass.getViewBinding(getId(id));if (viewBindings != null && viewBindings.getFieldBinding() != null) {FieldViewBinding existingBinding = viewBindings.getFieldBinding();error(element, "Attempt to use @%s for an already bound ID %d on '%s'. (%s.%s)",BindView.class.getSimpleName(), id, existingBinding.getName(),enclosingElement.getQualifiedName(), element.getSimpleName());return;}} else {bindingClass = getOrCreateTargetClass(targetClassMap, enclosingElement);}//4 生成FieldViewBinding 实体 String name = element.getSimpleName().toString();TypeName type = TypeName.get(elementType);boolean required = isFieldRequired(element);FieldViewBinding binding = new FieldViewBinding(name, type, required);//5。加入到 bindingClass 成员变量的集合中bindingClass.addField(getId(id), binding);// Add the type-erased version to the valid binding         targets set.erasedTargetNames.add(enclosingElement);}

element.getEnclosingElement();是什么呢?是父节点。就是上面我们说的。
基本的步骤就是上面的5步
1.检查用户使用的合法性

   // Start by verifying common generated code restrictions.boolean hasError = isInaccessibleViaGeneratedCode(BindView.class, "fields", element)|| isBindingInWrongPackage(BindView.class, element);
 private boolean isInaccessibleViaGeneratedCode(Class<? extends Annotation> annotationClass,String targetThing, Element element) {boolean hasError = false;// 得到父节点TypeElement enclosingElement = (TypeElement) element.getEnclosingElement();//判断修饰符,如果包含private or static 就会抛出异常。// Verify method modifiers.Set<Modifier> modifiers = element.getModifiers();if (modifiers.contains(PRIVATE) || modifiers.contains(STATIC)) {error(element, "@%s %s must not be private or static. (%s.%s)",annotationClass.getSimpleName(), targetThing, enclosingElement.getQualifiedName(),element.getSimpleName());hasError = true;}//判断父节点是否是类类型的,不是的话就会抛出异常//也就是说BindView 的使用必须在一个类里// Verify containing type.if (enclosingElement.getKind() != CLASS) {error(enclosingElement, "@%s %s may only be contained in classes. (%s.%s)",annotationClass.getSimpleName(), targetThing, enclosingElement.getQualifiedName(),element.getSimpleName());hasError = true;}//判断父节点如果是private 类,则抛出异常// Verify containing class visibility is not private.if (enclosingElement.getModifiers().contains(PRIVATE)) {error(enclosingElement, "@%s %s may not be contained in private classes. (%s.%s)",annotationClass.getSimpleName(), targetThing, enclosingElement.getQualifiedName(),element.getSimpleName());hasError = true;}return hasError;}

上面的代码里遇见注释了,这里说一下也就是我们在使用bindview注解的时候不能用使用

 类不能是private修饰 ,可以是默认的或者public //in adapterprivate  static final class ViewHolder {//....}//成员变量不能是private修饰 ,可以是默认的或者public @BindView(R.id.word)private  TextView word;  

接下来还有一个方法isBindingInWrongPackage。
这个看名字也才出来个大概 就是不能在android ,java这种源码的sdk中使用。如果你的包名是以android或者java开头就会抛出异常。

   private boolean isBindingInWrongPackage(Class<? extends Annotation> annotationClass,Element element) {//得到父节点TypeElement enclosingElement = (TypeElement) element.getEnclosingElement();String qualifiedName = enclosingElement.getQualifiedName().toString();if (qualifiedName.startsWith("android.")) {error(element, "@%s-annotated class incorrectly in Android framework package. (%s)",annotationClass.getSimpleName(), qualifiedName);return true;}if (qualifiedName.startsWith("java.")) {error(element, "@%s-annotated class incorrectly in Java framework package. (%s)",annotationClass.getSimpleName(), qualifiedName);return true;}return false;} 

到这里合法性检查就完了,如果你使用不当,就会抛出异常。

 // 。。。 异常抛出了// 不合法的直接返回if (hasError) {return;}

这里说明一点,在处理器中抛出异常,你不能直接像平常写java代码一样new thow xxx 一样,这样抛出去的异常不太好看。所以java处理器帮我们提供了一个辅助类Messager,这个可以帮助我们

      /*** Returns the messager used to report errors, warnings, and other* notices.** @return the messager*/Messager getMessager();

比如检查使用合法性抛出的异常信息-error方法最后都会调用

  private void error(Element element, String message, Object... args) {//Kind.ERROR 级别,就像你使用android的log一样printMessage(Kind.ERROR, element, message, args);}private void note(Element element, String message, Object... args) {printMessage(Kind.NOTE, element, message, args);}private void printMessage(Kind kind, Element element, String message, Object[] args) {if (args.length > 0) {message = String.format(message, args);}processingEnv.getMessager().printMessage(kind, message, element);
}

ok,到这里说了这么多才完成了检查。我们接着parseBindView的步骤2 获取值 ,这里就不多说了。好了,先休息下吧…

下一篇我们接着看。
深入理解ButterKnife源码并掌握原理(二)

深入理解ButterKnife源码并掌握原理(一)相关推荐

  1. 深入理解ButterKnife源码并掌握原理(四)

    到此我们整个的流程算分析完了. 最后我们看下对外提供的API bind 方法 那么还差一步,什么时候都要我们生成的java文件呢?答案是: ButterKnife.bind(this);方法. 我们看 ...

  2. 深入理解ButterKnife源码并掌握原理(三)

    上两篇我们分析完了处理器的process方法的findAndParseTargets方法来获取了一个集合,该集合包含了你使用注解的类的TypeElement和这个类中的注解的实例BindingClas ...

  3. 深入理解ButterKnife源码并掌握原理(二)

    好,我们接着parseBindView的步骤3 ,忘记了在哪里了,咦. 可以看下上一篇,哈哈. 步骤3 BindingClass bindingClass = targetClassMap.get(e ...

  4. 深入理解ButterKnife源码并掌握原理(五)

    前面四篇是自己参考有关知识和分析ButterKnife源码后写的,如果有不对的地方,还望指导下. 这一篇是实践篇.我们的目的是学会原理来变为自己的知识. 这个demo所使用的原理就是ButterKni ...

  5. MyBatis 源码分析 - 缓存原理

    1.简介 在 Web 应用中,缓存是必不可少的组件.通常我们都会用 Redis 或 memcached 等缓存中间件,拦截大量奔向数据库的请求,减轻数据库压力.作为一个重要的组件,MyBatis 自然 ...

  6. Tomcat7.0源码分析——请求原理分析(上)

    前言 谈起Tomcat的诞生,最早可以追溯到1995年.近20年来,Tomcat始终是使用最广泛的Web服务器,由于其使用Java语言开发,所以广为Java程序员所熟悉.很多早期的J2EE项目,由程序 ...

  7. 【赠书福利】掘金爆火小册同名《Spring Boot源码解读与原理剖析》正式出书了!...

    关注我们丨文末赠书 承载着作者的厚望,掘金爆火小册同名读物<Spring Boot源码解读与原理剖析>正式出书! 本书前身是掘金社区销量TOP的小册--<Spring Boot源码解 ...

  8. 遍历HashMap源码——红黑树原理、HashMap红黑树实现与反树型化(三)

    本章将是HashMap源码的最后一章,将介绍红黑树及其实现,HashMap的remove方法与反树型化.长文预警~~ 遍历HashMap源码--红黑树原理.HashMap红黑树实现与反树型化 什么是红 ...

  9. Java基础笔记(2)——HashMap的源码,实现原理,底层结构是怎么样的

    Java基础笔记(2)--HashMap的源码,实现原理,底层结构是怎么样的 HashMap的源码,实现原理,底层结构 1.HashMap: HashMap是基于哈希表的 Map 接口的实现.此实现提 ...

最新文章

  1. mongodb AND查询遇到多个index时候可能会做交集——和复合索引不同
  2. python优秀程序员条件_Python 条件语句
  3. 减少Java垃圾收集开销的5条提示
  4. PHP中复杂类型的一些探究。。。
  5. 华为交换机开机后接口等一直闪_交换机的工作原理
  6. java quartz
  7. MVC:感觉用户认证标识(IsAuthenticated)有点延时。
  8. mac 安装mysql
  9. linux 桌面共享xfce,漂亮而且轻量级的桌面-----xfce安装
  10. QEMU: virsh/qemu-kvm 总结
  11. go编译为linux可执行文件,go编译可执行文件
  12. SQL注入(持续更新中)
  13. Mac 电脑系统重装(详细流程,包含 Intel、Apple M系列)
  14. 美元符号 ($) 在 jQuery 中是什么意思?
  15. windows桌面图标全部不见了怎么办
  16. 智能客服搭建(3) - MRCP Server 与 FreeSWITCH 对接
  17. 微商分销功能不能用了
  18. 主板BIOS被破坏时的解决方法
  19. 分享一款自用的强悍高清录制软件Bandicam4.3.4.1503中文便携特别版
  20. 汉字数组,配合转换拼音

热门文章

  1. 基于better-scroll实现的类似ios选择器
  2. DataNode 运行状况
  3. 一目了然,看民生银行 IT 运维故障管理可视化案例
  4. Linux应用调试-strace命令
  5. 再记AE与AO的区别与联系
  6. 经典]Linux内核中ioremap映射的透彻理解【转】
  7. Ubuntu增加Swap分区大小
  8. 029 RDD Join相关API,以及程序
  9. 递归大总结之台阶问题
  10. 互联网金融爬虫怎么写-第二课 雪球网股票爬虫(正则表达式入门)