ButterKnife 原理解析

ButterKnife 算是一款知名老牌 Android 開發(fā)框架了负甸,通過注解綁定視圖,避免了 findViewById() 的操作,廣受好評迫淹!由于它是在編譯時對注解進(jìn)行解析完成相關(guān)代碼的生成,所以在項目編譯時會略耗時夕吻,但不會影響運行時的性能蔓倍。接下來讓我們從使用到原理一步步深入了解這把黃油刀的故事滞诺!

以下內(nèi)容基于 butterknife:8.8.1 版本阎曹,主要包括如下幾個方面的內(nèi)容:

  • 簡單使用
  • 原理分析
  • 注解處理器
  • JavaPoet

一癣缅、簡單使用

首先編寫一個 ButterKnife 簡單使用的例子,方便后續(xù)的分析友存。先在 app的 build.gradle 中加入如下配置祷膳,完成 ButterKnife 引入:

dependencies {
    ......
    implementation 'com.jakewharton:butterknife:8.8.1'
    annotationProcessor 'com.jakewharton:butterknife-compiler:8.8.1'
}

接下來在 Activity 中使用,界面上一個TextView一個Button屡立,很簡單就不解釋了:

public class MainActivity extends AppCompatActivity {
    @BindView(R.id.tv_title)
    TextView title;

    @OnClick(R.id.bt_submit)
    public void submit() {
        title.setText("hello world");
    }

    private Unbinder unbinder;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        unbinder = ButterKnife.bind(this);
    }

    @Override
    protected void onDestroy() {
        unbinder.unbind();
        super.onDestroy();
    }
}

二直晨、原理分析

最后編譯一下項目。直覺告訴我們應(yīng)該從ButterKnife.bind(this)開始分析膨俐,因為它像是 ButterKnife 和 Activity 建立綁定關(guān)系的過程勇皇,看具體的代碼:

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

sourceView代表當(dāng)前界面的頂級父 View,是一個FrameLayout焚刺,繼續(xù)看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);
    } 
    // 省略了相關(guān)異常處理代碼
  }

首先得到要綁定的 Activity 對應(yīng)的 Class敛摘,然后用根據(jù) Class 得到一個繼承了UnbinderConstructor,最后通過反射constructor.newInstance(target, source)得到Unbinder子類的一個實例乳愉,到此ButterKnife.bind(this)操作結(jié)束兄淫。這里我們重點關(guān)注findBindingConstructorForClass()方法是如何得到constructor實例的:

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

整體的流程是先檢查BINDINGS是否存在 Class 對應(yīng)的 Constructor,如果存在則直接返回蔓姚,否則去構(gòu)造對應(yīng)的 Constructor捕虽。其中BINDINGS是一個LinkedHashMap
Map<Class<?>, Constructor<? extends Unbinder>> BINDINGS = new LinkedHashMap<>(),緩存了對應(yīng)的 Class 和 Constructor 以提高效率坡脐!
接下來看當(dāng)不存在對應(yīng) Constructor 時如何構(gòu)造一個新的泄私,首先如果clsName是系統(tǒng)相關(guān)的,則直接返回 null,否則先創(chuàng)建一個新的 Class:

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

這里新的 Class 的 name 是 com.shh.sometest.MainActivity_ViewBinding晌端,最后用新的 Class 創(chuàng)建一個 繼承了Unbinder的 Constructor捅暴,并添加到BINDINGS

bindingCtor = (Constructor<? extends Unbinder>) bindingClass.getConstructor(cls, View.class);
BINDINGS.put(cls, bindingCtor);

所以最終bind()方法返回的是MainActivity_ViewBinding類的實例。既然可以返回MainActivity_ViewBinding的實例斩松,那MainActivity_ViewBinding這個類肯定是存在的伶唯【跫龋可以在如下目錄找到它(這個類是在項目編譯時期由 annotationProcessor 生成的惧盹,關(guān)于 annotationProcessor 后邊會說到):

