淺析 ButterKnife

xzyyjy 9年前發布 | 10K 次閱讀 Java Android開發 移動開發 butterknife

不管是Android開發的老司機也好,新司機也罷,想必大家都對 findViewById 這種樣板代碼感到了厭倦,特別是進行復雜的UI界面開發的時候,這種代碼就會顯的非常的臃腫,既影響開發時的效率,又影響美觀。

俗話說,不想偷懶的程序猿不叫工程師,那有什么方法可以讓我們寫這樣的代碼更加的有效率呢?

使用依賴注入框架

如果你不想寫那些無聊的樣板代碼,那么你可以嘗試一下現有的依賴注入庫。 ButterKnife 作為Jake Wharton大神寫的開源框架,號稱在編譯期間就可以實現依賴注入,沒有用到反射,不會降低程序性能等。那么問題來了,它到底是怎么做到的呢?

初探ButterKnife

ButterKnife 是Jake Wharton寫的開源依賴注入框架,它和 Android Annotations 比較類似,都是用到了Java Annotation Tool來在編譯期間生成輔助代碼來達到View注入的目的。

注解處理器是Java1.5引入的工具,它提供了在程序編譯期間掃描和處理注解的能力。它的原理就是在編譯期間讀取Java代碼,解析注解,然后動態生成Java代碼。下圖是Java編譯代碼的流程,可以看到,我們的注解處理器的工作在Annotation Processing階段,最終通過注解處理器生成的代碼會和源代碼一起被編譯成Java字節碼。不過比較遺憾的是你不能修改已經存在的Java文件,比如在已經存在的類中添加新的方法,所以通過Java Annotation Tool只能通過輔助類的方式來實現View的依賴注入,這樣會略微增加項目的方法數和類數,不過只要控制好,不會對項目有太大的影響

ButterKnife 在業務層的使用我就不介紹了,各位老司機肯定是輕車熟路。假如是我們自己寫類似于 ButterKnife 這樣的框架,那么我們的思路是這樣:定義注解,掃描注解,生成代碼。同時,我們需要用到以下這幾個工具:JavaPoet(你當然可以直接用Java Annotation Tool,然后直接通過字符串拼接的方式去生成java源碼,如果你生無可戀的話),Java Annotation Tool以及APT插件。為了后續更好的閱讀 ButterKnife 的源碼,我們先來介紹一下JavaPoet的基礎知識。

JavaPoet生成代碼

JavaPoet是一個可以生成 .java 源代碼的開源項目,也是出自JakeWharton之手,我們可以結合注解處理器在程序編譯階段動態生成我們需要的代碼。先介紹一個使用JavaPoet最基本的例子:

其中:

  • MethodSpec:代表一個構造函數或者方法聲明

  • TypeSpec:代表一個類、接口或者枚舉聲明

  • FieldSpec:代表一個成員變量聲明

  • JavaFile:代表一個頂級的JAVA文件

運行結果:

是不是很神奇?我們的例子只是把生成的代碼寫到了輸出臺, ButterKnife 通過Java Annotation Tool的 Filer 可以幫助我們以文件的形式輸出JAVA源碼。問:那如果我要生成下面這段代碼,我們會怎么寫?

很簡單嘛,依葫蘆畫瓢,只要把 MethodSpec 替換成下面這段:

然后代碼華麗的生成了:

唉,等等,好像哪里不對啊,生成代碼的格式怎么這么奇怪!難道我要這樣寫嘛:

這樣寫肯定是能達到我們的要求,但是未免也太麻煩了一點。其實JavaPoet提供了一個 addStatement 接口,可以自動幫我們換行以及添加分號,那么我們的代碼就可以寫成這個樣子:

生成的代碼:

好吧,其實格式也不是那么好看對不對?而且還要 addStatement 還需要夾雜 addCode 一起使用。為什么寫個for循環都這么難(哭泣臉)。其實JavaPoet早考慮到這個問題,它提供了 beginControlFlow() + endControlFlow() 兩個接口提供換行和縮進,再結合負責分號和換行的 addStatement() ,我們的代碼就可以寫成這樣子:

