Butterknife 源码剖析(一)

源码剖析——ButterKnife的工作流程

butterknife是一个android视图快速注入库,它通过给view字段添加java注解,可以让我们丢掉findViewById()来获取view的方法,从而简化了代码。本文是基于7.0.1版本的剖析。

首先来看下注解方式:
1、标准Annotation
标准的Annotation,我们经常用的@Override、@Deprecated、@SuppressWarnings,这些是java自带的几个Annotation,分别表示重写函数、不鼓励使用、忽略某项Warning。

2、元Annotation
元Annotation是指用来定义Annotation的Annotation,一般我们自定义Annotation时就会用到。主要包括以下几个:

  • @Documented是否会保存到Javadoc文档中
  • @Retention保留时间,可选值SOURCE(源码时),CLASS(编译时),RUNTIME(运行时),默认为CLASS,值为SOURCE大都为MarkAnnotation,这类Annotation大都用来校验,比如Override,Deprecated,SuppressWarnings
  • @Target可以用来修饰哪些程序元素,如TYPE,METHOD,CONSTRUCTOR,FIELD,PARAMETER等,未标注则表示可修饰所有
  • @Inherited是否可以被继承,默认为false

OK,我们来看看butterknife的@Bind注解,@Retention是编译时,@Target是字段。

@Retention(CLASS) @Target(FIELD)
public @interface Bind {
 /** View ID to which the field will be bound. */
 int[] value();
}

编译时 Annotation 指 @Retention 为 CLASS 的 Annotation,由apt(Annotation Processing Tool) 解析自动解析。ButterKnife便是用了Java Annotation Processing技术,就是在Java代码编译成Java字节码的时候就已经处理了@Bind、@OnClick(ButterKnife还支持很多其他的注解)这些注解了。

你可以你定义注解,并且自己定义解析器来处理它们。Annotation processing是在编译阶段执行的,它的原理就是读入Java源代码,解析注解,然后生成新的Java代码。新生成的Java代码最后被编译成Java字节码,注解解析器(Annotation Processor)不能改变读入的Java 类,比如不能加入或删除Java方法。

ButterKnife 工作流程
当你编译你的Android工程时,ButterKnife工程中ButterKnifeProcessor类的process()方法会执行以下操作:

  • 开始它会扫描Java代码中所有的ButterKnife注解@Bind、@OnClick、@OnItemClicked等。
  • 当它发现一个类中含有任何一个注解时,ButterKnifeProcessor会帮你生成一个Java类,名字类似$$ViewBinder,这个新生成的类实现了ViewBinder接口。
  • 这个ViewBinder类中包含了所有对应的代码,比如@Bind注解对应findViewById(), @OnClick对应了view.setOnClickListener()等等。
  • 最后当Activity启动ButterKnife.bind(this)执行时,ButterKnife会去加载对应的ViewBinder类调用它们的bind()方法。

来看个使用butterknife的例子:

@Bind(R.id.button)Button mButton;

@Override
protected voidon Create(Bundlesaved InstanceState){
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    ButterKnife.bind(this);
}

@OnClick(R.id.button) void clickButton() {
    Toast.makeText(MainActivity.this,“HelloWorld!”,Toast.LENGTH_SHORT).show();
}

onCreate()方法中调用了ButterKnife.bind(this)我们点进去看看:

public static void bind(Activity target) {
    bind(target, target, Finder.ACTIVITY);
}

调用了bind方法,参数分别为activity和Finder,继续点进去:

static void bind(Object target, Object source, Finder finder) {
  Class<?> targetClass = target.getClass();
  try {
    if (debug) Log.d(TAG, "Looking up view binder for " + targetClass.getName());
    //查找ViewBinder类
    ViewBinder<Object> viewBinder = findViewBinderForClass(targetClass);
    if (viewBinder != null) {
      viewBinder.bind(finder, target, source);
    }
  } catch (Exception e) {
    throw new RuntimeException("Unable to bind views for " + targetClass.getName(), e);
  }
 }

第5行findViewBinderForClass应该是去查找ViewBinder类,点进去:

private static ViewBinder<Object> findViewBinderForClass(Class<?> cls)
      throws IllegalAccessException, InstantiationException {
    //从内存中查找
    ViewBinder<Object> viewBinder = BINDERS.get(cls);
    if (viewBinder != null) {
      if (debug) Log.d(TAG, "HIT: Cached in view binder map.");
      return viewBinder;
    }
    String clsName = cls.getName();
    //检查是否为framework class
    if (clsName.startsWith(ANDROID_PREFIX) || clsName.startsWith(JAVA_PREFIX)) {
      if (debug) Log.d(TAG, "MISS: Reached framework class. Abandoning search.");
      return NOP_VIEW_BINDER;
    }
    try {
      //实例化“MainActivity$$ViewBinder”这样的类
      Class<?> viewBindingClass = Class.forName(clsName + ButterKnifeProcessor.SUFFIX);
      //noinspection unchecked
      viewBinder = (ViewBinder<Object>) viewBindingClass.newInstance();
      if (debug) Log.d(TAG, "HIT: Loaded view binder class.");
    } catch (ClassNotFoundException e) {
      if (debug) Log.d(TAG, "Not found. Trying superclass " + cls.getSuperclass().getName());
      //异常,则去父类查找
      viewBinder = findViewBinderForClass(cls.getSuperclass());
    }
    //放入内存并返回
    BINDERS.put(cls, viewBinder);
    return viewBinder;
  }

第3行是首先从内存中查找,看BINDERS的定义:

static final Map<Class<?>, ViewBinder<Object>> BINDERS = new LinkedHashMap<Class<?>, ViewBinder<Object>>();

内存中没有,第9行判断如果framework class,就放弃查找并返回ViewBinder的空实现实例。

  public static final String ANDROID_PREFIX = "android.";
  public static final String JAVA_PREFIX = "java.";
  static final ViewBinder<Object> NOP_VIEW_BINDER = new ViewBinder<Object>() {
    @Override public void bind(Finder finder, Object target, Object source) { }
    @Override public void unbind(Object target) { }
  };

第14行是去实例化“MainActivity$$ViewBinder”这样的类,如果viewBinder不为空,放入缓存并返回;如果ClassNotFoundException异常则去父类查找。

  public static final String SUFFIX = "$$ViewBinder";

我们回到上面的bind()方法,查找到的ViewBinder不为空,则执行viewBinder.bind(finder,target,source)方法。

当我们编译运行后,在build文件夹下找到MainActivity$$ViewBinder.java类,具体代码为:

public class MainActivity$$ViewBinder<T extends com.spirittalk.rxjavatraining.MainActivity> implements ViewBinder<T> {
  @Override public void bind(final Finder finder, final T target, Object source) {
    View view;
    view = finder.findRequiredView(source, 2131492970, "field 'mButton' and method 'clickButton'");
    target.mButton = finder.castView(view, 2131492970, "field 'mButton'");
    view.setOnClickListener(
      new butterknife.internal.DebouncingOnClickListener() {
        @Override public void doClick(android.view.View p0) {
          target.clickButton();
        }
      });
  }

  @Override public void unbind(T target) {
    target.mButton = null;
  }
}

最终,bind()方法会执行到MainActivity$$ViewBinder.java类中的bind()方法。

那么,MainActivity$$ViewBinder.java类是如何生成的呢?这个在下篇 编译期解析注解、生成java代码流程 中揭晓。

在上面的过程中可以看到,为什么你用@Bind、@OnClick等注解标注的属性或方法必须是public或protected的,因为ButterKnife是通过ExampleActivity.this.button来注入View的。

为什么要这样呢?有些注入框架比如roboguice你是可以把View设置成private的,答案就是性能。如果你把View设置成private,那么框架必须通过反射来注入View,一个很大的缺点就是在Activity运行时大量使用反射会影响App的运行性能,造成卡顿以及生成很多临时Java对象更容易触发GC,不管现在手机的CPU处理器变得多快,如果有些操作会影响性能,那么是肯定要避免的,这就是ButterKnife与其他注入框架的不同。

转载请标明出处:http://www.jianshu.com/p/95d4f0eb6027

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

推荐阅读更多精彩内容