中文字幕av专区_日韩电影在线播放_精品国产精品久久一区免费式_av在线免费观看网站

溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務條款》

怎樣淺析ButterKnife

發布時間:2021-12-20 15:15:02 來源:億速云 閱讀:131 作者:柒染 欄目:大數據

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

JavaPoet生成代碼

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

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

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

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

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

運行結果:
怎樣淺析ButterKnife

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

怎樣淺析ButterKnife

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

然后代碼華麗的生成了:
怎樣淺析ButterKnife

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

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

生成的代碼:
怎樣淺析ButterKnife

好吧,其實格式也不是那么好看對不對?而且還要addStatement還需要夾雜addCode一起使用。為什么寫個for循環都這么難(哭泣臉)。其實JavaPoet早考慮到這個問題,它提供了beginControlFlow() + endControlFlow()兩個接口提供換行和縮進,再結合負責分號和換行的addStatement(),我們的代碼就可以寫成這樣子:
怎樣淺析ButterKnife
生成的代碼相當的順眼:
怎樣淺析ButterKnife
其實JavaPoet還提供了很多有用的接口來幫我們更方便的生成代碼。更加詳細的用法請訪問https://github.com/square/javapoet,這里我就不贅述了。

Java Annotation Tool

那么ButterKnife又是怎么通過Java Annotation Tool來生成我們的輔助代碼呢?讓我們以ButterKnife最新版本8.4.0的源代碼為例。假如是我們自己寫ButterKnife這樣的框架,那么第一步肯定得先定義自己的注解。在ButterKnife源碼的butterknife-annotations包中,我們可以看到ButterKnife自定義的所有的注解,如下圖所示。
怎樣淺析ButterKnife
有了自定義注解,那我們的下一步就是實現自己的注解處理器了。我們結合ButterKnifeButterKnifeProcessor類來學習一下注解處理器的相關知識。為了實現自定義注解處理器,必須先繼承AbstractProcessor類。ButterKnifeProcessor通過繼承AbstractProcessor,實現了四個方法,如下圖所示:
怎樣淺析ButterKnife
怎樣淺析ButterKnife

  • init(ProcessingEnvironment env)
    通過輸入ProcessingEnvironment參數,你可以在得到很多有用的工具類,比如ElementsTypesFiler等。
    Elements是可以用來處理Element的工具類,可以理解為Java Annotation Tool掃描過程中掃描到的所有的元素,比如包(PackageElement)、類(TypeElement)、方法(ExecuteableElement)等
    Types是可以用來處理TypeMirror的工具類,它代表在JAVA語言中的一種類型,我們可以通過TypeMirror配合Elements來判斷某個元素是否是我們想要的類型
    Filer是生成JAVA源代碼的工具類,能不能生成java源碼就靠它啦

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

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

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

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

  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會記錄一個值binderClassNameButterKnife最終會根據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());
      }
    }

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

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

怎樣淺析ButterKnife
納尼,竟然這么簡單?我們觀察到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長這樣
怎樣淺析ButterKnife

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

需要注意的是,Utils.findRequiredViewAsTypeUtils.findRequiredViewUtils.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方法還允許傳入其他不同的參數,原理基本差不多,最終都會用到我們生成的輔助類,這里就不贅述了。
怎樣淺析ButterKnife
怎樣淺析ButterKnife

執行注解處理器

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

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

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

這里把使用ButterKnifeandroid-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問題的解答就分享到這里了,希望以上內容可以對大家有一定的幫助,如果你還有很多疑惑沒有解開,可以關注億速云行業資訊頻道了解更多相關知識。

向AI問一下細節

免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。

AI

高淳县| 老河口市| 嵊泗县| 昆明市| 梧州市| 克山县| 乌兰浩特市| 新竹县| 中方县| 皮山县| 灌云县| 延川县| 井冈山市| 甘谷县| 天长市| 田东县| 平罗县| 乌拉特前旗| 沾益县| 大理市| 贞丰县| 福海县| 灵台县| 慈溪市| 静安区| 武义县| 页游| 峨眉山市| 华阴市| 茶陵县| 准格尔旗| 手游| 卢氏县| 定州市| 昭平县| 满洲里市| 锡林浩特市| 吉林市| 曲水县| 正镶白旗| 延津县|