ButterKnife源码解析


使用Butterknife的主要目的是消除关于view实例化的样板代码,这是一个专为View类型的依赖注入框架。Dagger2是一个更加通用的依赖注入框架。

ButterKnife的使用非常简单,其工作原理是先对添加的注解域@Bind做编译时解析,生成一个中间类,这个中间类具有将经过注解的域实例化的能力在运行时使用中间类完成真正的实例化

1.ButterKnife的使用

1.注入View

在碎片中使用

@Override 
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
    View view = inflater.inflate(R.layout.fancy_fragment, container, false);
    ButterKnife.bind(this, view);
    // TODO Use fields...
    return view;
}

在适配器中使用

static class ViewHolder {
    @Bind(R.id.title) TextView name;
    @Bind(R.id.job_title) TextView jobTitle;

    public ViewHolder(View view) {
      ButterKnife.bind(this, view);
    }
}

2.监听器注入

@OnClick(R.id.submit)
public void sayHi(Button button) {
  button.setText("Hello!");
}

多个同类型的控件注册一个监听器

@OnClick({ R.id.door1, R.id.door2, R.id.door3 })
public void pickDoor(DoorView door) {
  if (door.hasPrizeBehind()) {
    Toast.makeText(this, "You win!", LENGTH_SHORT).show();
  } else {
    Toast.makeText(this, "Try again", LENGTH_SHORT).show();
  }
}

3.为控件列表注入动作

// 1.注入控件列表
@BindViews({ R.id.first_name, R.id.middle_name, R.id.last_name })
List<EditText> nameViews;

//2.绑定控件列表与动作
ButterKnife.apply(nameViews, DISABLE);
ButterKnife.apply(nameViews, ENABLED, false);

//3.定义动作
static final ButterKnife.Action<View> DISABLE = new ButterKnife.Action<View>() {
  @Override public void apply(View view, int index) {
    view.setEnabled(false);
  }
};

static final ButterKnife.Setter<View, Boolean> ENABLED = new ButterKnife.Setter<View, Boolean>() {
  @Override public void set(View view, Boolean value, int index) {
    view.setEnabled(value);
  }
};

4.单个控件的类型查询和转型

TextView firstName = ButterKnife.findById(view, R.id.first_name);
ImageView photo = ButterKnife.findById(view, R.id.photo);

5.NavigationView的注入
默认情况下,注入 NavigationView 实例并不意味着其头视图的中的元素也跟随着被注入,例如下例中的 TextView 虽然经过注解,但实际上并没有被实例化。

@Bind(R.id.navigation_view )
View navigationView = navigationView.getHeaderView(0);

@Bind(R.id.title_text_view) TextView titleTv;

必须对头视图单独进行注入

View headerView = navigationView.getHeaderView(0);
TextView textView = ButterKnife.findById(headerView, R.id.tvName);

2.编译时注解解析

public class MainActivity extends AppCompatActivity{

    @Bind(R.id.relative_layout)
    RelativeLayout mRelativeLayout;
    
    @BindString(R.string.app_name)
    String name;
}

编译时Java会使用类ButterKnifeProcessor进行编译时注解处理,为每一个注解过的类生成一个中间类,如MainActivity类将生成中间类MainActivity$$ViewBinder;运行时将执行该中间类完成注入。ButterKnifeProcessor处理代码如下

boolean process(Set<? extends TypeElement> elements, RoundEnvironment env) {
    //1.首先找出使用的所有包含注解的类集合Map<TypeElement, BindingClass>,其key是注解类,如MainActivity,其值为相应生成的BindingClass类,包含注解集合。
    Map targetClassMap = this.findAndParseTargets(env);
    
    //2.依次为每一个注解类创建中间类
    Iterator var4 = targetClassMap.entrySet().iterator();
    while(var4.hasNext()) {
        Entry entry = (Entry)var4.next();
        TypeElement typeElement = (TypeElement)entry.getKey();
        BindingClass bindingClass = (BindingClass)entry.getValue();
        //3.使用BindingClass类创建中间类 
        JavaFileObject e = this.filer.createSourceFile(bindingClass.getFqcn(), new Element[]{typeElement});
        Writer writer = e.openWriter();
        writer.write(bindingClass.brewJava());
        writer.flush();
        writer.close();
    }

    return true;
}

