ButterKnife源码解析


image.png

序言

继上次介绍了EventBus之后,今天我们来介绍另外一个好用的三方库ButterKnife,一个依赖注入框架。该库的作者是被称为Android之神的JakeWarton,我们所熟知的OKHttp、Retrofit、ButterKnife、RxAndroid、RxBinding都是他的杰作。
在使用ButterKnife之前,我们写过最多的代码大概是:

TextView tv = (TextView) findViewById(R.id.tv);

而有了ButterKnife之后,我们就可以告别findViewById(),ButterKnife会自动帮我们生成这些代码。接下来,抱着膜拜大神的态度,让我们一起学习ButterKnife。

ButterKnife的使用

ButterKnife的使用流程如下:

  1. 在工程目录下的build.gradle文件中添加ButterKnife插件路径
classpath "com.jakewharton:butterknife-gradle-plugin:8.6.0"
  1. 在module的build.gradle文件中添加依赖和注解解析器:
api 'com.jakewharton:butterknife:8.8.1'
kapt 'com.jakewharton:butterknife-compiler:8.6.0'
  1. 接下来在Activity或者Fragment的onCreate/onCreateView方法中绑定当前对象:
@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    Unbinder binder = ButterKnife.bind(this);
}
  1. 然后在需要注入的View上加注解,此处以BindView注解为例:
@BindView(R.id.rbStock)
RadioButton rbStock;
//省略其他控件
  1. 现在,就可以在代码中使用这些控件,而不需要通过findViewById初始化这些控件。
  2. 最后,在Activity的onDestroy方法中从ButterKnife解绑:
@Override
protected void onDestroy() {
    super.onDestroy();
    binder.unbind();
}

到这里,ButterKnife的使用方法介绍完了,可以看到使用过程比较简单。

源码分析

1. ButterKnife绑定Activity/Fragment

我们从ButterKnife绑定Activity入手,来分析原理。
这里,我们用ButterKnife绑定项目的MainActivity:

ButterKnife.bind(this);

跟踪ButterKnife的bind方法:

@NonNull @UiThread
public static Unbinder bind(@NonNull Activity target) {
    View sourceView = target.getWindow().getDecorView();
    return createBinding(target, sourceView);
}

这里会获取Activity对应Window的DecorView,然后调用createBinding方法:

private static Unbinder createBinding(@NonNull Object target, @NonNull View source) {
    Class<?> targetClass = target.getClass();
    if (debug) Log.d(TAG, "Looking up binding for " + targetClass.getName());
    Constructor<? extends Unbinder> constructor = findBindingConstructorForClass(targetClass);
    
    if (constructor == null) {
      return Unbinder.EMPTY;
    }
    
    //noinspection TryWithIdenticalCatches Resolves to API 19+ only type.
    try {
      return constructor.newInstance(target, source);
    } catch (IllegalAccessException e) {
      throw new RuntimeException("Unable to invoke " + constructor, e);
    } catch (InstantiationException e) {
      throw new RuntimeException("Unable to invoke " + constructor, e);
    } catch (InvocationTargetException e) {
      Throwable cause = e.getCause();
      if (cause instanceof RuntimeException) {
        throw (RuntimeException) cause;
      }
      if (cause instanceof Error) {
        throw (Error) cause;
      }
      throw new RuntimeException("Unable to create binding instance.", cause);
    }
}

着重看一下findBindingConstructorForClass方法:

@Nullable @CheckResult @UiThread
private static Constructor<? extends Unbinder> findBindingConstructorForClass(Class<?> cls) {
    Constructor<? extends Unbinder> bindingCtor = BINDINGS.get(cls);
    if (bindingCtor != null) {
      if (debug) Log.d(TAG, "HIT: Cached in binding map.");
      return bindingCtor;
    }
    String clsName = cls.getName();
    if (clsName.startsWith("android.") || clsName.startsWith("java.")) {
      if (debug) Log.d(TAG, "MISS: Reached framework class. Abandoning search.");
      return null;
    }
    try {
      Class<?> bindingClass = cls.getClassLoader().loadClass(clsName + "_ViewBinding");
      //noinspection unchecked
      bindingCtor = (Constructor<? extends Unbinder>) bindingClass.getConstructor(cls, View.class);
      if (debug) Log.d(TAG, "HIT: Loaded binding class and constructor.");
    } catch (ClassNotFoundException e) {
      if (debug) Log.d(TAG, "Not found. Trying superclass " + cls.getSuperclass().getName());
      bindingCtor = findBindingConstructorForClass(cls.getSuperclass());
    } catch (NoSuchMethodException e) {
      throw new RuntimeException("Unable to find binding constructor for " + clsName, e);
    }
    BINDINGS.put(cls, bindingCtor);
    return bindingCtor;
}

该方法接收一个Class对象,即我们的MainActivity对应的Class对象。首先会从BINDINGS中寻找是否存在构造器,BINDINGS定义如下:

@VisibleForTesting
static final Map<Class<?>, Constructor<? extends Unbinder>> BINDINGS = new LinkedHashMap<>();

它是一个Map,键是一个Class对象,值是一个构造器,至于是什么的构造器下面会揭晓答案。
继续上面的分析,如果在BINDINGS中找到构造器,则直接返回构造器。如果没有找到,就校验这个Class对象是否合法,如果合法,就执行下面一句代码:

Class<?> bindingClass = cls.getClassLoader().loadClass(clsName + "_ViewBinding");

加载一个名为MainActivity_ViewBinding的类,并且获取它的构造器,并保存在BINDINGS中,下次再bind这个类的时候,就直接从BINDINGS中获取构造器,提升性能。
MainActivity_ViewBinding是通过APT(编译时解析技术)生成的类。后面会介绍具体的生成过程。

再回到createBinding方法,获取到MainActivity_ViewBinding的构造器之后,会调用构造器的newInstance方法构建一个MainActivity_ViewBinding对象。我们看一下MainActivity_ViewBinding的定义:

public class MainActivity_ViewBinding implements Unbinder {
    private MainActivity target;
    
    @UiThread
    public MainActivity_ViewBinding(MainActivity target) {
      this(target, target.getWindow().getDecorView());
    }
    
    @UiThread
    public MainActivity_ViewBinding(MainActivity target, View source) {
      this.target = target;
    
      target.rbStock = Utils.findRequiredViewAsType(source, R.id.rbStock, "field 'rbStock'", RadioButton.class);
      //省略其他控件初始化代码
    }
    
    @Override
    @CallSuper
    public void unbind() {
      MainActivity target = this.target;
      if (target == null) throw new IllegalStateException("Bindings already cleared.");
      this.target = null;
    
      target.rbStock = null;
      //省略重复其他控置空代码
    }
}

这个类的构造方法中初始化了带@BindView注解的控件,我们看一下具体初始化过程:

public static <T> T findRequiredViewAsType(View source, @IdRes int id, String who,
      Class<T> cls) {
    View view = findRequiredView(source, id, who);
    return castView(view, id, who, cls);
}

public static View findRequiredView(View source, @IdRes int id, String who) {
    View view = source.findViewById(id);
    if (view != null) {
      return view;
    }
    String name = getResourceEntryName(source, id);
    throw new IllegalStateException("Required view '"
        + name
        + "' with ID "
        + id
        + " for "
        + who
        + " was not found. If this view is optional add '@Nullable' (fields) or '@Optional'"
        + " (methods) annotation.");
}

在findRequiredView方法中,我们看见了熟悉的findViewById方法,到这里相信大家都清楚了:ButterKnife的原理是通过APT技术在编译过程生成了一个XX_ViewBinding类,之后在绑定的时候,会创建XX_ViewBinding对象,而XX_ViewBinding的构造方法中会帮我们出初始化这些注解修饰的控件

MainActivity_ViewBinding实现了Unbinder接口,我们看一下接口定义:

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

很简单,只有一个unbind方法,而MainActivity_ViewBinding在unbind方法中把注解对应的控件和target都置为null,释放资源。就是使用过程第6步在onDestroy中会调用该方法。

2. 编译时生成_ViewBinding类

