1. 程式人生 > >butterknife及其背後的程式碼生成技術

butterknife及其背後的程式碼生成技術

本篇博文主要介紹butterknife使用及其背後的技術點,行文結構如下

0x00 butterknife

github原文是這樣介紹的

Field and method binding for Android views which uses annotation processing to generate boilerplate code for you.

翻譯過來就是:

用註解處理器為程式在編譯期生成一些樣板程式碼,用於把一些屬性欄位和回撥方法繫結到 Android 的 View,即專門為Android View設計的繫結註解,專業解決各種findViewById。

0x01 基本使用

配置

在主工程中

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

在Library中使用

首先需要 在專案buildscript中增加

buildscript {
  repositories {
    mavenCentral()
   }
  dependencies {
    classpath 'com.jakewharton:butterknife-gradle-plugin:8.8.1'
  }
}

然後在moudle中

apply plugin: 'com.android.library'
apply plugin: 'com.jakewharton.butterknife'

並且使用R2而不是R

class ExampleActivity extends Activity {
  @BindView(R2.id.user) EditText username;
  @BindView(R2.id.pass) EditText password;
    ...
}

Tips:使用R2在Library中是由於在Library中R不是final的,但是註解如BindView需要R裡面的id為final的。

而且通過classShark分析來看,該庫的方法數比較少,僅有112個

常規使用

  • Actvity中

對一個成員變數使用@BindView註解,並傳入一個View ID, ButterKnife 就能夠幫你找到對應的View,並自動的進行轉換(將View轉換為特定的子類):

  • 資源繫結

繫結資源到類成員上可以使用@BindBool、@BindColor、@BindDimen、@BindDrawable、@BindInt、@BindString。使用時對應的註解需要傳入對應的id資源,例如@BindString你需要傳入R.string.id_string的字串的資源id。

  • 佈局繫結

Butter Knife提供了bind的幾個過載,只要傳入跟佈局,便可以在任何物件中使用註解繫結,通常使用在fragment和adapter

  • Fragment中

    public class FancyFragment extends Fragment {
    
        @BindView(R.id.button1)
        Button button1;
        @BindView(R.id.button2)
        Button button2;
    
        @Override
        public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
            View view = inflater.inflate(R.layout.fancy_fragment, container, false);
            ButterKnife.bind(this, view); // TODO Use fields... return view; } }
    }
    
  • Adapter中

    public class MyAdapter extends BaseAdapter {
        @Override
        public View getView(int position, View view, ViewGroup parent) {
            ViewHolder holder;
            if (view != null) {
                holder = (ViewHolder) view.getTag();
            } else {
                view = inflater.inflate(R.layout.whatever, parent, false);
                holder = new ViewHolder(view);
                view.setTag(holder);
            }
            holder.name.setText("John Doe"); // etc... return view; } static class ViewHolder { @BindView(R.id.title) TextView name; @BindView(R.id.job_title) TextView jobTitle; public ViewHolder(View view) {
                ButterKnife.bind(this, view);
            }
    
        }
    }
    
  • 監聽器繫結

監聽器能夠自動的繫結到特定的執行方法上:

//單個繫結
@OnClick(R.id.submit)
public void submit(View view) {
  // TODO submit data to server...
}

//多個繫結
@OnClick({R.id.btnJumpToLib, R.id.btnOne, R.id.btnTwo})
void responseClick(View view) {
    switch (view.getId()) {
        case R.id.btnJumpToLib:
            jumpToLib();
            break;
        case R.id.btnOne:
            Toast.makeText(this, "click btnOne", Toast.LENGTH_SHORT).show();
            break;
        case R.id.btnTwo:
            Toast.makeText(this, "click btnTwo", Toast.LENGTH_SHORT).show();
            break;
        default:
            break;
    }

}
  • 重置繫結

Fragment的生命週期與Activity不同。在Fragment中,如果你在onCreateView中使用繫結,那麼你需要在onDestroyView中設定所有view為null。為此,ButterKnife返回一個Unbinder例項以便於你進行這項處理。在合適的生命週期回撥中呼叫unbind函式就可完成重置。