生成的代碼相當的順眼:

其實JavaPoet還提供了很多有用的接口來幫我們更方便的生成代碼。

Java Annotation Tool

那么 ButterKnife 又是怎么通過Java Annotation Tool來生成我們的輔助代碼呢?讓我們以 ButterKnife 最新版本8.4.0的源代碼為例。假如是我們自己寫 ButterKnife 這樣的框架,那么第一步肯定得先定義自己的注解。在 ButterKnife 源碼的butterknife-annotations包中,我們可以看到 ButterKnife 自定義的所有的注解,如下圖所示。

有了自定義注解,那我們的下一步就是實現自己的注解處理器了。我們結合 ButterKnife 的 ButterKnifeProcessor 類來學習一下注解處理器的相關知識。為了實現自定義注解處理器,必須先繼承AbstractProcessor類。 ButterKnifeProcessor 通過繼承 AbstractProcessor ,實現了四個方法,如下圖所示:

  • init(ProcessingEnvironment env)

    通過輸入 ProcessingEnvironment 參數,你可以在得到很多有用的工具類,比如 Elements , Types , Filer 等。

    Elements 是可以用來處理 Element 的工具類,可以理解為Java Annotation Tool掃描過程中掃描到的所有的元素,比如包(PackageElement)、類(TypeElement)、方法(ExecuteableElement)等

    Types 是可以用來處理 TypeMirror 的工具類,它代表在JAVA語言中的一種類型,我們可以通過 TypeMirror 配合 Elements 來判斷某個元素是否是我們想要的類型

    Filer 是生成JAVA源代碼的工具類,能不能生成java源碼就靠它啦

  • getSupportedAnnotationTypes()
    代表注解處理器可以支持的注解類型,由前面的分析可以知道, ButterKnife 支持的注解有 BindView 、 OnClick 等。

  • getSupportedSourceVersion()
    支持的JDK版本,一般使用 SourceVersion.latestSupported() ,這里使用 Collections.singleton(OPTION_SDK_INT) 也是可以的。

  • process(Set<? extends TypeElement> elements, RoundEnvironment env)
    process 是整個注解處理器的重頭戲,你所有掃描和處理注解的代碼以及生成Java源文件的代碼都寫在這里面,這個也是我們將要重點分析的方法。

ButterKnifeProcessor 的 process 方法看起來很簡單,實際上做了很多事情,大致可以分為兩個部分:

  1. 掃描所有的 ButterKnife 注解,并且生成以 TypeElement 為Key, BindingSet 為鍵值的HashMap。 TypeElement 我們在前面知道屬于類或者接口, BindingSet 用來記錄我們使用JavaPoet生成代碼時的一些參數,最終把該HashMap返回。這些邏輯對應于源碼中的 findAndParseTargets(RoundEnvironment env) 方法

  2. 生成輔助類。輔助類以 _ViewBinding 為后綴,比如在 MainActivity 中使用了 ButterKnife 注解,那么最終會生成 MainActivity_ViewBinding 輔助類。 MainActivity_ViewBinding 類中最終會生成對應于@BindView的 findViewById 等代碼。

    第一步,我們先來分析 findAndParseTargets(RoundEnvironment env) 源碼。由于方法太長,而且做的事情都差不多,我們只需要分析一小段即可。

private Map<TypeElement, BindingClass> findAndParseTargets(RoundEnvironment env) {
    Map<TypeElement, BindingSet.Builder> builderMap = new LinkedHashMap<>();
    Set<TypeElement> erasedTargetNames = new LinkedHashSet<>();
    --- 省略部分代碼---

    for (Element element : env.getElementsAnnotatedWith(BindView.class)) {
      if (!SuperficialValidation.validateElement(element)) continue;
      try {
        //遍歷所有被BindView注解的類
        parseBindView(element, targetClassMap, erasedTargetNames);
      } catch (Exception e) {
        logParsingError(element, BindView.class, e);
      }
    }
    --- 省略部分代碼---

     // Try to find a parent binder for each.
    for (Map.Entry<TypeElement, BindingClass> entry : targetClassMap.entrySet()) {
      TypeElement parentType = findParentType(entry.getKey(), erasedTargetNames);
      if (parentType != null) {
        BindingClass bindingClass = entry.getValue();
        BindingClass parentBindingClass = targetClassMap.get(parentType);
        bindingClass.setParent(parentBindingClass);
      }
    }

    return targetClassMap;
  }