MainActivity_ViewBinding

來看看它里邊都做了那些事:

public class MainActivity_ViewBinding implements Unbinder {
  private MainActivity target;

  private View view2131165217;

  @UiThread
  public MainActivity_ViewBinding(MainActivity target) {
    this(target, target.getWindow().getDecorView());
  }

  @UiThread
  public MainActivity_ViewBinding(final MainActivity target, View source) {
    this.target = target;

    View view;
    target.title = Utils.findRequiredViewAsType(source, R.id.tv_title, "field 'title'", TextView.class);
    view = Utils.findRequiredView(source, R.id.bt_submit, "method 'submit'");
    view2131165217 = view;
    view.setOnClickListener(new DebouncingOnClickListener() {
      @Override
      public void doClick(View p0) {
        target.submit();
      }
    });
  }

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

    target.title = null;

    view2131165217.setOnClickListener(null);
    view2131165217 = null;
  }
}

之前createBinding()方法中return constructor.newInstance(target, source);操作使用的就是MainActivity_ViewBinding類兩個參數(shù)的構(gòu)造函數(shù)。

重點看這個構(gòu)造函數(shù)瞪讼,首先是給target.title賦值钧椰,即MainActivity中的TextView,用到了一個findRequiredViewAsType()方法:

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

繼續(xù)看findRequiredView()方法:

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

最終還是通過findViewById()得到對應(yīng)View符欠,然后就是castView()

public static <T> T castView(View view, @IdRes int id, String who, Class<T> cls) {
    try {
      return cls.cast(view);
    } catch (ClassCastException e) {
      String name = getResourceEntryName(view, id);
      throw new IllegalStateException("View '"
          + name
          + "' with ID "
          + id
          + " for "
          + who
          + " was of the wrong type. See cause for more info.", e);
    }
  }

核心就是把findRequiredView()得到的 View 轉(zhuǎn)成指定類型的 View 嫡霞,如果 xml 中定義的 View 和 Activity 中通過注解綁定的 View 類型不一致,就會拋出上邊方法的異常希柿,可能很多人都遇到過诊沪。這樣target.title的賦值就結(jié)束了,接下來就是直接使用findRequiredView()找到對應(yīng) id 的Button曾撤,不用進(jìn)行類型轉(zhuǎn)換端姚,然后給它綁定點擊事件,最終調(diào)用了在MainActivity中給Button綁定點擊事件時定義的submit()方法挤悉。到這里就完成了 相關(guān) View 的賦值以及事件綁定渐裸!

MainActivity_ViewBinding類中還有一個unbind()方法,需要在ActivityFragmentonDestory()中調(diào)用装悲,以完成 相關(guān) View 引用的釋放以及點擊事件的解綁操作昏鹃!

三、注解處理器

那么诀诊,MainActivity_ViewBinding類時如何生成的呢洞渤?首先,要生成這個類就要先得到這個類必須的基礎(chǔ)信息属瓣,這就涉及到了annotationProcessor技術(shù)载迄,和 APT(Annotation Processing Tool)技術(shù)類似,它是一種注解處理器奠涌,項目編譯時對源代碼進(jìn)行掃描檢測找出存活時間為RetentionPolicy.CLASS的指定注解宪巨,然后對注解進(jìn)行解析處理,進(jìn)而得到要生成的類的必要信息溜畅,然后根據(jù)這些信息動態(tài)生成對應(yīng)的 java 類捏卓,至于如何生成 java 類就涉及到了后邊要說的 JavaPoet技術(shù)。

這里我們先看用注解處理器收集類信息的過程,之前我們已經(jīng)在app的 build.gradle引入了 ButterKnife 的注解處理器: butterknife-compiler怠晴,其中有一個ButterKnifeProcessor 類完成了注解處理器的核心邏輯遥金。這個類約有1500行代碼,先看下它的基本框架結(jié)構(gòu):

@AutoService(Processor.class)
public final class ButterKnifeProcessor extends AbstractProcessor {
  