上述编译时注解解析分为两步,首先搜索所有经过注解要处理的类集合,其key是注解类,如 MainActivity,其值为相应生成的 BindingClass 类;而后遍历集合使用 BindingClass 类提供的信息生成对应的中间类,如 MainActivity$$ViewBinder。

1.BindingClass 类与使用注解的类一一对应,它包含了该类的注解信息,如通过资源机制获取到控件的 id 信息,是生成中间类的信息来源。定义如下

final class BindingClass {
    //1.被注解的View信息集合
    private final Map<Integer, ViewBindings> viewIdMap; 
    //2.被注解的View集合信息集合
    private final Map<FieldCollectionViewBinding, int[]> collectionBindings; 
    //3.被注解的资源信息集合
    private final List<FieldResourceBinding> resourceBindings;
    //4.包名
    private final String classPackage;
    //5.类名
    private final String className;
    private final String targetClass;
    private String parentViewBinder;
}

其中字典集合Map<Integer, ViewBindings>key值表示控件的资源 id,而ViewBindings类表示对控件域的操作。

2.创建中间类
中间类的创建是使用Java流来写入的,其内容由bindingClass.brewJava()方法提供,可以处理原类中的注解,生成中间类如下

public class MainActivity$$ViewBinder<T extends MainActivity> implements ViewBinder<T> {

  @Override 
  public void bind(final Finder finder, final T target, Object source) {
    View view;
    view = finder.findRequiredView(source, 2131492971, "field 'mRelativeLayout'");
    target.mRelativeLayout = finder.castView(view, 2131492971, "field 'mRelativeLayout'");
    
    Resources res = finder.getContext(source).getResources();
    target.name = res.getString(2131099669);
  }

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

调用中间类中的bind方法就完成了资源的实例化。以string为例 2131099669 即根据@BindString(R.string.app_name)注解得到的资源 id。

3.对象实例化

中间类虽然具备了实例化注解对象的能力,但在编译时并没有得到执行,必须在运行时进行注解对象实例化

ButterKnife.bind(this);

该方法的实际执行代码如下

@NonNull @UiThread
public static Unbinder bind(@NonNull Activity target) {
    return getViewBinder(target).bind(Finder.ACTIVITY, target, target);
}

1.首先根据活动类MainActivity寻找其所生成的中间类MainActivity$$ViewBinder,并生成对象。

@NonNull @CheckResult @UiThread
static ViewBinder<Object> getViewBinder(@NonNull Object target){
    Class<?> targetClass = target.getClass();
    return findViewBinderForClass(targetClass);
}

这里 targetClass 就是活动 MainActivity,因为中间类的命名是固定的,即本类加$$ViewBinder后缀,故可以直接使用类名来查找中间类 MainActivity$$ViewBinder ,而后使用反射创建中间类实例

2.执行中间类对象的bind方法对所注解对象进行实例化。该类继承于接口ViewBinder,只有一个bind方法。

public interface ViewBinder<T> {
  Unbinder bind(Finder finder, T target, Object source);
}

其中Finder是一个枚举类(VIEW,ACTIVITY,DIALOG),之所以要将它们区别开来,是因为它们查找资源的方式有所差别,对于不同的类型有不同的实现。

执行注解对象实例化的代码最终委托给Finder类,包括绑定View与向下转型两步,例如对于 ACTIVITY 是这样实现的。

public class MainActivity$$ViewBinder<T extends MainActivity> implements ViewBinder<T> {

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

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,050评论 25 707
  • 本文主要介绍Android之神JakeWharton的一个注解框架,听说现在面试官现在面试都会问知不知道JakeW...
    Zeit丶阅读 974评论 4 6
  • 参考1参考2参考3参考4 一:基本原理 编译时注解+APT编译时注解:Rentention为CLASS的直接。保留...
    shuixingge阅读 539评论 0 3
  • 11月20日我读了十页从68页到78页 好词:栩栩如生、 镶嵌、不屈不挠、澎湃、剽窃、期期艾艾、滚落、南北战争、粘...
    静如子鈺阅读 421评论 0 0
  • Don’t stand before me,or behind me.Just stand by me. 文|熊...
    熊恩阅读 528评论 0 0