接下来,我们看一下怎样通过APT技术生成MainActivity_ViewBinding类。了解APT的同学会知道,主要是通过AbstractProcessor类在编译过程中处理注解。
下面,我们就看一下ButterKnife对应注解处理类ButterKnifeProcessor

@AutoService(Processor.class)
public final class ButterKnifeProcessor extends AbstractProcessor {
    private static final String OPTION_SDK_INT = "butterknife.minSdk";
    static final Id NO_ID = new Id(-1);
    static final String VIEW_TYPE = "android.view.View";
    static final String ACTIVITY_TYPE = "android.app.Activity";
    static final String DIALOG_TYPE = "android.app.Dialog";
    private static final String COLOR_STATE_LIST_TYPE = "android.content.res.ColorStateList";
    private static final String BITMAP_TYPE = "android.graphics.Bitmap";
    private static final String DRAWABLE_TYPE = "android.graphics.drawable.Drawable";
    private static final String TYPED_ARRAY_TYPE = "android.content.res.TypedArray";
    private static final String NULLABLE_ANNOTATION_NAME = "Nullable";
    private static final String STRING_TYPE = "java.lang.String";
    private static final String LIST_TYPE = List.class.getCanonicalName();
    private static final List<Class<? extends Annotation>> LISTENERS = Arrays.asList(OnCheckedChanged.class, OnClick.class, OnEditorAction.class, OnFocusChange.class, OnItemClick.class, OnItemLongClick.class, OnItemSelected.class, OnLongClick.class, OnPageChange.class, OnTextChanged.class, OnTouch.class);
    private static final List<String> SUPPORTED_TYPES = Arrays.asList("array", "attr", "bool", "color", "dimen", "drawable", "id", "integer", "string");
    private Elements elementUtils;
    private Types typeUtils;
    private Filer filer;
    private Trees trees;
    private int sdk = 1;
    private final Map<QualifiedId, Id> symbols = new LinkedHashMap();

    public ButterKnifeProcessor() {
    }

    //初始化一些参数和工具类
    public synchronized void init(ProcessingEnvironment env) {
        super.init(env);
        String sdk = (String)env.getOptions().get("butterknife.minSdk");
        if (sdk != null) {
            try {
                this.sdk = Integer.parseInt(sdk);
            } catch (NumberFormatException var5) {
                env.getMessager().printMessage(Kind.WARNING, "Unable to parse supplied minSdk option '" + sdk + "'. Falling back to API 1 support.");
            }
        }

        this.elementUtils = env.getElementUtils();
        this.typeUtils = env.getTypeUtils();
        this.filer = env.getFiler();

        try {
            this.trees = Trees.instance(this.processingEnv);
        } catch (IllegalArgumentException var4) {
            ;
        }

    }

    //获取该Processor能处理的注解集合
    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(BindView.class);
        annotations.add(BindViews.class);
        annotations.addAll(LISTENERS);
        return annotations;
    }

    //主要的处理方法,分析将从这里开始
    public boolean process(Set<? extends TypeElement> elements, RoundEnvironment env) {
        Map<TypeElement, BindingSet> bindingMap = this.findAndParseTargets(env);
        Iterator var4 = bindingMap.entrySet().iterator();

        while(var4.hasNext()) {
            Entry<TypeElement, BindingSet> entry = (Entry)var4.next();
            TypeElement typeElement = (TypeElement)entry.getKey();
            BindingSet binding = (BindingSet)entry.getValue();
            JavaFile javaFile = binding.brewJava(this.sdk);

            try {
                javaFile.writeTo(this.filer);
            } catch (IOException var10) {
                this.error(typeElement, "Unable to write binding for type %s: %s", typeElement, var10.getMessage());
            }
        }

        return false;
    }
    
    //省略很多方法
}
    

我们就直接从process方法开始分析,这个方法的逻辑比较简单,通过findAndParseTargets方法找到所有使用ButterKnife的类,然后根据注解信息生成对应的_ViewBinding类
可以把这段代码分为两个过程:处理注解并记录信息生成_ViewBinding类

a. 处理注解并记录信息