遍歷找到被注解的 Element 之后,通過 parseBindView(Element element, Map<TypeElement, BindingSet.Builder> builderMap,Set<TypeElement> erasedTargetNames) 方法去解析各個 Element 。在 parseBindView 方法中,首先會去檢測被注解的元素是不是View或者 Interface ,如果滿足條件則去獲取被注解元素的注解的值,如果相應的的 BindingSet.Builder 沒有被綁定過,那么通過 getOrCreateBindingBuilder 方法生成或者直接從 targetClassMap 中獲取(為了提高效率,生成的 BindingSet.Builder 會被存儲在 targetClassMap 中)。 getOrCreateBindingBuilder 方法比較簡單,我就不貼代碼了,生成的 BindingSet.Builder 會記錄一個值 binderClassName , ButterKnife 最終會根據 binderClassName 作為輔助類的類名。

private void parseBindView(Element element, Map<TypeElement, BindingSet.Builder> builderMap,
      Set<TypeElement> erasedTargetNames) {
    TypeElement enclosingElement = (TypeElement) element.getEnclosingElement();

    // Start by verifying common generated code restrictions.
    boolean hasError = isInaccessibleViaGeneratedCode(BindView.class, "fields", element)
        || isBindingInWrongPackage(BindView.class, element);

    // Verify that the target type extends from View.
    TypeMirror elementType = element.asType();
    --- 省略類型校驗邏輯的代碼---

     // 獲取注解的值
    int id = element.getAnnotation(BindView.class).value();

    BindingSet.Builder builder = builderMap.get(enclosingElement);
    if (builder != null) {
      ViewBindings viewBindings = builder.getViewBinding(getId(id));
      if (viewBindings != null && viewBindings.getFieldBinding() != null) {
        FieldViewBinding existingBinding = viewBindings.getFieldBinding();
        error(element, "Attempt to use @%s for an already bound ID %d on '%s'. (%s.%s)",
            BindView.class.getSimpleName(), id, existingBinding.getName(),
            enclosingElement.getQualifiedName(), element.getSimpleName());
        return;
      }
    } else {
       //如果沒有綁定過,那么通過該方法獲得對應的builder并且返回。這里的targetClassMap會存儲已經生成的builder,必要的時候提高效率   
      builder = getOrCreateBindingBuilder(builderMap, enclosingElement);
    }

    String name = element.getSimpleName().toString();
    TypeName type = TypeName.get(elementType);
    boolean required = isFieldRequired(element);

    builder.addField(getId(id), new FieldViewBinding(name, type, required));

    erasedTargetNames.add(enclosingElement);
  }

parseBindView 以及 findAndParseTargets 的解析工作完成后,所有的解析結果都會存放在 targetClassMap 中作為結果返回。我們現在來看 process 第二步的處理過程:遍歷 targetClassMap 中所有的 builder ,并且通過 Filer 生成JAVA源文件。

---代碼省略---
 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());
      }
    }

那么生成的代碼都長什么樣子呢?讓我們打開 BindingSet 的 brewJava(int sdk) 方法一探究竟。

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

納尼,竟然這么簡單?我們觀察到 JavaFile 的靜態方法 builder(String packageName, TypeSpec typeSpec) 第二個參數為 TypeSpec ,前面提到過 TypeSpec 是JavaPoet提供的用來生成類的接口,打開 createType(int sdk) ,霍霍,原來控制將要生成的代碼的邏輯在這里:

private TypeSpec createType(int sdk) {
     // 生成類名為bindingClassName的類
    TypeSpec.Builder result = TypeSpec.classBuilder(bindingClassName.simpleName())
        .addModifiers(PUBLIC);
     //ButterKnife的BindingSet初始化都是通過BindingSet的build方法初始化的,所以isFinal一般被初始化為false
    if (isFinal) {
      result.addModifiers(FINAL);
    }


    if (parentBinding != null) {
       //如果有父類的話,那么注入該子類的時候,也會順帶注入其父類
      result.superclass(parentBinding.bindingClassName);
    } else {
       //如果沒有父類,那么實現Unbinder接口(所以所有生成的輔助類都會繼承Unbinder接口)
      result.addSuperinterface(UNBINDER);
    }
     //增加一個變量名為target,類型為targetTypeName的成員變量
    if (hasTargetField()) {
      result.addField(targetTypeName, "target", PRIVATE);
    }

    if (!constructorNeedsView()) {
      // Add a delegating constructor with a target type + view signature for reflective use.
      result.addMethod(createBindingViewDelegateConstructor(targetTypeName));
    }
    //核心方法,生成***_ViewBinding方法,我們控件的綁定比如findViewById之類的方法都在這里生成
    result.addMethod(createBindingConstructor(targetTypeName, sdk));

    if (hasViewBindings() || parentBinding == null) {
     //生成unBind方法
      result.addMethod(createBindingUnbindMethod(result, targetTypeName));
    }

    return result.build();
  }

接下來讓我們看看核心語句 createBindingConstructor 在 * _ViewBinding方法內到底干了什么:

private MethodSpec createBindingConstructor(TypeName targetType, int sdk) {
    //方法修飾符為PUBLIC,并且添加注解為UiThread
    MethodSpec.Builder constructor = MethodSpec.constructorBuilder()
        .addAnnotation(UI_THREAD)
        .addModifiers(PUBLIC);

    if (hasMethodBindings()) {
       //如果有OnClick注解,那么添加方法參數為targetType final target
      constructor.addParameter(targetType, "target", FINAL);
    } else {
       //如果沒有OnClick注解,那么添加方法參數為targetType target
      constructor.addParameter(targetType, "target");
    }

    if (constructorNeedsView()) {
       //如果有注解的View控件,那么添加View source參數
      constructor.addParameter(VIEW, "source");
    } else {
      //否則添加Context source參數
      constructor.addParameter(CONTEXT, "context");
    }

    if (hasUnqualifiedResourceBindings()) {
      constructor.addAnnotation(AnnotationSpec.builder(SuppressWarnings.class)
          .addMember("value", "$S", "ResourceType")
          .build());
    }

     //如果有父類,那么會根據不同情況調用不同的super語句
    if (parentBinding != null) {
      if (parentBinding.constructorNeedsView()) {
        constructor.addStatement("super(target, source)");
      } else if (constructorNeedsView()) {
        constructor.addStatement("super(target, source.getContext())");
      } else {
        constructor.addStatement("super(target, context)");
      }
      constructor.addCode("\n");
    }

     //如果有綁定過Field(不一定是View),那么添加this.target = target語句
    if (hasTargetField()) {
      constructor.addStatement("this.target = target");
      constructor.addCode("\n");
    }

    if (hasViewBindings()) {
      if (hasViewLocal()) {

        // Local variable in which all views will be temporarily stored.
        constructor.addStatement("$T view", VIEW);
      }
      for (ViewBindings bindings : viewBindings) {
        //View綁定的最常用,也是最關鍵的語句,生成類似于findViewById之類的代碼
        addViewBindings(constructor, bindings);
      }
      /**
       * 如果將多個view組成一個List或數組,然后進行綁定,
       * 比如@BindView({ R.id.first_name, R.id.middle_name, R.id.last_name })
       * List<EditText> nameViews;會走這段邏輯
       */
      for (FieldCollectionViewBinding binding : collectionBindings) {
        constructor.addStatement("$L", binding.render());
      }

      if (!resourceBindings.isEmpty()) {
        constructor.addCode("\n");
      }
    }
---省略一些綁定resource資源的代碼---
}