public class FancyFragment extends Fragment {
    @BindView(R.id.button1)
    Button button1;
    @BindView(R.id.button2)
    Button button2;
    private Unbinder unbinder;

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        View view = inflater.inflate(R.layout.fancy_fragment, container, false);
        unbinder = ButterKnife.bind(this, view); // TODO Use fields...
        return view;
    }

    @Override
    public void onDestroyView() {
        super.onDestroyView();
        unbinder.unbind();
    }
}
  • 可選繫結

在預設情況下, @bind和監聽器的繫結都是必須的,如果目標view沒有找到的話,Butter Knife將會丟擲個異常。

如果你並不想使用這樣的預設行為而是想建立一個可選的繫結,那麼你只需要在變數上使用@Nullable註解或在函式上使用@Option註解。

注意事項

  • 在Activity中 ButterKnife.bind(this);必須在setContentView()之後,且父類bind繫結後,子類不需要再bind
  • 在Fragment中使用 ButterKnife.bind(this, mRootView);
  • 屬性佈局不能用private or static 修飾,否則會報錯
  • setContentView() 不能通過註解實現。
  • ButterKnife已經更新到版本8.0.7了,以前的版本中叫做@InjectView了,而現在改用叫@Bind,更加貼合語義。
  • 在Fragment生命週期中,onDestoryView也需要Butterknife.unbind(this)
  • 在Libbray中使用R2.id.xxx

0x02 程式碼生成技術探索

從上面的使用我們可以看出,一個註解就可以將相關聯的程式碼自動生成如demo中的實現了Unbinder介面的MainActivity_ViewBinding檔案(build/generated/source/apt/)。這裡涉及到3個核心技術

  • 編譯期註解
  • APT(註解處理器)
  • javaPoet(自動生成程式碼)

在分析ButterKnife原始碼之前首先需要了解以上3個技術點,下面將逐一介紹這三個技術點,最後再聊聊

註解

Java自帶的註解

主要分類兩大類

  • 元註解

  • 普通註解

元註解(meta-annotation)

翻譯一下就是“註解的註解”,即註解用來註解其他註解的註解,公有4個,常用於自定義註解

  • @Target

Target描述了這個註解的使用範圍,使用方法如@Target(ElementType.TYPE),ElementType的取值有七種,如下:

ElemenetType.CONSTRUCTOR 構造器宣告
ElemenetType.FIELD 域宣告(包括 enum 例項)
ElemenetType.LOCAL_VARIABLE 區域性變數宣告
ElemenetType.METHOD 方法宣告
ElemenetType.PACKAGE 包宣告
ElemenetType.PARAMETER 引數宣告
ElemenetType.TYPE 類,介面(包括註解型別)或enum宣告
  • @Retention

Retention用來描述這個註解的生命週期,英文意思為“保留、保持”、,即註解的“存活時間”,使用方法如下

@Retention(RetentionPolicy.RUNTIME)

儲存策略總共有3種

SOURCE:  Annotation只保留在原始碼中,當編譯器編譯的時候就會拋棄它。(即原始檔保留)
CLASS:   編譯器將把Annotation記錄在Class檔案中,不過當java程式執行的時候,JVM將拋棄它。(即class保留)
RUNTIME: 在Retationpolicy.CLASS的基礎上,JVM執行的時候也不會拋棄它,所以我們一般在程式中可以通過反射來獲得這個註解,然後進行處理。

Tips:

  • 我們知道,Java程式碼會有原始碼(java)經過編譯器編譯成class檔案(二進位制位元組碼),然後由JVM虛擬機器去解釋執行,Retention用於描述註解在這個階段存活的時間,因此根據註解不同的生命週期會有不同的處理註解的方式,這一點非常重要。之前註解往往被人詬病使用反射速度慢,其實使用的是Runtime策略的註解,而butterKnife一些使用的是class策略的註解,僅僅會影響編譯期速度,但是由於僅僅儲存在位元組碼中,因此需要通過其他的手段把註解資訊保留下來傳遞到虛擬機器去執行,通常使用的是自定義註解處理器
  • 首先要明確生命週期長度 SOURCE < CLASS < RUNTIME ,所以前者能作用的地方後者一定也能作用。一般如果需要在執行時去動態獲取註解資訊,那隻能用 RUNTIME 註解;如果要在編譯時進行一些預處理操作,比如生成一些輔助程式碼(如 ButterKnife),就用 CLASS註解;如果只是做一些檢查性的操作,比如 @Override 和 @SuppressWarnings,則可選用 SOURCE 註解。

  • @Document