先看一下findAndParseTargets方法:

private Map<TypeElement, BindingSet> findAndParseTargets(RoundEnvironment env) {
    Map<TypeElement, Builder> builderMap = new LinkedHashMap();
    Set<TypeElement> erasedTargetNames = new LinkedHashSet();
    this.scanForRClasses(env);

    //省略处理其他注解的代码
    
    var4 = env.getElementsAnnotatedWith(BindView.class).iterator();

    while(var4.hasNext()) {
        element = (Element)var4.next();

        try {
            this.parseBindView(element, builderMap, erasedTargetNames);
        } catch (Exception var12) {
            this.logParsingError(element, BindView.class, var12);
        }
    }

    //省略处理其他注解的代码

    var4 = LISTENERS.iterator();

    while(var4.hasNext()) {
        Class<? extends Annotation> listener = (Class)var4.next();
        this.findAndParseListener(env, listener, builderMap, erasedTargetNames);
    }

    Deque<Entry<TypeElement, Builder>> entries = new ArrayDeque(builderMap.entrySet());
    LinkedHashMap bindingMap = new LinkedHashMap();

    while(!entries.isEmpty()) {
        Entry<TypeElement, Builder> entry = (Entry)entries.removeFirst();
        TypeElement type = (TypeElement)entry.getKey();
        Builder builder = (Builder)entry.getValue();
        TypeElement parentType = this.findParentType(type, erasedTargetNames);
        if (parentType == null) {
            bindingMap.put(type, builder.build());
        } else {
            BindingSet parentBinding = (BindingSet)bindingMap.get(parentType);
            if (parentBinding != null) {
                builder.setParent(parentBinding);
                bindingMap.put(type, builder.build());
            } else {
                entries.addLast(entry);
            }
        }
    }

    return bindingMap;
}

这个方法很长,主要的作用就是找到项目中所有使用ButterKnife注解的类,分别处理他们,最后把他们的信息保存在一个Map中:

LinkedHashMap<ElementType, BindingSet> bindingMap = new LinkedHashMap();

这个Map以ElementType(使用注解的类)为key,值则是一个BindingSet对象(记录了类中用到的注解及作用对象等信息)。后面就是通过这个Map生成对应的_ViewBinding类。
接下来,我们分析一下注解BindView的处理过程,主要是通过parseBindView方法处理:

private void parseBindView(Element element, Map<TypeElement, Builder> builderMap, Set<TypeElement> erasedTargetNames) {
    TypeElement enclosingElement = (TypeElement)element.getEnclosingElement();
    //检查是否满足使用注解的条件
    boolean hasError = this.isInaccessibleViaGeneratedCode(BindView.class, "fields", element) || this.isBindingInWrongPackage(BindView.class, element);
    TypeMirror elementType = element.asType();
    if (elementType.getKind() == TypeKind.TYPEVAR) {
        TypeVariable typeVariable = (TypeVariable)elementType;
        elementType = typeVariable.getUpperBound();
    }

    Name qualifiedName = enclosingElement.getQualifiedName();
    Name simpleName = element.getSimpleName();
    //不满足条件,抛出异常
    if (!isSubtypeOfType(elementType, "android.view.View") && !this.isInterface(elementType)) {
        if (elementType.getKind() == TypeKind.ERROR) {
            this.note(element, "@%s field with unresolved type (%s) must elsewhere be generated as a View or interface. (%s.%s)", BindView.class.getSimpleName(), elementType, qualifiedName, simpleName);
        } else {
            this.error(element, "@%s fields must extend from View or be an interface. (%s.%s)", BindView.class.getSimpleName(), qualifiedName, simpleName);
            hasError = true;
        }
    }
    //满足条件,开始解析注解
    if (!hasError) {
        int id = ((BindView)element.getAnnotation(BindView.class)).value();
        //从缓存builderMap中查找注解使用类对应的builder对象
        Builder builder = (Builder)builderMap.get(enclosingElement);
        QualifiedId qualifiedId = this.elementToQualifiedId(element, id);
        String existingBindingName;
        if (builder != null) {
            existingBindingName = builder.findExistingBindingName(this.getId(qualifiedId));
            if (existingBindingName != null) {
                this.error(element, "Attempt to use @%s for an already bound ID %d on '%s'. (%s.%s)", BindView.class.getSimpleName(), id, existingBindingName, enclosingElement.getQualifiedName(), element.getSimpleName());
                return;
            }
        } else {
            //如果缓存中没有找到注解使用类对应的builder对象,则创建这个对象,并且保存在builderMap中
            builder = this.getOrCreateBindingBuilder(builderMap, enclosingElement);
        }

        existingBindingName = simpleName.toString();
        TypeName type = TypeName.get(elementType);
        boolean required = isFieldRequired(element);
        //把注解BindView修饰的字段添加到builder中
        builder.addField(this.getId(qualifiedId), new FieldViewBinding(existingBindingName, type, required));
        erasedTargetNames.add(enclosingElement);
    }
}