addViewBindings 我們簡單看看就好。需要注意的是:

  • 因為生成代碼時確實要根據不同條件來生成不同代碼,所以使用了 CodeBlock.Builder 接口。 CodeBlock.Builder 也是JavaPoet提供的,該接口提供了類似字符串拼接的能力

  • 生成了類似于 target.fieldBinding.getName() = .findViewById(bindings.getId().code) 或者 target.fieldBinding.getName() = .findRequiredView(bindings.getId().code) 之類的代碼,我們可以清楚的看到,這里沒有用到反射,所以被@BindView注解的變量的修飾符不能為private。

    private void addViewBindings(MethodSpec.Builder result, ViewBindings bindings) {
      if (bindings.isSingleFieldBinding()) {
        // Optimize the common case where there's a single binding directly to a field.
        FieldViewBinding fieldBinding = bindings.getFieldBinding();
        /**
         * 這里使用了CodeBlock接口,顧名思義,該接口提供了類似字符串拼接的接口
         * 另外,從target.$L 這條語句來看,我們就知道為什么使用BindView注解的
         * 變量不能為private了
         */
        CodeBlock.Builder builder = CodeBlock.builder()
            .add("target.$L = ", fieldBinding.getName());
    
        boolean requiresCast = requiresCast(fieldBinding.getType());
        if (!requiresCast && !fieldBinding.isRequired()) {
          builder.add("source.findViewById($L)", bindings.getId().code);
        } else {
          builder.add("$T.find", UTILS);
          builder.add(fieldBinding.isRequired() ? "RequiredView" : "OptionalView");
          if (requiresCast) {
            builder.add("AsType");
          }
          builder.add("(source, $L", bindings.getId().code);
          if (fieldBinding.isRequired() || requiresCast) {
            builder.add(", $S", asHumanDescription(singletonList(fieldBinding)));
          }
          if (requiresCast) {
            builder.add(", $T.class", fieldBinding.getRawType());
          }
          builder.add(")");
        }
        result.addStatement("$L", builder.build());
        return;
      }
    
      List<ViewBinding> requiredViewBindings = bindings.getRequiredBindings();
      if (requiredViewBindings.isEmpty()) {
        result.addStatement("view = source.findViewById($L)", bindings.getId().code);
      } else if (!bindings.isBoundToRoot()) {
        result.addStatement("view = $T.findRequiredView(source, $L, $S)", UTILS,
            bindings.getId().code, asHumanDescription(requiredViewBindings));
      }
    
      addFieldBindings(result, bindings);
       // 監聽事件綁定
      addMethodBindings(result, bindings);
    }

    addMethodBindings(result, bindings) 實現了監聽事件的綁定,也通過 MethodSpec.Builder 來生成相應的方法,由于源碼太長,這里就不貼源碼了。

小結: createType 方法到底做了什么?

  • 生成類名為 className_ViewBing 的類

  • className_ViewBing 實現了 Unbinder 接口(如果有父類的話,那么會調用父類的構造函數,不需要實現Unbinder接口)

  • 根據條件生成 className_ViewBing 構造函數(實現了成員變量、方法的綁定)以及 unbind 方法(解除綁定)等

如果簡單使用 ButterKnife ,比如我們的 MainActivity 長這樣

那么生成的最終 MainActivity_ViewBinding 類的代碼就長下面這樣子,和我們分析源碼時預估的樣子差不多。

需要注意的是, Utils.findRequiredViewAsType 、 Utils.findRequiredView 、 Utils.castView 的區別。其實 Utils.findRequiredViewAsType 就是 Utils.findRequiredView (相當于 findViewById )+ Utils.castView (強制轉型,class類接口)。

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

MainActivity_ViewBinding 類的調用過程就比較簡單了。 MainActivity 一般會調用 ButterKnife.bind(this) 來實現View的依賴注入,這個也是 ButterKnife 和Google親兒子 AndroidAnnotations 的區別: AndroidAnnotations 不需要自己手動調用 ButterKnife.bind(this) 等類似的方法就可以實現View的依賴注入,但是讓人蛋疼的是編譯的時候會生成一個子類,這個子類是使用了 AndroidAnnotations 類后面加了一個 _ ,比如 MainActivity 你就要使用 MainActivity_ 來代替,比如 Activity 的跳轉就必須這樣寫: startActivity(new Intent(this,MyActivity_.class)) ,這兩個開源庫的原理基本差不多,哪種方法比較好看個人喜好去選擇吧。

