Butterknife源碼解析

前言

Jake Wharton大神的Butterknife可謂是造福廣大Android開發(fā)者, 再也不用重復(fù)寫findViewById和setOnClickListener了.但是用了這么久的Butterknife, 一直以為用的是反射, 直到看了源碼...(路漫漫其修遠(yuǎn)兮,吾將上下而求索)

Butterknife的使用

Git地址: https://github.com/JakeWharton/butterknife, 里面有具體的引用及使用說明, 這里不再介紹.

Butterknife的原理

談到Butterknife的原理, 不得不先提一下運行時注解和編譯時注解的區(qū)別:

  • 編譯時注解: (代碼生成)在編譯時掃描所有注解,對注解進(jìn)行處理,在項目中生成一份代碼,然后在項目運行時直接調(diào)用;
  • 運行時注解: (代碼注入)在代碼中通過注解進(jìn)行標(biāo)記,在運行時通過反射在原代碼的基礎(chǔ)上完成注入;

Butterknife就是用的編譯時注解(Annotation Processing Tool),簡稱APT技術(shù). 關(guān)于APT, 可以戳這里,里面有demo,介紹了編譯時自動生成代碼的整個過程. 看完這篇文章再看Butterknife就有章可循了.

Butterknife源碼解析

了解APT之后就知道了最重要的有兩步,一編譯時自動生成代碼,二運行時進(jìn)行代碼綁定.下面以Butterknife最新版本8.8.1的源碼進(jìn)行說明;

編譯時自動生成代碼

首先來看ButterKnifeProcessor類,繼承AbstractProcessor,需要復(fù)寫的方法:
init(): 主要初始化一些工具類

@Override 
public synchronized void init(ProcessingEnvironment env) {
    super.init(env);

    ... //獲取sdk和是否debug
    elementUtils = env.getElementUtils();//獲取元素相關(guān)信息的工具類
    typeUtils = env.getTypeUtils();//處理TypeMirror的工具類
    filer = env.getFiler();//生成java文件的工具類
    try {
      trees = Trees.instance(processingEnv);
    } catch (IllegalArgumentException ignored) {
    }
  }

getSupportedAnnotationTypes(): 返回支持的注解類型,包括BindAnim,BindArray等注解,以及OnClick,OnCheckedChanged等監(jiān)聽.

private Set<Class<? extends Annotation>> getSupportedAnnotations() {
    Set<Class<? extends Annotation>> annotations = new LinkedHashSet<>();

    annotations.add(BindAnim.class);
    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(BindFont.class);
    annotations.add(BindInt.class);
    annotations.add(BindString.class);
    annotations.add(BindView.class);
    annotations.add(BindViews.class);
    annotations.addAll(LISTENERS);

    return annotations;
  }

process():生成代碼的核心部分,分兩步,掃描處理代碼中所有的注解,然后通過javapoet生成java文件. 返回值表示這組注解是否被這個 Processor 接受,如果接受(返回true)后續(xù)子的 processor 不會再對這個注解進(jìn)行處理.

@Override public boolean process(Set<? extends TypeElement> elements, RoundEnvironment env) {
    //掃描代碼中所有的注解,存到map中
    Map<TypeElement, BindingSet> bindingMap = findAndParseTargets(env);

    for (Map.Entry<TypeElement, BindingSet> entry : bindingMap.entrySet()) {
      TypeElement typeElement = entry.getKey();
      BindingSet binding = entry.getValue();
     //生成java文件
      JavaFile javaFile = binding.brewJava(sdk, debuggable);
      try {
        javaFile.writeTo(filer);
      } catch (IOException e) {
        error(typeElement, "Unable to write binding for type %s: %s", typeElement, e.getMessage());
      }
    }
    return false;
  }

RoundEnvironment表示當(dāng)前或之前的運行環(huán)境,可以通過該對象查找注解.
TypeElement代表源代碼中的元素類型, 如包,類,屬性,方法, 源代碼中每一部分都是一種類型.包為PackageElement,類為TypeElement,屬性為VariableElement,方法為ExecuteableElement ,都是Element的子類.
然后看下findAndParseTargets方法是如何掃描所有注解的.