代码中关键的地方给出了注释,各位同学可以根据注释自行理解代码。其他注解的处理方式跟注解BindView类似,这里不做介绍。就这样,把类和它对应的注解信息都保存到Map中。

b. 生成_ViewBinding类

我们再来看一下process方法:

public boolean process(Set<? extends TypeElement> elements, RoundEnvironment env) {
    Map<TypeElement, BindingSet> bindingMap = this.findAndParseTargets(env);
    Iterator var4 = bindingMap.entrySet().iterator();

    while(var4.hasNext()) {
        Entry<TypeElement, BindingSet> entry = (Entry)var4.next();
        TypeElement typeElement = (TypeElement)entry.getKey();
        BindingSet binding = (BindingSet)entry.getValue();
        JavaFile javaFile = binding.brewJava(this.sdk);

        try {
            javaFile.writeTo(this.filer);
        } catch (IOException var10) {
            this.error(typeElement, "Unable to write binding for type %s: %s", typeElement, var10.getMessage());
        }
    }

    return false;
}

通过第一步得到了bindingMap对象,接下来遍历bindingMap,通过BindingSet的brewJava方法生成对应的ViewBinding类:

JavaFile brewJava(int sdk) {
    return JavaFile.builder(this.bindingClassName.packageName(), this.createType(sdk)).addFileComment("Generated code from Butter Knife. Do not modify!", new Object[0]).build();
}

static BindingSet.Builder newBuilder(TypeElement enclosingElement) {
    TypeMirror typeMirror = enclosingElement.asType();
    boolean isView = ButterKnifeProcessor.isSubtypeOfType(typeMirror, "android.view.View");
    boolean isActivity = ButterKnifeProcessor.isSubtypeOfType(typeMirror, "android.app.Activity");
    boolean isDialog = ButterKnifeProcessor.isSubtypeOfType(typeMirror, "android.app.Dialog");
    TypeName targetType = TypeName.get(typeMirror);
    if (targetType instanceof ParameterizedTypeName) {
        targetType = ((ParameterizedTypeName)targetType).rawType;
    }

    String packageName = MoreElements.getPackage(enclosingElement).getQualifiedName().toString();
    String className = enclosingElement.getQualifiedName().toString().substring(packageName.length() + 1).replace('.', '$');
    ClassName bindingClassName = ClassName.get(packageName, className + "_ViewBinding", new String[0]);
    boolean isFinal = enclosingElement.getModifiers().contains(Modifier.FINAL);
    return new BindingSet.Builder((TypeName)targetType, bindingClassName, isFinal, isView, isActivity, isDialog);
}

这里就不深入介绍生成ViewBinding类的过程,它使用到了javapoet技术,javapoet技术是square公司开源的,专门用来生成java源码,很多使用APT技术的库都是使用到javapoet技术。

总结

到这里,ButterKnife的原理我们就介绍完了,各位同学按照本文思路,对着ButterKnife源码进行学习。
如果各位同学使用过kotlin,就知道kotlin中不需要ButterKnife,kotlin提供了插件,可以直接获取xml中的控件,还提供了一个功能强大又易用的库anko,有兴趣的小伙伴可以了解一下。

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