言歸正傳,輔助類生成后,最終的調用過程一般是 ButterKnife.bind(this) 開始,查看 ButterKnife.bind(this) 源碼,最終會走到 createBinding 以及 findBindingConstructorForClass 這個方法中,源碼如下圖所示,這個方法就是根據你傳入的類名找到對應的輔助類,最終通過調用 constructor.newInstance(target, source) 來實現View以及其他資源的綁定工作。這里需要注意的是在 findBindingConstructorForClass 使用輔助類的時候,其實是有用到反射的,這樣第一次使用的時候會稍微降低程序性能,但是 ButterKnife 會把通過反射生成的實例保存到HashMap中,下一次直接從HashMap中取上次生成的實例,這樣就極大的降低了反射導致的性能問題。當然 ButterKnife.bind 方法還允許傳入其他不同的參數,原理基本差不多,最終都會用到我們生成的輔助類,這里就不贅述了。

執行注解處理器

注解處理器已經有了,比如 ButterKnifeProcessor ,那么怎么執行它呢?這個時候就需要用到 android-apt 這個插件了,使用它有兩個目的:

  1. 允許配置只在編譯時作為注解處理器的依賴,而不添加到最后的APK或library

  2. 設置源路徑,使注解處理器生成的代碼能被Android Studio正確的引用

這里把使用 ButterKnife 時 android-apt 的配置作為例子,在工程的 build.gradle 中添加 android-apt 插件

buildscript {
  repositories {
    mavenCentral()
   }
  dependencies {
    classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8'
  }
}

在項目的build.gradle中添加

apply plugin: 'android-apt'

android {
  ...
}

dependencies {
  compile 'com.jakewharton:butterknife:8.4.0'
  apt 'com.jakewharton:butterknife-compiler:8.4.0'
}

總結

ButterKnife 作為一個被廣泛使用的依賴注入庫,有很多優點:

  • 沒有使用反射,而是通過Java Annotation Tool動態生成輔助代碼實現了View的依賴注入,提升了程序的性能

  • 提高開發效率,減少代碼量

當然也有一些不太友好的地方:

  • 會額外生成新的類和方法數,主要是會加速觸及65535方法數,當然,如果App已經有分dex了可以不用考慮

  • 也不是完全沒有用到反射,比如第一次調用 ButterKnife.bind(this) 語句使用輔助類的時候就用到了,會稍微影響程序的性能(但是也僅僅是第一次)

ButterKnife 之旅到這里就結束了,至于要不要在項目中使用大家可以根據實際情況來選擇,畢竟適合自己的才是最好的。

參考文檔

  1. [初探JavaPoet]

    http://blog.ornithopter.me/post/android/javapoet

  2. Generating Java Source Files with JavaPoet

    http://www.hascode.com/2015/02/generating-java-source-files-with-javapoet/

  3. Java注解處理器

    http://www.race604.com/annotation-processing/

  4. android-apt

    http://www.jianshu.com/p/2494825183c5

  5. 深入淺出ButterKnife

    http://km.oa.com/articles/show/286521?kmref=search&from_page=1&no=1&is_from_iso=1

  6. ButterKnife框架原理

    https://bxbxbai.github.io/2016/03/12/how-butterknife-works/

  7. 淺析ButterKnife的實現

    http://blog.csdn.net/ta893115871/article/details/52497297

  8. 深入理解ButterKnife源碼并掌握原理

    http://www.bcoder.cn/13581.html

  9. ButterKnife源碼剖析

    http://blog.csdn.net/chenkai19920410/article/details/51020151

 

來自:http://mp.weixin.qq.com/s?__biz=MzI1NjEwMTM4OA==&mid=2651232205&idx=1&sn=6c24e6eef2b18f253284b9dd92ec7efb&chksm=f1d9eaaec6ae63b82fd84f72c66d3759c693f164ff578da5dde45d367f168aea0038bc3cc8e8&scene=0#wechat_redirect

 

 本文由用戶 xzyyjy 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!