private Map<TypeElement, BindingSet> findAndParseTargets(RoundEnvironment env) {
    Map<TypeElement, BindingSet.Builder> builderMap = new LinkedHashMap<>();
    Set<TypeElement> erasedTargetNames = new LinkedHashSet<>();
    //建立view與id的關(guān)系
    scanForRClasses(env);

   // Process each @BindView element.
   //env.getElementsAnnotatedWith(BindView.class))獲取所有注解是BindView的元素
    for (Element element : env.getElementsAnnotatedWith(BindView.class)) {
      // we don't SuperficialValidation.validateElement(element)
      // so that an unresolved View type can be generated by later processing rounds
      try {
        parseBindView(element, builderMap, erasedTargetNames);
      } catch (Exception e) {
        logParsingError(element, BindView.class, e);
      }
    }    return bindingMap;
  }

首先看scanForRClasses()如何建立view與id的關(guān)系

private void scanForRClasses(RoundEnvironment env) {
    if (trees == null) return;
    //R文件掃描器,掃描代碼中所有的R文件
    RClassScanner scanner = new RClassScanner();

    for (Class<? extends Annotation> annotation : getSupportedAnnotations()) {
      for (Element element : env.getElementsAnnotatedWith(annotation)) {
        JCTree tree = (JCTree) trees.getTree(element, getMirror(element, annotation));
        if (tree != null) { // tree can be null if the references are compiled types and not source
          //獲取R文件的包名
          String respectivePackageName =
              elementUtils.getPackageOf(element).getQualifiedName().toString();
          scanner.setCurrentPackageName(respectivePackageName);
          tree.accept(scanner);
        }
      }
    }

    for (Map.Entry<String, Set<String>> packageNameToRClassSet : scanner.getRClasses().entrySet()) {
      String respectivePackageName = packageNameToRClassSet.getKey();
      for (String rClass : packageNameToRClassSet.getValue()) {
        //解析R文件
        parseRClass(respectivePackageName, rClass);
      }
    }
  }

parseRClass()利用IdScanner尋找R文件內(nèi)部類,如array,attr,string等

private void parseRClass(String respectivePackageName, String rClass) {
    Element element;

    try {
      element = elementUtils.getTypeElement(rClass);
    } catch (MirroredTypeException mte) {
      element = typeUtils.asElement(mte.getTypeMirror());
    }

    JCTree tree = (JCTree) trees.getTree(element);
    if (tree != null) { // tree can be null if the references are compiled types and not source
      //利用IdScanner尋找R文件內(nèi)部類,如array,attr,string等
      IdScanner idScanner = new IdScanner(symbols, elementUtils.getPackageOf(element)
          .getQualifiedName().toString(), respectivePackageName);
      tree.accept(idScanner);
    } else {
      parseCompiledR(respectivePackageName, (TypeElement) element);
    }
  }

在IdScanner類內(nèi)部利用VarScanner掃描R文件內(nèi)部類(id,string等)的屬性(鍵值對:資源名和id)

private static class IdScanner extends TreeScanner {
    private final Map<QualifiedId, Id> ids;
    private final String rPackageName;
    private final String respectivePackageName;

    IdScanner(Map<QualifiedId, Id> ids, String rPackageName, String respectivePackageName) {
      this.ids = ids;
      this.rPackageName = rPackageName;
      this.respectivePackageName = respectivePackageName;
    }

    @Override public void visitClassDef(JCTree.JCClassDecl jcClassDecl) {
      for (JCTree tree : jcClassDecl.defs) {
        if (tree instanceof ClassTree) {
          ClassTree classTree = (ClassTree) tree;
          String className = classTree.getSimpleName().toString();
          if (SUPPORTED_TYPES.contains(className)) {
            ClassName rClassName = ClassName.get(rPackageName, "R", className);
            VarScanner scanner = new VarScanner(ids, rClassName, respectivePackageName);
            ((JCTree) classTree).accept(scanner);
          }
        }
      }
    }
  }