Document標記這個註解應該被 javadoc工具記錄。預設情況下,Javadoc是不包括註解的。

  • @Inherited

Inherited譯為可繼承的,如果一個使用了@Inherited 修飾的 annotation型別 被用於一個 class,則這個 annotation 將被用於該class的子類。

普通註解

用於描述程式碼的註解

自定義註解

通過使用元註解可以實現我們自己的註解,使用@interface自定義註解時,不能繼承其他的註解或介面。@interface用來宣告一個註解,其中的每一個方法實際上是聲明瞭一個配置引數。方法的名稱就是引數的名稱,返回值型別就是引數的型別,其中可以通過default來宣告引數的預設值。,看下butterknife中的自定義註解BindeView

    @Retention(CLASS) @Target(FIELD)
    public @interface BindView {
      /** View ID to which the field will be bound. */
      @IdRes int value();
    }

使用

@BindView(R.id.title) TextView title;

處理註解

具體例子請參看Demo

處理註解的方式跟元註解 @Retention 相關,再次強調一下

由於不同註解策略帶來的註解生命週期長度 SOURCE < CLASS < RUNTIME ,所以前者能作用的地方後者一定也能作用。一般如果需要在執行時去動態獲取註解資訊,那隻能用 RUNTIME 註解;如果要在編譯時進行一些預處理操作,比如生成一些輔助程式碼(如 ButterKnife),頁面路由資訊等,就用 CLASS註解;如果只是做一些檢查性的操作,比如 @Override 和 @SuppressWarnings,則可選用 SOURCE 註解。

@Retention(SOURCE)

原始碼註解(RetentionPolicy.SOURCE)的生命週期只存在Java原始檔這一階段,是3種生命週期中最短的註解。基本無需刻意去做處理,如@InDef、@StringDef等

@Retention(Class)

使用APT去處理註解

@Retention(RunTime)

生命週期最長通常可以使用反射,也可以使用自定義註解器

下面詳細介紹一下實現一個用APT處理@Retention(Class)策略的註解,在介紹之前首先得看下一java中處理註解的流程

Annotation processing 是javac中用於編譯時掃描和解析Java註解的工具

你可以定義註解,並且自定義解析器來處理他們,Annotation processing是在編譯階段執行的,它的原理就是讀入Java原始碼,解析註解,然後生成新的Java程式碼。新生成的Java程式碼最後被編譯成Java位元組碼,註解解析器(Annotation Processor)不能改變讀入的Java 類,比如不能加入或刪除Java方法

因為需要引用apt外掛,所以需要在 buildscript加入

        classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8'

那麼什麼是android-apt呢?官網有這麼一段描述:

The android-apt plugin assists in working with annotation processors in combination with Android Studio. It has two purposes:

1、Allow to configure a compile time only annotation processor as a dependency, not including the artifact in the final APK or library
2、Set up the source paths so that code that is generated from the annotation processor is correctly picked up by Android Studio

大體來講它有兩個作用:

- 能在編譯時期去依賴註解處理器並進行工作,但在生成 APK 時不會包含任何遺留的東西
- 能夠輔助 Android Studio 在專案的對應目錄中存放註解處理器在編譯期間生成的檔案

自定義註解器需要繼承AbstractProcessor,然後實現如下重要方法:

  • init()
    初始化,得到Elements、Types、Filer等工具類
  • getSupportedAnnotationTypes()
    描述註解處理器需要處理的註解
  • process()
    掃描分析註解,生成程式碼

並且在生成檔案時使用到了javaPoet來自動生成程式碼,javaPoet主要是用來

JavaPoet is a Java API for generating .java source files.

0x03 butterKnife核心原始碼分析

