重构ButterKnife

简介:

        ButterKnife是一个编译时注解的框架,旨在通过注解的方式帮助开发者简化一些常用操作比如findViewByid(),

setOnClick()等操作,而它的原理就是自定义一个继承于AbstractProcessor的注解处理器,添加支持的注解,然后

在编译时会遍历所有的类文件,找到所有支持的注解回调给process方法,然后在这个方法里,通过javapoet生成相

应的java文件。相信大家已经很了解它了,这里就不多做介绍了。

对比:

       进入主题,用过xutil的朋友都知道,这个框架也是支持注解来简化代码的,而且它支持Layout注入,所以Fragment和Activity的代码可以写成下面这样:

Fragment:

@ContentView(R.layout.simple_fragment)
public class SimpleFragment extends BaseFragment {
}

Activity:

@ContentView(R.layout.simple_activity)
public class SimpleActivity extends BaseActivity {
}

       通过注解代替手写方法调用使得代码看起来清晰,尤其是对于一行代码都不想多写的强迫症患者来说,这无疑是极好的方式,遗憾的是xutil注解使用的是 @Retention(RetentionPolicy.RUNTIME)即运行时注解,所以每个注解都是在运行的时候通过反射获取到对应的值然后才执行对应方法,所以毋庸置疑的是,这比直接调用对应的方法要慢的多,要耗性能的多。

       而ButterKnife使用的是@Retention(RetentionPolicy.Class)即编译时注解,它会在编译期从java文件中

遍历所有已经在AbstractProcessorgetSupportedAnnotationTypes方法里添加支持的注解,然后生成对应的**_ViewBinding文件,而实际运行时,只需要反射一次获取到**_ViewBinding的构造器并实例,即可直接调用注解所生成好的方法。贴一个**_ViewBinding可能会直观一点,当我只注入了一个名为title的TextView,所生成的SimpleActivity_ViewBinding代码如下:

public class SimpleActivity_ViewBinding implements Unbinder {
  private SimpleActivity target;

  @UiThread
  public SimpleActivity_ViewBinding(SimpleActivity target) {
    this(target, target.getWindow().getDecorView());
  }
  @UiThread
  public SimpleActivity_ViewBinding(SimpleActivity target, View source) {
    this.target = target;
    target.title = Utils.findRequiredViewAsType(source, R.id.title, "field 'title'", TextView.class);
  }
}

      其中findRequiredViewAsType内部直接调用了findViewById操作,不涉及任何反射,所以这样就可以很好的解决反射所带来的性能问题。

       xutil仅支持View、Layout和Event的注入,而ButterKnife支持20多种类型的注入(详细类型请点击以上连接)

但是偏偏不包含Layout,这就有点尴尬了,这里也不知道为什么ButterKnife作者不支持Layout注入,可能觉得

写setContentView也就在onCreate写一行代码?但是findViewById也是一行代码啊!好吧,抛开所有的纠结,

既然ButterKnife不支持,我们通过改造它使其支持Layout的注入,让它如虎添翼!

重构:

      既然要改造它,那么首先得先摸清它的“底细”了,这里主要循着Layout注入的线索来讲解其原理。

1,首先定义注解 BindLayout

@Retention(CLASS) @Target(TYPE)
public @interface BindLayout {
    @LayoutRes int value();
}

ButterKnife的注解处理器ButterKnifeProcessor是继承自AbstractProcessor,主要方法有两个:

getSupportedAnnotationTypes这个方法返回你希望支持的注解类型,这里添加bindLayout支持
private Set<Class<? extends Annotation>> getSupportedAnnotations() {
  Set<Class<? extends Annotation>> annotations = new LinkedHashSet<>();
  annotations.add(BindArray.class);
  annotations.add(BindBitmap.class);
  annotations.add(BindBool.class);
  annotations.add(BindColor.class);
  annotations.add(BindDimen.class);
  annotations.add(BindDrawable.class);
  annotations.add(BindFloat.class);
  annotations.add(BindInt.class);
  annotations.add(BindString.class);
  annotations.add(BindLayout.class);//添加BindLayout支持
  annotations.add(BindView.class);
  annotations.add(BindViews.class);
  annotations.addAll(LISTENERS);
  return annotations;
}
process处理主要逻辑,包括获取遍历检查所有的注解,然后采用建造者模式,生成不同的bulider,然后生成
BindingSet,最后再通过javaPoet生成对应的java文件。
@Override public boolean process(Set<? extends TypeElement> elements, RoundEnvironment env) {
  Map<TypeElement, BindingSet> bindingMap = findAndParseTargets(env);

  for (Map.Entry<TypeElement, BindingSet> entry : bindingMap.entrySet()) {
    TypeElement typeElement = entry.getKey();
    BindingSet binding = entry.getValue();

    JavaFile javaFile = binding.brewJava(sdk);
    try {
      javaFile.writeTo(filer);
    } catch (IOException e) {
      error(typeElement, "Unable to write binding for type %s: %s", typeElement, e.getMessage());
    }
  }

  return false;
}
同理,我们也把Layout注解的parse添加进来
private Map<TypeElement, BindingSet> findAndParseTargets(RoundEnvironment env) {
  Map<TypeElement, BindingSet.Builder> builderMap = new LinkedHashMap<>();
  Set<TypeElement> erasedTargetNames = new LinkedHashSet<>();

  scanForRClasses(env);
  // Process each @Bind element.
  for (Element element : env.getElementsAnnotatedWith(BindLayout.class)) {
    if (!SuperficialValidation.validateElement(element)) continue;
    try {
      parseBindLayout(element, builderMap, erasedTargetNames);
    } catch (Exception e) {
      logParsingError(element, BindLayout.class, e);
    }
  }
parseBindLayout方法主要对注解进行了检查,然后从map缓存中获取bulider,若没有则新建加入map,这里bulider
传入一个layoutID的参数即可
private void parseBindLayout(Element element, Map<TypeElement, BindingSet.Builder> targetClassMap,
                             Set<TypeElement> erasedTargetNames) {
  TypeElement typeElement = (TypeElement) element;
  Set<Modifier> modifiers = element.getModifiers();

  if (modifiers.contains(PRIVATE)) {
    error(element, "@%s %s must not be private. (%s.%s)",
            BindLayout.class.getSimpleName(), "types", typeElement.getQualifiedName(),
            element.getSimpleName());
    return;
  }

  String qualifiedName = typeElement.getQualifiedName().toString();
  if (qualifiedName.startsWith("android.")) {
    error(element, "@%s-annotated class incorrectly in Android framework package. (%s)",
            BindLayout.class.getSimpleName(), qualifiedName);
    return;
  }

  if (qualifiedName.startsWith("java.")) {
    error(element, "@%s-annotated class incorrectly in Java framework package. (%s)",
            BindLayout.class.getSimpleName(), qualifiedName);
    return;
  }

  int layoutId = typeElement.getAnnotation(BindLayout.class).value();
  if(layoutId == 0){
    error(element, "@%s for a Activity must specify one layout ID. Found: %s. (%s.%s)",
            BindLayout.class.getSimpleName(), layoutId, typeElement.getQualifiedName(),
            element.getSimpleName());
    return;
  }
  BindingSet.Builder builder = targetClassMap.get(typeElement);

  if (builder == null) {
      builder = getOrCreateBindingBuilder(targetClassMap, typeElement);
  }

  builder.setContentLayoutId(layoutId);
  log(element, "element:" + element + "; targetMap:" + targetClassMap + "; erasedNames: " + erasedTargetNames);
}
相应的,需要在bulider和BuildSet里添加一个成员变量layoutId,bulider还再添加一个setContentLayoutId方法
static final class Builder {
    private final TypeName targetTypeName;
    private final ClassName bindingClassName;
    private final boolean isFinal;
    private final boolean isView;
    private final boolean isActivity;
    private final boolean isDialog;
    private int layoutId;
    private BindingSet parentBinding;

    private final Map<Id, ViewBinding.Builder> viewIdMap = new LinkedHashMap<>();
    private final ImmutableList.Builder<FieldCollectionViewBinding> collectionBindings =
            ImmutableList.builder();
    private final ImmutableList.Builder<ResourceBinding> resourceBindings = ImmutableList.builder();
    void setContentLayoutId(@LayoutRes int layoutId) {
        this.layoutId = layoutId;
    }...
以上工作便是获取BindLayout注解的值layoutId,并且传入builder构造出BindingSet,而javaPoet便是根据
Bindingset里面定义的规则来生成java文件的,接下来便是修改Bindingset里面的文件生成规则。
整个流程在createType这个方法里,包括生成类,构造器,view、事件绑定,解绑方法等
private TypeSpec createType(int sdk) {
    TypeSpec.Builder result = TypeSpec.classBuilder(bindingClassName.simpleName())
            .addModifiers(PUBLIC);
    if (isFinal) {
        result.addModifiers(FINAL);
    }
    if (parentBinding != null) {
        result.superclass(parentBinding.bindingClassName);
    } else {
        result.addSuperinterface(UNBINDER);
    }
    if (hasTargetField()) {
        result.addField(targetTypeName, "target", PRIVATE);
    }
    if (isFragBindLayout()) {
        result.addField(VIEW, "source", PRIVATE);
    }
    if (isView) {
        result.addMethod(createBindingConstructorForView());
    } else if (isActivity) {
        result.addMethod(createBindingConstructorForActivity());
    } else if (isDialog) {
        result.addMethod(createBindingConstructorForDialog());
    }
    if (!constructorNeedsView()) {
        // Add a delegating constructor with a target type + view signature for reflective use.
        result.addMethod(createBindingViewDelegateConstructor());
    }
    result.addMethod(createBindingConstructor(sdk));
    if (hasViewBindings() || parentBinding == null) {
        result.addMethod(createBindingUnbindMethod(result));
        result.addMethod(createGetLayoutMethod());
    }
    return result.build();
}
我们先来看 createBindingConstructor (sdk) 这个方法是生成Activity两个参数构造器的方法(看上面贴出的代码里的
SimpleActivity_ViewBinding),这里this.target = target 后面添加了setContentView方法
private MethodSpec createBindingConstructor(int sdk) {
	...
    if (hasTargetField()) {
        constructor.addStatement("this.target = target");
        constructor.addCode("\n");
    }
    if (layoutId != 0 && isActivity) {
        constructor.addStatement("target.setContentView($L)", layoutId);
        constructor.addCode("\n");
    }
	...
    return constructor.build();
}
至此Activity的Layout注入已经完成,生成的_ViewBinding文件如下,
 
public class SimpleActivity_ViewBinding implements Unbinder {
	...
  @UiThread
  public SimpleActivity_ViewBinding(SimpleActivity target, View source) {
    this.target = target;
    target.setContentView(2130903085);
    target.title = Utils.findRequiredViewAsType(source, R.id.title, "field 'title'", TextView.class);
  }
接下来处理Fragment的Layout注入,Fragment比较麻烦,为什么呢,因为Fragment的layout是在onCreatView回调里面
通过传入LayoutInflater加载Layout再返回,不能像Activity直接调用setConteneView,所以必须调用ButterKnife
的绑定方法时候返回View给onCreatView,如下:
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
    super.onCreateView(inflater, container, savedInstanceState);
    View rootView = ButterKnife.bind(this,inflater,container);
    return rootView;
}
因为bind方法返回的是UnBinder,所以先在UnBinder里面添加Layout返回的方法,传入两个参数,具体实现是通过
inflater.inflate(LayoutId, container, false)来加载Layout
 
UnBinder如下:
public interface Unbinder {
  @UiThread void unbind();
  @UiThread
  Object getLayout( LayoutInflater inflater, ViewGroup container);

  Unbinder EMPTY = new Unbinder() {
    @Override public void unbind() { }

    @Override
    public Object getLayout(LayoutInflater inflater, ViewGroup container) {
      return null;
    }
  };
}
在ButterKnife.java里添加bind方法:
@NonNull
@UiThread
public static View bind(@NonNull Fragment target, @NonNull LayoutInflater inflater, @NonNull ViewGroup container) {
    return (View) createBinding(target, null).getLayout(inflater, container);
}
做好以上工作后就可以去BindingSet修改文件生成规则了,为方便区分先在BindingSet里添加方法判断
private boolean isFragBindLayout() {
    return layoutId != 0 && !isActivity;
}
因为我们需要返回Layout,所以先定义成员变量source,同时因为每个_ViewBind文件都实现了UnBinder接口,
所以需要生成GetLayout方法
private TypeSpec createType(int sdk) {
	...
    if (isFragBindLayout()) {
        result.addField(VIEW, "source", PRIVATE);
    }
	...
    if (hasViewBindings() || parentBinding == null) {
        result.addMethod(createBindingUnbindMethod(result));
        result.addMethod(createGetLayoutMethod());
    }
    return result.build();
}
接下来看看createGetLayoutMethod(),除了一些参数的配置外,主要就是判断当是Fragment绑定Layout,则生成
inflate加载layoutId代码并赋值给成员量source,接着判断是否Fragment绑定Layout和存在View,事件绑定,
生成对应的findView,setonClick...代码,最后就是返回source,如果是Activity误调用了getLayout方法,
则抛出异常"sorry,you can't call this way"
private MethodSpec createGetLayoutMethod() {
    MethodSpec.Builder result = MethodSpec.methodBuilder("getLayout")
            .returns(TypeName.OBJECT)
            .addAnnotation(Override.class)
            .addModifiers(PUBLIC)
            .addParameter(LAYOUTINFLATER, "inflater")
            .addParameter(VIEWGROUP, "container");
    if (isFragBindLayout()) {
        result.addStatement("source = inflater.inflate($L, container, false)", layoutId);
        result.addCode("\n");
    }
    if (isFragBindLayout() && hasViewBindings()) {
        if (hasViewLocal()) {
            result.addStatement("$T view", VIEW);
        }
        for (ViewBinding binding : viewBindings) {
            addViewBinding(result, binding);
        }
        for (FieldCollectionViewBinding binding : collectionBindings) {
            result.addStatement("$L", binding.render());
        }
        if (!resourceBindings.isEmpty()) {
            result.addCode("\n");
        }
    }
    if (!isFinal && parentBinding == null) {
        result.addAnnotation(CALL_SUPER);
    }
    if (isFragBindLayout()) {
        result.addStatement("return source");
    } else {
        result.addStatement("throw new $T($S)", IllegalStateException.class,
                "sorry,you can't call this way");
    }
    return result.build();
}
因为在getLayout方法里初始化View,所以就不在构造器生成初始化View的代码了:
private MethodSpec createBindingConstructor(int sdk) {
     ...
    if (!isFragBindLayout() && hasViewBindings()) {
        if (hasViewLocal()) {
            // Local variable in which all views will be temporarily stored.
            constructor.addStatement("$T view", VIEW);
        }
        for (ViewBinding binding : viewBindings) {
            addViewBinding(constructor, binding);
        }
        for (FieldCollectionViewBinding binding : collectionBindings) {
            constructor.addStatement("$L", binding.render());
        }
        if (!resourceBindings.isEmpty()) {
            constructor.addCode("\n");
        }
    }
	...
    return constructor.build();
}
至此Fragment的layout注入工作已经大功告成,假如你的Fragment如下:
@BindLayout(R.layout.simple_activity)
public class SimpleFragment extends Fragment {
    @BindView(R.id.title)TextView title;
    @OnClick(R.id.subtitle)void click(){ }
    @Override
    public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
        super.onCreateView(inflater, container, savedInstanceState);
        View rootView = ButterKnife.bind(this,inflater,container);
        return rootView;
    }
}
那么生成的SimpleFragment_ViewBinding如下:
public class SimpleFragment_ViewBinding implements Unbinder {
  private SimpleFragment target;

  private View source;

  private View view2131361902;

  @UiThread
  public SimpleFragment_ViewBinding(final SimpleFragment target, View source) {
    this.target = target;

  }

  @Override
  @CallSuper
  public void unbind() {
    SimpleFragment target = this.target;
    if (target == null) throw new IllegalStateException("Bindings already cleared.");
    this.target = null;

    target.title = null;

    view2131361902.setOnClickListener(null);
    view2131361902 = null;
  }

  @Override
  @CallSuper
  public Object getLayout(LayoutInflater inflater, ViewGroup container) {
    source = inflater.inflate(2130903085, container, false);

    View view;
    target.title = Utils.findRequiredViewAsType(source, R.id.title, "field 'title'", TextView.class);
    view = Utils.findRequiredView(source, R.id.subtitle, "method 'click'");
    view2131361902 = view;
    view.setOnClickListener(new DebouncingOnClickListener() {
      @Override
      public void doClick(View p0) {
        target.click();
      }
    });
    return source;
  }
}
可以看出Layout的注入是成功的,它在getLayout里被加载,然后做一些View和事件绑定的操作,最后返回,最终返
到Fragment的onCreateView方法中。以上便是全部思路,是基于最新版Butterknife8.4.0上做的修改,如果有需
要源码的同学请在评论区留下邮箱。
 


 

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值