在VarScanner類內(nèi)部記錄資源名稱及id

private static class VarScanner extends TreeScanner {
    private final Map<QualifiedId, Id> ids;
    private final ClassName className;
    private final String respectivePackageName;

    private VarScanner(Map<QualifiedId, Id> ids, ClassName className,
        String respectivePackageName) {
      this.ids = ids;
      this.className = className;
      this.respectivePackageName = respectivePackageName;
    }

    @Override public void visitVarDef(JCTree.JCVariableDecl jcVariableDecl) {
      if ("int".equals(jcVariableDecl.getType().toString())) {
        int id = Integer.valueOf(jcVariableDecl.getInitializer().toString());
        String resourceName = jcVariableDecl.getName().toString();
        QualifiedId qualifiedId = new QualifiedId(respectivePackageName, id);
        ids.put(qualifiedId, new Id(id, className, resourceName));
      }
    }
  }

到此就建立了所有view與id的關(guān)系,然后看如何處理注解, 以BindView為例(其他類似). 將每一個BindView注解的元素存放到被綁定類的成員變量中.

private void parseBindView(Element element, Map<TypeElement, BindingSet.Builder> builderMap,
      Set<TypeElement> erasedTargetNames) {
    //獲取BindView注解元素的父元素, 如MainActivity
    TypeElement enclosingElement = (TypeElement) element.getEnclosingElement();

    // Start by verifying common generated code restrictions.
    //isInaccessibleViaGeneratedCode檢查父元素是否是類且非private
    //isBindingInWrongPackage檢查父元素是否是系統(tǒng)相關(guān)的類
    boolean hasError = isInaccessibleViaGeneratedCode(BindView.class, "fields", element)
        || isBindingInWrongPackage(BindView.class, element);

    // Verify that the target type extends from View.
    TypeMirror elementType = element.asType();
    if (elementType.getKind() == TypeKind.TYPEVAR) {
      TypeVariable typeVariable = (TypeVariable) elementType;
      elementType = typeVariable.getUpperBound();
    }
    Name qualifiedName = enclosingElement.getQualifiedName();
    Name simpleName = element.getSimpleName();
    //判斷BindView注解的元素是view的子類或接口,否則有錯return
    if (!isSubtypeOfType(elementType, VIEW_TYPE) && !isInterface(elementType)) {
      if (elementType.getKind() == TypeKind.ERROR) {
        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 {
        error(element, "@%s fields must extend from View or be an interface. (%s.%s)",
            BindView.class.getSimpleName(), qualifiedName, simpleName);
        hasError = true;
      }
    }

    if (hasError) {
      return;
    }

    // Assemble information on the field.
    //獲取BindView注解的value,及id
    int id = element.getAnnotation(BindView.class).value();
    //builderMap緩存所有被綁定類的信息, 鍵是父元素(如MainActivity),值是鍵對應(yīng)的被綁定類的信息(如MainActivity_ViewBinding)
    //獲取BindingSet.Builder, 有則使用緩存,無則創(chuàng)建
    BindingSet.Builder builder = builderMap.get(enclosingElement);
    QualifiedId qualifiedId = elementToQualifiedId(element, id);
    if (builder != null) {
      //從被綁定類的成員變量中查找這個id,不為空,說明有多個view綁定了同一個id, 拋異常
      String existingBindingName = builder.findExistingBindingName(getId(qualifiedId));
      if (existingBindingName != null) {
        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 = getOrCreateBindingBuilder(builderMap, enclosingElement);
    }
    //獲取注解元素的屬性名,類型,是否必須
    String name = simpleName.toString();
    TypeName type = TypeName.get(elementType);
    boolean required = isFieldRequired(element);
    //加入到成員變量集合中
    builder.addField(getId(qualifiedId), new FieldViewBinding(name, type, required));

    // Add the type-erased version to the valid binding targets set.
    erasedTargetNames.add(enclosingElement);
  }

再看如何創(chuàng)建被綁定類BindingSet

private BindingSet.Builder getOrCreateBindingBuilder(
      Map<TypeElement, BindingSet.Builder> builderMap, TypeElement enclosingElement) {
    BindingSet.Builder builder = builderMap.get(enclosingElement);
    if (builder == null) {
      builder = BindingSet.newBuilder(enclosingElement);
      builderMap.put(enclosingElement, builder);
    }
    return builder;
  }
static Builder newBuilder(TypeElement enclosingElement) {
    //獲取注解父元素的類型,View/Activity/Dialog
    TypeMirror typeMirror = enclosingElement.asType();

    boolean isView = isSubtypeOfType(typeMirror, VIEW_TYPE);
    boolean isActivity = isSubtypeOfType(typeMirror, ACTIVITY_TYPE);
    boolean isDialog = isSubtypeOfType(typeMirror, DIALOG_TYPE);

    TypeName targetType = TypeName.get(typeMirror);
    if (targetType instanceof ParameterizedTypeName) {
      targetType = ((ParameterizedTypeName) targetType).rawType;
    }
    //獲取父元素的包名和類名
    String packageName = getPackage(enclosingElement).getQualifiedName().toString();
    String className = enclosingElement.getQualifiedName().toString().substring(
        packageName.length() + 1).replace('.', '$');
    //獲取綁定的類名,加后綴_ViewBinding(后面介紹生成加后綴_ViewBinding的java文件)
    ClassName bindingClassName = ClassName.get(packageName, className + "_ViewBinding");

    boolean isFinal = enclosingElement.getModifiers().contains(Modifier.FINAL);
    return new Builder(targetType, bindingClassName, isFinal, isView, isActivity, isDialog);
  }

到此完成了掃描所有注解的工作,再看如何生成java文件.就是process()中的binding.brewJava(sdk, debuggable);

JavaFile brewJava(int sdk, boolean debuggable) {
    //bindingClassName就是加了后綴_ViewBinding的類
    return JavaFile.builder(bindingClassName.packageName(), createType(sdk, debuggable))
        .addFileComment("Generated code from Butter Knife. Do not modify!")
        .build();
  }
private TypeSpec createType(int sdk, boolean debuggable) {
    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);
    }
    //添加構(gòu)造方法
    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, debuggable));

    if (hasViewBindings() || parentBinding == null) {
      result.addMethod(createBindingUnbindMethod(result));
    }

    return result.build();
  }