有了上面的基礎現在可以好好分析一下了,現在再次回憶下上面提到註解處理流程圖,

butterknife的核心思路就是

在編譯原始檔時,會分析掃描註解,當掃描到butterknife定義的@BindView、@OnClick等註解時,會使用JavaPoet來生成程式碼。生成後的檔案會再次分析,直到沒有分析到需要處理的註解位置。

千萬不要說成用註解+反射哦~

分析原始碼主要分為以下兩步:

  • apt 自動生成的類
  • 生成的類是如何關聯到butterknife內部框架

自動生成的類

先看下demo中自動生成的類的部分程式碼

從這個生成類我們可以獲取如下資訊:

  • butterknife通過註解生成了一些輔助程式碼,從它的框架層面幫我們遮蔽了繁瑣的細節

  • 需要獲取decorview才能對相關Id的view進行操作(這也印證了之前的一個點,在Activity中 ButterKnife.bind(this);必須在setContentView()之後)

Tips:

  • 作為應用程式的主執行緒,ActivityThread負責處理各種核心事件,如AMS通知應用程序去啟動一個Activity這個任務,最終將轉化為ActvityThread所管理的LAUNCH_ACTIVITY訊息,然後呼叫handleLaunchActivity,這是整個ViewTree建立流程的起點
  • 該函式主要生成一個Activity物件,並呼叫他們的attach方法,然後通過mInstrumentation.callActivityOnCreate呼叫Activity.onCreate,從而得到一個PhoneWindow物件。其中window物件在Activity中可以被看成“介面的框架”,因此有了框架之後還需要生成具體的內容,即Activity的mDecor(而產生DecorView則是由setContentView發起的)
  • Activity的setContentView只是一箇中介,它將通過對應的Window物件來完成DecorView的構造,具體參看PhoneWindow#setContentView#installDecor()
  • 從Zygote程序fork出應用程序之後,會通過反射來呼叫ActivityThread的main方法.具體是通過RuntimeInit的invokeStaticMain方法中

如何消除了fidviewById的強轉?

在生成程式碼中可以看到如下程式碼

 target.mTvTitle = Utils.findRequiredViewAsType(source, R.id.tvTitle, "field 'mTvTitle'", TextView.class);

Utils#findRequiredViewAsType,其中source就是之前傳入的taget.getWindow.getDecoreView

  public static <T> T findRequiredViewAsType(View source, @IdRes int id, String who,
      Class<T> cls) {
    View view = findRequiredView(source, id, who); //找到view
    return castView(view, id, who, cls);//強轉 如(TexteView)mTextView之類
  }


  public static View findRequiredView(View source, @IdRes int id, String who) {
    View view = source.findViewById(id); //從decoreView中執行findViewById
    if (view != null) {
      return view;
    }
    ……
  }

  public static <T> T castView(View view, @IdRes int id, String who, Class<T> cls) {
    try {
      return cls.cast(view);
    } catch (ClassCastException e) {
    ……
    }
  }

MainActivity如何知曉框架為它生成的MainActivity_ViewBind這個輔助類並例項化?

還記得在使用之前需要使用bind函式,以activity為例,

ButterKnife.bind(this);

ButterKnife#bind

    @NonNull @UiThread
  public static Unbinder bind(@NonNull Activity target) {
    View sourceView = target.getWindow().getDecorView(); //獲取devoreView
    return createBinding(target, sourceView); //執行繫結操作
  }

繼續看下繫結操作做了什麼

ButterKnife#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 {
    //通過反射建立的例項 MainActivity_ViewBinding(final MainActivity target, View source)
      return constructor.newInstance(target, source); 
    } catch (IllegalAccessException e) {
        ……
    }
  }

這個函式就是先找到對應的構造器,然後建立例項,核心點進一步交到了findBindingConstructorForClass函式中

  @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 {
      //獲得到對應的viebindind類,檔案的命名規則是類名 + "_ViewBinding"
      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);
    }
    //將得到的Constructor快取起來,避免反射的效能問題。
    BINDINGS.put(cls, bindingCtor);
    return bindingCtor;
  }

如何處理註解然後對應的程式碼輔助檔案