    @Override
    public synchronized void init(ProcessingEnvironment env) {
        super.init(env);
        String sdk = env.getOptions().get(OPTION_SDK_INT);
        ......
        debuggable = !"false".equals(env.getOptions().get(OPTION_DEBUGGABLE));
        elementUtils = env.getElementUtils();
        typeUtils = env.getTypeUtils();
        filer = env.getFiler();
        try {
            trees = Trees.instance(processingEnv);
        } catch (IllegalArgumentException ignored) {
        }
    }

    @Override
    public Set<String> getSupportedOptions() {
        return ImmutableSet.of(OPTION_SDK_INT, OPTION_DEBUGGABLE);
    }

    @Override
    public Set<String> getSupportedAnnotationTypes() {
        Set<String> types = new LinkedHashSet<>();
        for (Class<? extends Annotation> annotation : getSupportedAnnotations()) {
            types.add(annotation.getCanonicalName());
        }
        return types;
    }

    @Override
    public boolean process(Set<? extends TypeElement> elements, RoundEnvironment env) {
        Map<TypeElement, BindingSet> bindingMap = findAndParseTargets(env);

        for (Map.Entry<TypeElement, BindingSet> entry : bindingMap.entrySet()) {
            TypeElement typeElement = entry.getKey();
            BindingSet binding = entry.getValue();

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

   @Override
   public SourceVersion getSupportedSourceVersion() {
       return SourceVersion.latestSupported();
   }
}

注意蒜田,ButterKnifeProcessor類上使用了@AutoService(Processor.class)注解稿械,來實現(xiàn)注解處理器的注冊,注冊到 javac 后冲粤,在項目編譯時就能執(zhí)行注解處理器了美莫。

ButterKnifeProcessor繼承了AbstractProcessor抽象類,并重寫以上五個方法梯捕,如果我們自定義解處理器也是類似的厢呵,看下這幾個方法:

1、init()

首先 init() 方法完成sdk版本的判斷以及相關(guān)幫助類的初始化傀顾,幫助類主要有以下幾個:

  • Elements elementUtils襟铭,注解處理器運行掃描源文件時,以獲取元素(Element)相關(guān)的信息短曾。Element 有以下幾個子類:
    包(PackageElement)寒砖、類(TypeElement)、成員變量(VariableElement)嫉拐、方法(ExecutableElement)
  • Types typeUtils哩都,
  • Filer filer,用來生成 java 類文件椭岩。
  • Trees trees茅逮,
2、getSupportedAnnotationTypes()

該方法返回一個Set<String>判哥,代表ButterKnifeProcessor要處理的注解類的名稱集合献雅,即 ButterKnife 支持的注解:butterknife-annotations

3、getSupportedSourceVersion()

返回當(dāng)前系統(tǒng)支持的 java 版本塌计。

4挺身、getSupportedOptions()

返回注解處理器可處理的注解操作。

5锌仅、process()

最后章钾,process() 方法是我們要重點分析的,在這里完成了目標(biāo)類信息的收集并生成對應(yīng) java 類热芹。

首先看注解信息的掃描收集贱傀,即通過findAndParseTargets()方法:

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

        scanForRClasses(env);
        ......
        ......
        // env.getElementsAnnotatedWith(BindView.class)獲取所有使用BindView注解的元素
        for (Element element : env.getElementsAnnotatedWith(BindView.class)) {
            try {
                parseBindView(element, builderMap, erasedTargetNames);
            } catch (Exception e) {
                logParsingError(element, BindView.class, e);
            }
        }
        ......
        ......
        // 將builderMap中的數(shù)據(jù)添加到隊列中
        Deque<Map.Entry<TypeElement, BindingSet.Builder>> entries =
                new ArrayDeque<>(builderMap.entrySet());
        Map<TypeElement, BindingSet> bindingMap = new LinkedHashMap<>();
        while (!entries.isEmpty()) {
            // 出隊列
            Map.Entry<TypeElement, BindingSet.Builder> entry = entries.removeFirst();

            TypeElement type = entry.getKey();
            BindingSet.Builder builder = entry.getValue();
            // 查找當(dāng)前類元素的父類元素
            TypeElement parentType = findParentType(type, erasedTargetNames);
            // 如果沒找到則保存TypeElement和對應(yīng)BindingSet
            if (parentType == null) {
                bindingMap.put(type, builder.build());
            } else {
                BindingSet parentBinding = bindingMap.get(parentType);
                if (parentBinding != null) {
                    // 如果找到父類元素,則給當(dāng)前類元素對應(yīng)的BindingSet.Builder設(shè)置父BindingSet
                    builder.setParent(parentBinding);
                    bindingMap.put(type, builder.build());
                } else {
                    // 再次入隊列
                    entries.addLast(entry);
                }
            }
        }
        return bindingMap;
    }

只保留了BindView注解信息的掃描解析過程伊脓,省略其它類似邏輯府寒,先將掃描得到的注解相關(guān)信息保存到builderMaperasedTargetNames中,最后對這些信息進(jìn)行重新整理返回一個以TypeElement為 key 、BindingSet為 value 的 Map株搔,其中TypeElement代表使用了 ButterKnife 的類剖淀,即 Activity、Fragment等纤房,BindingSetbutterknife-compiler中的一個自定義類纵隔,用來存儲要生成類的基本信息以及注解元素的相關(guān)信息。
parseBindView()方法用來解析使用了BindView注解的元素:

private void parseBindView(Element element, Map<TypeElement, BindingSet.Builder> builderMap,
                               Set<TypeElement> erasedTargetNames) {
        // 首先要注意炮姨,此時element是VariableElement類型的捌刮,即成員變量
        // enclosingElement是當(dāng)前元素的父類元素,一般就是我們使用ButteKnife時定義的View類型成員變量所在的類剑令,可以理解為之前例子中的MainActivity
        TypeElement enclosingElement = (TypeElement) element.getEnclosingElement();

        // 進(jìn)行相關(guān)校驗
        // 1糊啡、isInaccessibleViaGeneratedCode(),先判斷當(dāng)前元素的是否是private或static類型吁津,
        // 再判斷其父元素是否是一個類以及是否是private類型。
        // 2堕扶、isBindingInWrongPackage()碍脏,是否在系統(tǒng)相關(guān)的類中使用了ButteKnife注解
        boolean hasError = isInaccessibleViaGeneratedCode(BindView.class, "fields", element)
                || isBindingInWrongPackage(BindView.class, element);

        // TypeMirror表示Java編程語言中的一種類型。 類型包括基元類型稍算,聲明的類型(類和接口類型)典尾,數(shù)組類型,類型變量和空類型糊探。 
        // 還表示了通配符類型參數(shù)钾埂,可執(zhí)行文件的簽名和返回類型,以及與包和關(guān)鍵字void相對應(yīng)的偽類型科平。
        TypeMirror elementType = element.asType();
        // 如果當(dāng)前元素是類的成員變量
        if (elementType.getKind() == TypeKind.TYPEVAR) {
            TypeVariable typeVariable = (TypeVariable) elementType;
            elementType = typeVariable.getUpperBound();
        }
        Name qualifiedName = enclosingElement.getQualifiedName();
        Name simpleName = element.getSimpleName();
        // 判斷當(dāng)前元素是否是 View 的子類褥紫,或者是接口,不是的話拋出異常
        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;
        }

