Android 浅析 ButterKnife (二) 源码解析

Android 浅析 ButterKnife (二) 源码解析


前言

Linus Benedict Torvalds : RTFSC – Read The Fucking Source Code

概括

这章将会根据由浅入深的过程浅析ButterKnife的注解方式。

工程结构

  • butterknife
  • butterknife-annotations
  • butterknife-compiler

可以看到工程的目录结构还是很清楚的
butterknife负责工程的主要逻辑调用入口。
butterknife-annotations负责所有注解的自定义文件。
butterknife-compiler负责在编译时解析Annotations。

butterknife-annotations

这里面的文件都是由注解组成。

/**
 * Bind a field to the view for the specified ID. The view will automatically be cast to the field
 * type.
 * <pre><code>
 * {@literal @}Bind(R.id.title) TextView title;
 * </code></pre>
 */
@Retention(CLASS) @Target(FIELD)
public @interface Bind {
  /** View ID to which the field will be bound. */
  @IdRes int[] value();
}

例如我们最经典的Bind方法的注解就是在这里定义的,这里通过注释我们可以很直接知道这些注解做的是什么就不再一一解析了。

butterknife-compiler

接下来我们看编译过程,这里是butterknife的主要部分。
所有的注解要在编译时进行解析,都需要自定义一个类继承于javax.annotation.processing.AbstractProcessor,通过复写其中的方法来实现。

先来看下支持注解的类类型:

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 //
);

//获取支持注解的类型
@Override public Set<String> getSupportedAnnotationTypes() {
  Set<String> types = new LinkedHashSet<>();
  types.add(Bind.class.getCanonicalName());

  for (Class<? extends Annotation> listener : LISTENERS) {
    types.add(listener.getCanonicalName());
  }

  types.add(BindArray.class.getCanonicalName());
  types.add(BindBitmap.class.getCanonicalName());
  types.add(BindBool.class.getCanonicalName());
  types.add(BindColor.class.getCanonicalName());
  types.add(BindDimen.class.getCanonicalName());
  types.add(BindDrawable.class.getCanonicalName());
  types.add(BindInt.class.getCanonicalName());
  types.add(BindString.class.getCanonicalName());
  types.add(Unbinder.class.getCanonicalName());

  return types;
}

这里我们可以看到基本都涵盖了我们在butterknife-annotations自定义的文件。

接着我们来看主要的逻辑:

@Override public boolean process(Set<? extends TypeElement> elements, RoundEnvironment env) {
  //查找和解析注解
  Map<TypeElement, BindingClass> targetClassMap = findAndParseTargets(env);

  //循环遍历拿出注解中的键值
  for (Map.Entry<TypeElement, BindingClass> entry : targetClassMap.entrySet()) {
    TypeElement typeElement = entry.getKey();
    BindingClass bindingClass = entry.getValue();
    //写进文件,生成辅助类
    bindingClass.brewJava().writeTo(filer);
  }

  return true;
}

这里我们看到主要的process函数里实现的就三个功能
1:查找和解析注解;
2:循环遍历拿出注解中的键值;
3:写进文件,生成辅助类。

我们先来看第一个功能,查找和解析注解:

private Map<TypeElement, BindingClass> findAndParseTargets(RoundEnvironment env) {
    Map<TypeElement, BindingClass> targetClassMap = new LinkedHashMap<>();
    Set<TypeElement> erasedTargetNames = new LinkedHashSet<>();

    // Process each @Bind element.
    for (Element element : env.getElementsAnnotatedWith(Bind.class)) {
      if (!SuperficialValidation.validateElement(element)) continue;
      parseBind(element, targetClassMap, erasedTargetNames);
    }
    
    // Process each annotation that corresponds to a listener.
    for (Class<? extends Annotation> listener : LISTENERS) {
      findAndParseListener(env, listener, targetClassMap, erasedTargetNames);
    }
    
    // Process each @BindInt element.
    for (Element element : env.getElementsAnnotatedWith(BindInt.class)) {
      if (!SuperficialValidation.validateElement(element)) continue;
      try {
        parseResourceInt(element, targetClassMap, erasedTargetNames);
      } catch (Exception e) {
        logParsingError(element, BindInt.class, e);
      }
    }
    ...
}

我们拿了一个比较简单的@BindInt来分析。