這里用到了javapoet,感興趣的可以戳這里, 通過TypeSpec生成類,并一步步地添加構(gòu)造方法等.
編譯之后就會在build目錄下會產(chǎn)生原Activity.class以及原Activity_ViewBinding.class兩份代碼,.來看下_ViewBinding代碼的結(jié)構(gòu),以官方SimpleActivity_ViewBinding為例.在構(gòu)造函數(shù)中生成控件以及事件監(jiān)聽,在unbind對所有控件,事件以及原Activity的引用置空,等待GC回收.

public class SimpleActivity_ViewBinding<T extends SimpleActivity> implements Unbinder {
  protected T target;

  private View view2130968578;

  private View view2130968579;

  @UiThread
  public SimpleActivity_ViewBinding(final T target, View source) {
    this.target = target;

    View view;
    //target就是SimpleActivity,所以SimpleActivity中的title,subtitle等被BindView注解的元素都不能是private的.
    target.title = Utils.findRequiredViewAsType(source, R.id.title, "field 'title'", TextView.class);
    target.subtitle = Utils.findRequiredViewAsType(source, R.id.subtitle, "field 'subtitle'", TextView.class);
    view = Utils.findRequiredView(source, R.id.hello, "field 'hello', method 'sayHello', and method 'sayGetOffMe'");
    target.hello = Utils.castView(view, R.id.hello, "field 'hello'", Button.class);
    view2130968578 = view;
    view.setOnClickListener(new DebouncingOnClickListener() {
      @Override
      public void doClick(View p0) {
        target.sayHello();
      }
    });
    view.setOnLongClickListener(new View.OnLongClickListener() {
      @Override
      public boolean onLongClick(View p0) {
        return target.sayGetOffMe();
      }
    });
    view = Utils.findRequiredView(source, R.id.list_of_things, "field 'listOfThings' and method 'onItemClick'");
    target.listOfThings = Utils.castView(view, R.id.list_of_things, "field 'listOfThings'", ListView.class);
    view2130968579 = view;
    ((AdapterView<?>) view).setOnItemClickListener(new AdapterView.OnItemClickListener() {
      @Override
      public void onItemClick(AdapterView<?> p0, View p1, int p2, long p3) {
        target.onItemClick(p2);
      }
    });
    target.footer = Utils.findRequiredViewAsType(source, R.id.footer, "field 'footer'", TextView.class);
    target.headerViews = Utils.listOf(
        Utils.findRequiredView(source, R.id.title, "field 'headerViews'"), 
        Utils.findRequiredView(source, R.id.subtitle, "field 'headerViews'"), 
        Utils.findRequiredView(source, R.id.hello, "field 'headerViews'"));
  }

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