先看下butterKnife工程結構

所有涉及到的註解都在butterknife-annotations中,butterknife-complier就是自定義的註解處理器(處理Class策略的註解),然後生成對應的程式碼輔助檔案

正如之前提到的 註解處理器裡包含下面幾個重要的方法:

  • init()
    初始化,得到Elements、Types、Filer等工具類
  • getSupportedAnnotationTypes()
    描述註解處理器需要處理的註解
  • process()
    掃描分析註解,生成程式碼

因此核心點都在註解處理器的process()函式中,要抓住重點

ButterKnifeProcessor#process

  @Override public boolean process(Set<? extends TypeElement> elements, RoundEnvironment env) {
    //獲取bing資訊,重點!
    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;
  }

可以看到核心函式十分簡練,分工明確,找到工程中所有bind的資訊,然後生成對應的檔案

先看找繫結資訊函式 ButterKnifeProcessor#findAndParseTargets,這個函式超長,主要是解析定義的各種註解,我們這裡擷取解析BindeView一段來看,其他原理都一樣

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

    scanForRClasses(env);


    // Process each @BindView element.
    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;
  }

這個方法裡是處理各種註解的主方法,多餘的我都刪掉了,這個方法主要是獲取所有的註解,然後解析註解,把註解的所有資訊封裝到BindingSet中,那麼解析的具體操作應該就在parseBindView(element, builderMap, erasedTargetNames)中,接著往下看:

butterknife#parseBindView 解析BindView註解的方法