private void parseResourceInt(Element element, Map<TypeElement, BindingClass> targetClassMap,
    Set<TypeElement> erasedTargetNames) {
  boolean hasError = false;
  TypeElement enclosingElement = (TypeElement) element.getEnclosingElement();
  ...
  // Verify common generated code restrictions.
  hasError |= isInaccessibleViaGeneratedCode(BindInt.class, "fields", element);
  hasError |= isBindingInWrongPackage(BindInt.class, element);

  if (hasError) {return;}

  // Assemble information on the field.
  String name = element.getSimpleName().toString();
  int id = element.getAnnotation(BindInt.class).value();

  BindingClass bindingClass = getOrCreateTargetClass(targetClassMap, enclosingElement);
  FieldResourceBinding binding = new FieldResourceBinding(id, name, "getInteger", false);
  bindingClass.addResource(binding);

  erasedTargetNames.add(enclosingElement);
}

这里我们首先看到isInaccessibleViaGeneratedCode(),它里面判断了三个点:

  1. 验证方法修饰符不能为privatestatic
  2. 验证包含类型不能为非Class
  3. 验证包含类的可见性并不是private

接着我们来看isBindingInWrongPackage,它判断了这个类的包名,包名不能以android.和java.开头,butterknife不可以在Android Framework和JDK框架内部使用。

最后就是调用getOrCreateTargetClass函数获取或者生成一个绑定类,并将之存入数组。

接下来我们肯定要分析最常用的注解@Bind了:

private void parseBind(Element element, Map<TypeElement, BindingClass> targetClassMap, Set<TypeElement> erasedTargetNames) {
  // Verify common generated code restrictions.
  if (isInaccessibleViaGeneratedCode(Bind.class, "fields", element)
      || isBindingInWrongPackage(Bind.class, element)) {
    return;
  }

  TypeMirror elementType = element.asType();
  if (elementType.getKind() == TypeKind.ARRAY) {
    parseBindMany(element, targetClassMap, erasedTargetNames);
  } else if (LIST_TYPE.equals(doubleErasure(elementType))) {
    parseBindMany(element, targetClassMap, erasedTargetNames);
  } else {
    parseBindOne(element, targetClassMap, erasedTargetNames);
  }
}

看出这里第一步也是判断两个isInaccessibleViaGeneratedCode()isBindingInWrongPackage的条件,接着通过parseBindMany()来解析这个注解:

  1. 验证这个类型是一个List还是一个array
  2. 验证这个目标类型是否继承自View
  3. 在作用域里组装信息。
  4. 当然,最后将生成的BindingClass存入数组中。

分析完findAndParseTargets我们接着回到process往下看bindingClass.brewJava()

JavaFile brewJava() {
    TypeSpec.Builder result = TypeSpec.classBuilder(className)
        .addModifiers(PUBLIC)
        .addTypeVariable(TypeVariableName.get("T", ClassName.bestGuess(targetClass)));

    if (parentViewBinder != null) {
      result.superclass(ParameterizedTypeName.get(ClassName.bestGuess(parentViewBinder),
          TypeVariableName.get("T")));
    } else {
      result.addSuperinterface(ParameterizedTypeName.get(VIEW_BINDER, TypeVariableName.get("T")));
    }

    if (hasUnbinder()) {
      result.addType(createUnbinderClass());
    }

    result.addMethod(createBindMethod());

    return JavaFile.builder(classPackage, result.build())
        .addFileComment("Generated code from Butter Knife. Do not modify!")
        .build();
  }

这个函数通过将我们绑定类的信息写入到文件外还负责创建绑定和创建解除绑定。
在将绑定函数写入到文件后,整个编译器的注解方法就结束了。接下来就到运行时的注解过程了。

butterknife

Field and method binding for Android views. Use this class to simplify finding views and attaching listeners by binding them with annotations.
这是关于ButterKnife类的说明,关于ButterKnife的使用基本都是通过这个主类来实现的。
这里面最关键的函数就数bind()了:

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

每个bind函数实际上都是通过相同的函数不同参数实现。

static void bind(@NonNull Object target, @NonNull Object source, @NonNull Finder finder) {
    Class<?> targetClass = target.getClass();

    ViewBinder<Object> viewBinder = findViewBinderForClass(targetClass);
    viewBinder.bind(finder, target, source);
}

首先通过findViewBinderForClass生成每个类,然后再调用这些类的bind方法进行绑定,这些类的方法都是在编译期通过createBindMethod()方法一个个生成的。这些也就是辅助类的用处了。

总结

以上就是对ButterKnife的浅析,具体来说就是在你写下注解后,ButterKnife会帮你在编译期帮你把代码补全,然后在运行期来调用。

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

推荐阅读更多精彩内容