    target.title = null;
    target.subtitle = null;
    target.hello = null;
    target.listOfThings = null;
    target.footer = null;
    target.headerViews = null;

    view2130968578.setOnClickListener(null);
    view2130968578.setOnLongClickListener(null);
    view2130968578 = null;
    ((AdapterView<?>) view2130968579).setOnItemClickListener(null);
    view2130968579 = null;

    this.target = null;
  }
}

編譯時自動生成代碼講完了, 然后來看代碼的綁定.

代碼綁定

用過Butterknife的朋友都知道是通過ButterKnife.bind(this)進(jìn)行綁定(以Activity為例).

@NonNull @UiThread
  public static Unbinder bind(@NonNull Activity target) {
    //獲取跟布局view
    View sourceView = target.getWindow().getDecorView();
    return createBinding(target, sourceView);
  }
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());
    //找到被綁定類(如SimpleActivity_ViewBinding)的構(gòu)造函數(shù)
    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);
    }
  }

到此就完成了原Activity與Activity_ViewBinding的綁定,即在ButterKnife.bind(this)時就完成原Activity注解控件及事件的初始化,原Activity就可以直接調(diào)用了.

如理解有誤,歡迎指正.最后附上相關(guān)技術(shù)的文章:
APT----《android-apt》
Javapoet----《javapoet——讓你從重復(fù)無聊的代碼中解放出來》

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 本文主要介紹Android之神JakeWharton的一個注解框架,聽說現(xiàn)在面試官現(xiàn)在面試都會問知不知道JakeW...
    Zeit丶閱讀 1,073評論 4 6
  • 參考1參考2參考3參考4 一:基本原理 編譯時注解+APT編譯時注解:Rentention為CLASS的直接。保留...
    shuixingge閱讀 643評論 0 3
  • 使用Butterknife的主要目的是消除關(guān)于view實例化的樣板代碼,這是一個專為View類型的依賴注入框架。D...
    9bc96fd72f8e閱讀 411評論 0 5
  • 按照慣例,隨筆又偷懶了。 今天收到上海方面確認(rèn),被收了,現(xiàn)在是到了簽訂三方協(xié)議的階段,今天打算去離三方協(xié)議,雖然遇...
    夏十里閱讀 125評論 0 0
  • 今天回家了,家里杏花已經(jīng)落完,青杏藏在滿樹綠葉中,絲毫不起眼。清明的雨已經(jīng)下完,滋潤了整片大地,回升的氣溫使得萬...
    b2d8dedfdf11閱讀 289評論 0 0

友情鏈接更多精彩內(nèi)容