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 nameViews;

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

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

static final ButterKnife.Setter ENABLED = new ButterKnife.Setter() {
  @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 elements, RoundEnvironment env) {
    //1.首先找出使用的所有包含注解的类集合Map,其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 viewIdMap; 
    //2.被注解的View集合信息集合
    private final Map collectionBindings; 
    //3.被注解的资源信息集合
    private final List resourceBindings;
    //4.包名
    private final String classPackage;
    //5.类名
    private final String className;
    private final String targetClass;
    private String parentViewBinder;
}

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

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

public class MainActivity$$ViewBinder implements ViewBinder {

  @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 getViewBinder(@NonNull Object target){
    Class targetClass = target.getClass();
    return findViewBinderForClass(targetClass);
}

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

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

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

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

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

public class MainActivity$$ViewBinder implements ViewBinder {

    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\'");
    }
}

你可能感兴趣的:(ButterKnife源码解析)