        // 獲得元素使用BindView注解時設(shè)置的屬性值瞪慧,即 View 對應(yīng)的xml中的id
        int id = element.getAnnotation(BindView.class).value();
        // 嘗試獲取父元素對應(yīng)的BindingSet.Builder
        BindingSet.Builder builder = builderMap.get(enclosingElement);
        // QualifiedId記錄了當(dāng)前元素的包信息以及id
        QualifiedId qualifiedId = elementToQualifiedId(element, id);
        if (builder != null) {
            String existingBindingName = builder.findExistingBindingName(getId(qualifiedId));
            // 如果當(dāng)前id已經(jīng)被綁定髓考,則拋出異常
            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 {
            // 創(chuàng)建一個新的BindingSet.Builder并返回,并且以enclosingElement 為key添加到builderMap中
            builder = getOrCreateBindingBuilder(builderMap, enclosingElement);
        }

        String name = simpleName.toString();
        TypeName type = TypeName.get(elementType);
        // 判斷當(dāng)前元素是否使用了Nullable注解
        boolean required = isFieldRequired(element);
        // 創(chuàng)建一個FieldViewBinding弃酌,它包含了元素名氨菇、類型、是否是Nullable
        // 然后和元素id一同添加到BindingSet.Builder
        builder.addField(getId(qualifiedId), new FieldViewBinding(name, type, required));

        // 記錄當(dāng)前元素的父類元素
        erasedTargetNames.add(enclosingElement);
    }

部分代碼已經(jīng)寫了注釋妓湘,這里看下getOrCreateBindingBuilder()方法的實現(xiàn):

private BindingSet.Builder getOrCreateBindingBuilder(
      Map<TypeElement, BindingSet.Builder> builderMap, TypeElement enclosingElement) {
    // 先判斷enclosingElement對應(yīng)的BindingSet.Builder是否已存在
    BindingSet.Builder builder = builderMap.get(enclosingElement);
    if (builder == null) {
      // 創(chuàng)建一個BindingSet.Builder
      builder = BindingSet.newBuilder(enclosingElement);
      // 添加到builderMap
      builderMap.put(enclosingElement, builder);
    }
    return builder;
  }

由于用BindingSet保存了注解相關(guān)的信息查蓉,所以繼續(xù)了解下它的newBuilder()過程:

static Builder newBuilder(TypeElement enclosingElement) {
    TypeMirror typeMirror = enclosingElement.asType();
    // 判斷當(dāng)前父元素的類型
    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('.', '$');
    // 即最終要生成的java類的名稱
    ClassName bindingClassName = ClassName.get(packageName, className + "_ViewBinding");
    // 判斷父類元素是否為final類型
    boolean isFinal = enclosingElement.getModifiers().contains(Modifier.FINAL);
    return new Builder(targetType, bindingClassName, isFinal, isView, isActivity, isDialog);
  }

所以BindingSet主要保存了要生成的目標(biāo)類的基本特征信息,以及類中使用了 ButterKnife 注解的元素的信息榜贴,這樣一個BindingSet就和一個使用了ButterKnife 的類對應(yīng)了起來豌研。

四、JavaPoet

到這里要生成的目標(biāo)類基本信息就收集就完成了,接下來就是生成 java 類文件了聂沙,再回到 process()方法:

@Override
public boolean process(Set<? extends TypeElement> elements, RoundEnvironment env) {
    Map<TypeElement, BindingSet> bindingMap = findAndParseTargets(env);

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

遍歷 bindingMap秆麸,根據(jù)BindingSet得到一個JavaFile對象,然后輸入 java 類及汉,這個過程用到了JavaPoet開源庫沮趣,提供了一種友好的方式來輔助生成 java 類代碼,同時將類代碼生成文件坷随,否則需要自己拼接字符串來實現(xiàn)房铭,可以發(fā)現(xiàn)BindingSet除了保存信息目標(biāo)類信息外,還封裝了 JavaPoet 生成目標(biāo)類代碼的過程温眉。

在繼續(xù)往下分析前缸匪,先了解下 JavaPoet 中一些重要的類(這些類還有許多實用的方法哦):

  • TypeSpec 表示類、接口类溢、或者枚舉聲明
  • ParameterSpec 表示參數(shù)聲明
  • MethodSpec 表示構(gòu)造函數(shù)凌蔬、方法聲明
  • FieldSpec 表示成員變量,一個字段聲明
  • CodeBlock 表示代碼塊闯冷,用來拼接代碼
  • JavaFile 表示Java類的代碼

還有幾個占位符也了解下:

  • $L砂心,for Literals 替換字符串、原語蛇耀、JavaPoet中的類型
    例如beginControlFlow("for (int i = $L; i < $L; i++)", 1, 10)
    addStatement("result = result $L i", "+")
  • $S辩诞,for Strings 替換字符串,例如addStatement("return $S", "hello")
  • $T纺涤,for Types 替換類型译暂,例如addStatement("return new $T()", Date.class)
  • $N,for Names 替換JavaPoet中的聲明

有了一些基礎(chǔ)概念后撩炊,繼續(xù)看用 JavaPoet 生成對應(yīng)JavaFile的過程:

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

用到的JavaFile.builder()方法需要兩個參數(shù):要生成的目標(biāo)類的包名外永,以及TypeSpec對象,即createType()方法的返回值衰抑,代表一個類象迎、接口、枚舉聲明呛踊。結(jié)合文章開始的例子砾淌,這個TypeSpec對象應(yīng)代表一個類聲明:

private TypeSpec createType(int sdk, boolean debuggable) {
    // TypeSpec.classBuilder() 方法設(shè)置類名稱
    // addModifiers() 方法設(shè)置類的修飾符為 public
    TypeSpec.Builder result = TypeSpec.classBuilder(bindingClassName.simpleName())
        .addModifiers(PUBLIC);
    // 如果是final類則添加final修飾符
    if (isFinal) {
      result.addModifiers(FINAL);
    }

    if (parentBinding != null) {
      result.superclass(parentBinding.bindingClassName);
    } else {
      // 讓當(dāng)前類Unbinder接口,之前生成的MainActivity_ViewBinding類就實現(xiàn)了Unbinder接口
      result.addSuperinterface(UNBINDER);
    }

    if (hasTargetField()) {
      // 添加一個類成員變量谭网,可以對應(yīng)到MainActivity_ViewBinding類中的private MainActivity target
      result.addField(targetTypeName, "target", PRIVATE);
    }
    
    // 判斷目標(biāo)類的類型
    if (isView) {
      result.addMethod(createBindingConstructorForView());
    } else if (isActivity) {
      // 由于之前的例子是Activity類型的汪厨,所以會走到這里,去生成MainActivity_ViewBinding類默認(rèn)的構(gòu)造函數(shù)
      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());
    }
    // 生成view綁定的構(gòu)造函數(shù)愉择,可以對應(yīng)到MainActivity_ViewBinding類兩個參數(shù)的構(gòu)造函數(shù)
    result.addMethod(createBindingConstructor(sdk, debuggable));

    if (hasViewBindings() || parentBinding == null) {
      // 生成ubinder()方法
      result.addMethod(createBindingUnbindMethod(result));
    }
    return result.build();
  }

這里重點看下createBindingConstructor()的過程劫乱,代碼較多织中,只保留部分以方便分析:

private MethodSpec createBindingConstructor(int sdk, boolean debuggable) {
    MethodSpec.Builder constructor = MethodSpec.constructorBuilder()
        .addAnnotation(UI_THREAD)
        .addModifiers(PUBLIC);
    if (hasMethodBindings()) {
      // 給構(gòu)造函數(shù)添加一個修飾符為final、targetTypeName衷戈,名稱為target的參數(shù)狭吼,即final MainActivity target
      constructor.addParameter(targetTypeName, "target", FINAL);
    } else {
      constructor.addParameter(targetTypeName, "target");
    }
    if (constructorNeedsView()) {
      // 給構(gòu)造函數(shù)添加一個View類型的source參數(shù)
      constructor.addParameter(VIEW, "source");
    } else {
      constructor.addParameter(CONTEXT, "context");
    }
    ......
    if (hasTargetField()) {
      // 給構(gòu)造函數(shù)添加一行this.target = target;的聲明代碼
      constructor.addStatement("this.target = target");
      constructor.addCode("\n");
    }

    if (hasViewBindings()) {
      if (hasViewLocal()) {
        // 給構(gòu)造函數(shù)添加一行View view;的聲明代碼
        constructor.addStatement("$T view", VIEW);
      }
      for (ViewBinding binding : viewBindings) {
        // 根據(jù)id查找view并完成賦值
        addViewBinding(constructor, binding, debuggable);
      }
      ......
    }
    ......
    return constructor.build();
  }

那么是如何根據(jù)id查找view呢?答案就在addViewBinding()方法殖妇,以下解釋同樣類比MainActivity_ViewBinding兩個參數(shù)的構(gòu)造函數(shù):

private void addViewBinding(MethodSpec.Builder result, ViewBinding binding, boolean debuggable) {
    if (binding.isSingleFieldBinding()) {
      FieldViewBinding fieldBinding = binding.getFieldBinding();
      // CodeBlock 表示代碼塊刁笙,用來完成view查找、賦值語句的拼接
      // 相當(dāng)于target.title = 
      CodeBlock.Builder builder = CodeBlock.builder()
          .add("target.$L = ", fieldBinding.getName());
      // 由于例子中title是TextView谦趣,不是View所以requiresCast為true
      boolean requiresCast = requiresCast(fieldBinding.getType());
      // debuggable為true疲吸、fieldBinding.isRequired()為true,則以下if條件不成立
      if (!debuggable || (!requiresCast && !fieldBinding.isRequired())) {
        if (requiresCast) {
          builder.add("($T) ", fieldBinding.getType());
        }
        builder.add("source.findViewById($L)", binding.getId().code);
      } else {
        // 繼續(xù)給代碼塊添加Utils.find
        builder.add("$T.find", UTILS);
        // 根據(jù)上邊的分析可知前鹅,會給代碼塊添加RequiredView
        builder.add(fieldBinding.isRequired() ? "RequiredView" : "OptionalView");
        if (requiresCast) {
          // 給代碼塊添加AsType
          builder.add("AsType");
        }
        // 給代碼塊添加(source, R.id.tv_title
        builder.add("(source, $L", binding.getId().code);
        if (fieldBinding.isRequired() || requiresCast) {
          // 繼續(xù)添加view的相關(guān)描述摘悴,例如field 'title'
          builder.add(", $S", asHumanDescription(singletonList(fieldBinding)));
        }
        if (requiresCast) {
          繼續(xù)添加view的Class類型,例如TextView.class
          builder.add(", $T.class", fieldBinding.getRawType());
        }
        // 添加右括號舰绘,到這里就完成了view的查找與賦值
        // 例如target.title = Utils.findRequiredViewAsType(source, R.id.tv_title, "field 'title'", TextView.class);
        builder.add(")");
      }
      result.addStatement("$L", builder.build());
      return;
    }

結(jié)合對createType()流程的分析蹂喻,我們基本了解了 MainActivity_ViewBinding 類中構(gòu)造函數(shù)的構(gòu)建過程、以及 title(之前例子的TextView)的查找賦值的代碼是如何構(gòu)建出來的除盏,這樣就把注解處理器中 process()方法中BindView注解的處理流程就跑通了叉橱。隨然這只是一小部分的分析,但并不妨礙我們理解其它注解背后的工作流程者蠕。

五、小結(jié)

可以看出 ButterKnife 整個過程是在項目編譯階段完成的掐松,主要用到了 annotationProcessor 和 JavaPoet 技術(shù)踱侣,使用時通過生成的輔助類完成操作,并不是在項目運行時通過注解加反射實現(xiàn)的大磺,所以并不會影響項目運行時的性能抡句,可能僅在項目編譯時有略微的影響。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末杠愧,一起剝皮案震驚了整個濱河市待榔,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌流济,老刑警劉巖锐锣,帶你破解...
    沈念sama閱讀 206,214評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異绳瘟,居然都是意外死亡雕憔,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,307評論 2 382
  • 文/潘曉璐 我一進(jìn)店門糖声,熙熙樓的掌柜王于貴愁眉苦臉地迎上來斤彼,“玉大人分瘦,你說我怎么就攤上這事×鹞” “怎么了嘲玫?”我有些...
    開封第一講書人閱讀 152,543評論 0 341
  • 文/不壞的土叔 我叫張陵,是天一觀的道長并扇。 經(jīng)常有香客問我去团,道長,這世上最難降的妖魔是什么拜马? 我笑而不...
    開封第一講書人閱讀 55,221評論 1 279
  • 正文 為了忘掉前任渗勘,我火速辦了婚禮,結(jié)果婚禮上俩莽,老公的妹妹穿的比我還像新娘旺坠。我一直安慰自己,他們只是感情好扮超,可當(dāng)我...
    茶點故事閱讀 64,224評論 5 371
  • 文/花漫 我一把揭開白布取刃。 她就那樣靜靜地躺著,像睡著了一般出刷。 火紅的嫁衣襯著肌膚如雪璧疗。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,007評論 1 284
  • 那天馁龟,我揣著相機(jī)與錄音崩侠,去河邊找鬼。 笑死坷檩,一個胖子當(dāng)著我的面吹牛却音,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播矢炼,決...
    沈念sama閱讀 38,313評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼系瓢,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了句灌?” 一聲冷哼從身側(cè)響起夷陋,我...
    開封第一講書人閱讀 36,956評論 0 259
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎胰锌,沒想到半個月后骗绕,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,441評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡匕荸,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,925評論 2 323
  • 正文 我和宋清朗相戀三年爹谭,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片榛搔。...
    茶點故事閱讀 38,018評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡诺凡,死狀恐怖东揣,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情腹泌,我是刑警寧澤嘶卧,帶...
    沈念sama閱讀 33,685評論 4 322
  • 正文 年R本政府宣布,位于F島的核電站凉袱,受9級特大地震影響芥吟,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜专甩,卻給世界環(huán)境...
    茶點故事閱讀 39,234評論 3 307
  • 文/蒙蒙 一钟鸵、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧涤躲,春花似錦棺耍、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,240評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至嫩挤,卻和暖如春害幅,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背岂昭。 一陣腳步聲響...
    開封第一講書人閱讀 31,464評論 1 261
  • 我被黑心中介騙來泰國打工以现, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人约啊。 一個月前我還...
    沈念sama閱讀 45,467評論 2 352
  • 正文 我出身青樓叼风,卻偏偏與公主長得像,于是被迫代替她去往敵國和親棍苹。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 42,762評論 2 345

推薦閱讀更多精彩內(nèi)容

  • 上一篇我們講解了ButterKnife的設(shè)計思想茵汰,理解了ButterKnife綁定相關(guān)源碼的實現(xiàn)邏輯枢里。但是它是怎么...
    Ihesong閱讀 992評論 0 2
  • 什么是注解注解分類注解作用分類 元注解 Java內(nèi)置注解 自定義注解自定義注解實現(xiàn)及使用編譯時注解注解處理器注解處...
    Mr槑閱讀 1,070評論 0 3
  • 博文出處:ButterKnife源碼分析,歡迎大家關(guān)注我的博客蹂午,謝謝栏豺! 0x01 前言 在程序開發(fā)的過程中,總會有...
    俞其榮閱讀 2,021評論 1 18
  • 本文主要介紹Android之神JakeWharton的一個注解框架豆胸,聽說現(xiàn)在面試官現(xiàn)在面試都會問知不知道JakeW...
    Zeit丶閱讀 978評論 4 6
  • 取模運算(即取余數(shù)) 10%4 = 2 a%b 當(dāng)a < b ,結(jié)果是a, 例:2%5=2奥洼; 當(dāng) a=b,結(jié)果...
    大夢想家程序員閱讀 26,691評論 0 3