private void parseBindView(Element element, Map<TypeElement, BindingSet.Builder> builderMap,
      Set<TypeElement> erasedTargetNames) {
    TypeElement enclosingElement = (TypeElement) element.getEnclosingElement();
    ……
    // Assemble information on the field.
    //這裡開始看到解析BindView註解所標記的id
    int id = element.getAnnotation(BindView.class).value();

    BindingSet.Builder builder = builderMap.get(enclosingElement);
    QualifiedId qualifiedId = elementToQualifiedId(element, id);
    ……
    String name = simpleName.toString();
    TypeName type = TypeName.get(elementType);
    boolean required = isFieldRequired(element);
    // id作為屬性放入到BindSet中
    builder.addField(getId(qualifiedId), new FieldViewBinding(name, type, required));

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

其他註解類似處理,

程式碼生成-應用javaPoet框架

程式碼生成入口是在butterknife#process,使用了javaPoet的JavaFile

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

呼叫函式鏈

brewJava->createType

  private TypeSpec createType(int sdk, boolean debuggable) {
    //生成類名,修飾符是pblic
    TypeSpec.Builder result = TypeSpec.classBuilder(bindingClassName.simpleName())
        .addModifiers(PUBLIC);
    if (isFinal) {
      result.addModifiers(FINAL);
    }
    // 繼承關係和應用介面資訊
    if (parentBinding != null) {
      result.superclass(parentBinding.bindingClassName);
    } else {
      //private static final ClassName UNBINDER = ClassName.get("butterknife", "Unbinder");因此生成類最終實現Unbider介面
      result.addSuperinterface(UNBINDER); 
    }
    ……
    // 根據型別,新增不一樣的構造方法
    if (isView) {
      result.addMethod(createBindingConstructorForView());
    } else if (isActivity) {
      //activity建構函式預設建構函式,一個引數
      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的一些函式,預設建構函式會呼叫這個建構函式,若對這句話不好理解,可閱讀生成程式碼2個建構函式
    result.addMethod(createBindingConstructor(sdk, debuggable));
    if (hasViewBindings() || parentBinding == null) {
    //建立unbinder函式
      result.addMethod(createBindingUnbindMethod(result));
    }
    return result.build();
  }
  //生成activy中的建構函式
  private MethodSpec createBindingConstructorForActivity() {
    MethodSpec.Builder builder = MethodSpec.constructorBuilder()
        .addAnnotation(UI_THREAD)
        .addModifiers(PUBLIC)
        .addParameter(targetTypeName, "target");
    if (constructorNeedsView()) {
      builder.addStatement("this(target, target.getWindow().getDecorView())");
    } else {
      builder.addStatement("this(target, target)");
    }
    return builder.build();
  }

該函式負責生成諸多函式,如主要建構函式,包含findView的繫結的多個建構函式,解繫結unbinder函式等等

這裡我們可以看出生成類的規範,如實現了Unbinder介面,也看到了之前MainActivity_ViewBinding類預設建構函式自動生成的程式碼,這裡只是一個類的大概,只生成了一個引數的建構函式,具體涉及到內部的各種view,id的操作的建構函式還在上面的result.addMethod(createBindingConstructor(sdk, debuggable));函式中完成,

  private MethodSpec createBindingConstructor(int sdk, boolean debuggable) {
     // 建立構造方法,方法修飾符為 public ,並且添加註解為UiThread
    MethodSpec.Builder constructor = MethodSpec.constructorBuilder()
        .addAnnotation(UI_THREAD)
        .addModifiers(PUBLIC);
    ……
    // 如果有註解的 View
    if (constructorNeedsView()) {
      constructor.addParameter(VIEW, "source");
    } else {
      // 否則新增 Context context 引數
      constructor.addParameter(CONTEXT, "context");
    }
    ……
    // 如果有 View 繫結
    if (hasViewBindings()) {
      if (hasViewLocal()) {
        // Local variable in which all views will be temporarily stored.
        constructor.addStatement("$T view", VIEW);
      }
      for (ViewBinding binding : viewBindings) {
        // 為 View 繫結生成類似於 findViewById 之類的程式碼!!!
        addViewBinding(constructor, binding, debuggable);
      }
      for (FieldCollectionViewBinding binding : collectionBindings) {
        constructor.addStatement("$L", binding.render(debuggable));
      }

      if (!resourceBindings.isEmpty()) {
        constructor.addCode("\n");
      }
    }
    ……
    return constructor.build();
  }

可以看到生成程式碼的邏輯,其中有個重要的那些findView的方法,addViewBinding,程式碼解析如下

private void addViewBinding(MethodSpec.Builder result, ViewBinding binding) {
    if (binding.isSingleFieldBinding()) {
        // Optimize the common case where there's a single binding directly to a field.
        FieldViewBinding fieldBinding = binding.getFieldBinding();
        // 注意這裡直接使用了 target. 的形式,所以屬性肯定是不能 private 的
        CodeBlock.Builder builder = CodeBlock.builder()
                .add("target.$L = ", fieldBinding.getName());
        // 下面都是 View 繫結的程式碼
        boolean requiresCast = requiresCast(fieldBinding.getType());
        if (!requiresCast && !fieldBinding.isRequired()) {
            builder.add("source.findViewById($L)", binding.getId().code);
        } else {
            builder.add("$T.find", UTILS);
            builder.add(fieldBinding.isRequired() ? "RequiredView" : "OptionalView");
            if (requiresCast) {
                builder.add("AsType");
            }
            builder.add("(source, $L", binding.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 requiredBindings = binding.getRequiredBindings();
    if (requiredBindings.isEmpty()) {
        result.addStatement("view = source.findViewById($L)", binding.getId().code);
    } else if (!binding.isBoundToRoot()) {
        result.addStatement("view = $T.findRequiredView(source, $L, $S)", UTILS,
                binding.getId().code, asHumanDescription(requiredBindings));
    }

    addFieldBinding(result, binding);
    // OnClick 等監聽事件繫結
    addMethodBindings(result, binding);
}

至此程式碼生成部分分析結束~

0x04 小結

  • 本篇博文主要依託butetrknife框架,介紹它的使用,在分析原始碼之前鋪墊了其核心技術,註解的一些高階玩法、自定義註解處理器、javapoet自動生成程式碼,進而深入分析了butterknife原始碼;
  • 其中apt+javaPoet目前也是應用比較廣泛,在一些大的開源庫,如EventBus3.0+,頁面路由 ARout、Dagger、Retrofit等均有使用的身影
  • 註解不僅僅是通過反射一種方式來使用,也可以使用APT在編譯期處理

最後希望大家能夠有所收穫,週末愉快~

參考連結