1. 程式人生 > >View註解框架----ButterKnife

View註解框架----ButterKnife

一.簡介

ButterKnife 是 jake Wharton 的一個用於 View 註解框架,目前已經有 22000+ star , ButterKnife 的用處就是為開發者減少類似於 findViewById, setOnClickListener 等重複的程式碼,取代之的是通過註解對程式碼進行標記,讓編譯器自動生成需要的程式碼。

那麼 ButterKnife 究竟怎麼實現的呢?簡單地說就是使用了註解,通常我們對註解的處理,又兩種方式,一是通過反射處理註解,一種是通過註解處理器,而 ButterKnife 就是通過註解處理器實現對註解的處理,但是在整個框架的其他地方中還是有使用到反射的。

二.反射

1.反射的由來

java 物件在執行的時候有時因為多型的原因會產生兩種型別 :

  • 編譯時型別
  • 執行時型別

比如對於 Person p = new Student,它的編譯的時型別就是 Person,而執行時型別就是 Student。對於編譯時型別在編譯前就可以確定,而對於執行時型別,因為只有在執行的時候才能確定,所以有時不能直接通過物件進行訪問。但是有時程式需要呼叫的是物件在執行時的型別方法,那有該怎麼辦呢? 解決方式有兩種:

  • 在編譯時和執行的都完全知道型別的具體資訊,使用 instanceof 進行,再進行型別轉換。
  • 無法知道該物件屬於哪些類,程式只能依靠執行時資訊確定,就必須使用反射。

2.反射的使用

反射就是根據一些類的資訊確定一個物件屬於哪個類還可以因此生成一個該類的物件。首先就是獲取一個類的 Class 物件,每一個類都有一個 Class 物件,儲存著這個類的資訊。

(1)獲取 Class 物件
  • Class forName(“全限定類名”)。
  • 通過某個類的 Class 屬性,獲得該類對應的 Class 物件。比如 String.class
  • 呼叫某個物件的 getClass 方法。
  • 通過類載入器,Class.loadClass(className)
(2)從Class 中獲取資訊
  • getConstructor 等,獲取構造器
  • getMethod 等,獲取 Class 對應類的方法
  • getField 等,獲取類的成員變數
  • getAnnotation 等,獲取 class 上的 Annotation 就是註解資訊。
  • 等等
(3)通過反射生成並操作一個物件

1.建立一個物件

  • Class newInstance,要求有對應類有預設構造器
  • 獲得制定的構造器,再呼叫構造器的 newInstance ,這種方式可以可以選擇使用指定的構造器器建立例項。

2.呼叫方法

每個 Method 有一個 invoke (Object ,Object…)方法,Object 是方法的主調就是方法的呼叫物件,Object… 是方法的引數

3.訪問成員變數

通過 getFields/getField 可以對成員變數進行訪問,set/get 方法。

在訪問成員變數和呼叫方法的時候需要對他們的訪問許可權做一些處理,setAccessible(boolean flag) :

  • true,指示在使用方法或者成員變數的時候取消 java 的 訪問許可權檢查。
  • false,指示 Method 在使用的時候 實施 java 的訪問許可權檢查。

三.註解 Annotation

註解就是對部分程式碼進行標記,然後可以在編譯,類載入,執行的時候被讀取,並執行相應的處理, 從而對原始檔補充一些資訊。

1.基本的 Annotation

  • @Override ,指定一個方法必須重寫父類的方法,就是告訴編譯器,檢查這個方法,保證父類有一個父類重寫的方法,否則編譯就出錯。
  • @Deprecated,標記過時的方法等
  • @SupperssWarnings,指示被修飾的程式元素,取消顯示指定的編譯器警告,要在括號內使用 name = value ,為該 Annotation 的成員變數設定值。
  • 等等

2.自定義的 Annotation

定義一個 Annotation 與介面類似,不過使用的是 @interface 而不是 interface 。一個 Annotation,可以帶成員變數 且以無形參的方法來宣告,其方法和返回值定義了成員變數的名字和型別。也可以設定預設值 default .

自定義的 Annotation 可以分為兩類:

  • 包含成員變數,可以接受更多的元資料,也稱為元資料 Annotation。
  • 沒有成員包含成員變數,成為標記的 Annotation 。

2.元 Annotation

元 Annotation 就是對註解進行註解,換句話說就是對自定義的註解補充一些資訊。

[email protected]

只能用於修飾 Annotation 定義,也就是在自定義一個註解的時候修飾在 @interface 上方。Retention 指定被修飾的 Annotation 可以保留多長的時間,具體策略需要 指定 value 成員變數,有三種可選的時間長度策略。

  • RetentionPolicy.Class 記錄在 class 檔案中,即執行時,在 JVM 不可獲取 Annotation。
  • RetentionPolicy.Runtime 可以在 執行的時候也獲得,程式可以通過反射的獲取該 Annotation。
  • RetentionPolicy.SOURCE 只保留在原始碼中,編譯的時候就直接丟棄。
[email protected]

用於指定修飾的 Annotation 能用於修飾哪些程式元素,包含一個 value 變數,同樣只能修飾一個 Annotation 定義。具體有以下策略

  • ElementType.ANNOTATION_TYPE,指定定義的 Annotation 只能修飾 Annotation。
  • ElementType.CONSTRUCTOR 指定定義的 Annotation 只能修飾構造器
  • ElementType.FIELD 指定定義的 Annotation 只能修飾 成員變數
  • ElementType.LOCAL_VARIABLE 指定定義的 Annotation 只能修飾 區域性變數
  • ElementType.METHOD 指定修飾的 Annotation 只能修飾方法定義
  • ElementType.PACKAGE 指定修飾的 Annotation 只能修飾包定義
  • ElementType.Type 指定修飾的 Annotation 可以修飾類介面或者列舉定義
[email protected]

指定修飾的 Annotation 具有繼承性,則繼承的類都具有預設定義的 Annotation。

3.提取 Annotation

只有 使用 @Retention(RetentionPolicy.RUNTIME )修飾,JVM 才會在 裝載 class 的時候提取 Annotation ,因為註解可以修飾類,方法等,因此註解提取的資訊是一個 AnnotationElement,這是所有程式元素的(class , Method , Constructor ) 的父介面。也可以通過 getAnnotation 等直接提取出 Annotation 資訊。

4.編譯時註解 Annotation

前面說過註解資訊的提取實在 JVM 裝載 class 的時候提取的,因此 @Retention 的 引數就設定為 RetentionPolicy.RUNTIME ,但是這多少對 JVM 的效能有所消耗,那有什麼優化的方法嗎?答案就是使用 編譯時處理 Annotation 技術,Annotation Processing Tool 簡稱 APT。 APT 的原理就是通過繼承 繼承 AbstractProcessor 類實現一個 APT 工具,並裝在到編譯類庫中,編譯的時候就會使用 APT 工具處理 Annotation, 可以根據原始檔中的 Annotation 生成額外的原始檔和其他檔案,APT 還會編譯生成的程式碼檔案和原來的原始檔將他們一起生成 class 檔案,這些附屬檔案的內容也都與原始碼的相關。而對註解的資訊的處理通常就放在生成的檔案中,在執行時就不用再提取,直接關聯並執行處理即可。

三.ButterKnife

下面以 findViewById 和 setOnClickListener 對應的 @BindView 和 @ OnClick 為例簡單介紹 ButterKnife 巨集觀實現。

1.定義註解

在 ButterKnife 原始碼中 butterknife-annotations 庫主要用來存放自定義註解。可註解的型別如圖所示:

image.png

@BindView 的註解:

/**
 * Bind a field to the view for the specified ID. The view will automatically be cast to the field
 * type.
 * <pre><code>
 * {@literal @}BindView(R.id.title) TextView title;
 * </code></pre>
 */
@Retention(RUNTIME) @Target(FIELD)
public @interface BindView {
  /** View ID to which the field will be bound. */
  @IdRes int value();
}

BindView 有兩個註解修飾,從前面的關於註解的知識就可以知道 @Retention(RUNTIME) 表示在保留註解資訊到執行的時候, @Target(FIELD) 表示這個註解只能修飾成員變數。

@OnClick 的註解

/**
 * Bind a method to an {@link OnClickListener OnClickListener} on the view for each ID specified.
 * <pre><code>
 * {@literal @}OnClick(R.id.example) void onClick() {
 *   Toast.makeText(this, "Clicked!", Toast.LENGTH_SHORT).show();
 * }
 * </code></pre>
 * Any number of parameters from
 * {@link OnClickListener#onClick(android.view.View) onClick} may be used on the
 * method.
 *
 * @see OnClickListener
 */
@Target(METHOD)
@Retention(RUNTIME)
@ListenerClass(
    targetType = "android.view.View",
    setter = "setOnClickListener",  
    type = "butterknife.internal.DebouncingOnClickListener",
    method = @ListenerMethod(
        name = "doClick",
        parameters = "android.view.View"
    )
)
public @interface OnClick {
  /** View IDs to which the method will be bound. */
  @IdRes int[] value() default { View.NO_ID };
}

BindView 有三個註解修飾, @Retention(RUNTIME) 表示在保留註解資訊到執行的時候, @Target(METHOD) 表示這個註解只能方法,@ListenerClass 就是對方法進一步描述,比如呼叫者型別 targetType,呼叫方法 setter,方法監聽 type,最後執行的方法和方法引數 name ,parameters。

2.註解處理器

ButterKnifeProcessor 註解處理器是 APT 中對註解處理的關鍵類,在 ButterKnife 中註解處理器位於 butterknife - compiler 包中。

image.png

ButterKnifeProcessor 繼承自 AbstractProcessor ,而 AbstractProcessor 實現了 Processor 介面,這個介面主要有兩個重要的方法。

public interface Processor {
   
    ...
    void init(ProcessingEnvironment processingEnv);
    
    ...
    
    boolean process(Set<? extends TypeElement> annotations,
                    RoundEnvironment roundEnv);
    ...
}

init 方法 :

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

    String sdk = env.getOptions().get(OPTION_SDK_INT);
    if (sdk != null) {
      try {
        this.sdk = Integer.parseInt(sdk);
      } catch (NumberFormatException e) {
        env.getMessager()
            .printMessage(Kind.WARNING, "Unable to parse supplied minSdk option '"
                + sdk
                + "'. Falling back to API 1 support.");
      }
    }

    debuggable = !"false".equals(env.getOptions().get(OPTION_DEBUGGABLE));
    useAndroidX = hasAndroidX(env.getElementUtils());

    typeUtils = env.getTypeUtils();
    filer = env.getFiler();
    try {
      trees = Trees.instance(processingEnv);
    } catch (IllegalArgumentException ignored) {
    }
  }

這個方法主要的作用是:

  • 獲取java SDK 的版本
  • 獲取 ElementUtils,用於處理 Element ,Element 就是 Java 中的包,類等元素。
  • 獲取 TypeUtils,用於處理 javaType,Type 就是 java 的型別元素,比如有原始型別(普通的型別),引數化型別(比如List),陣列型別,原生型別(int ,long) 等。
  • 獲取 Filer ,用於最後生成相關的檔案。

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

      JavaFile javaFile = binding.brewJava(sdk, debuggable, useAndroidX);
      try {
        javaFile.writeTo(filer);
      } catch (IOException e) {
        error(typeElement, "Unable to write binding for type %s: %s", typeElement, e.getMessage());
      }
    }

    return false;
  }

在 brewJava 方法中 有如下程式碼:

 static Builder newBuilder(TypeElement enclosingElement) {
    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('.', '$');
    ClassName bindingClassName = ClassName.get(packageName, className + "_ViewBinding");   //注意這段命名

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

對於 process 方法,主要做了如下幾件事:

  • 呼叫 findAndParseTargets ,掃描所有的 ButterKnife 註解。
  • 呼叫 brewJava 生成 JavaFile。
  • 最後生成一個 對應的 class 檔案。這個class 檔名可以在 brewJava 的方法中可以看到,對應著 bindingClassName ,所以最後生成的 class 名字就為 xxxx_ViewBinding.

為了驗證,在 MainActicity 中使用 ButterKnife 註解,然後執行一下,最後可以在 build-intermediates-classes 下找到對應的 MainActivity_ViewBinding。

image.png

2.繫結 bind 方法

在經過前面兩個步驟後,對於 ButterKnife 的使用也就是一句 bind 方法。

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

    }

直接看 bing 方法的具體實現

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

對於 bind 方法,首先就是獲取 目前 Window 的一個 根檢視 DecorView ,這裡可以比較一下 findViewById 的實現中有這麼一段

public static AppCompatDelegate create(Activity activity, AppCompatCallback callback) {
        return create(activity, activity.getWindow(), callback);
    }

通過比較就可以看出,對於註解來說最後的實現還是一樣的。下面接著看 createBinding

private static Unbinder createBinding(@NonNull Object target, @NonNull View source) {
    Class<?> targetClass = target.getClass();
    
    Constructor<? extends Unbinder> constructor = findBindingConstructorForClass(targetClass);

   
    try {
      return constructor.newInstance(target, source);
    } catch 
      ...
  }

在這裡 會先去獲取一個物件的構造器 ,然後呼叫這個構造器去例項化一個物件 。獲取構造器的方法對應著 findBindingConstructorForClass

  private static Constructor<? extends Unbinder> findBindingConstructorForClass(Class<?> cls) {
  
    Constructor<? extends Unbinder> bindingCtor = BINDINGS.get(cls);
   
    String clsName = cls.getName();
    try {
      Class<?> bindingClass = Class.forName(clsName + "_ViewBinding");
      
      bindingCtor = (Constructor<? extends Unbinder>) bindingClass.getConstructor(cls, View.class);
     ...
    BINDINGS.put(cls, bindingCtor);
    return bindingCtor;
  }
  }

在這個方法中,可以看到這個構造器的類就是以 ++clsName + “_ViewBinding”++,為名字的,以 MainActivity 為例就是對應著前面的 MainActivity_ViewBinding 。

最後是通過構造器的 newInstance(target, source) 方法建立了一個物件,並將 Context 物件(target) 和 DecorView (source) 傳過去。

以 @BindView 和 @OnClick 為例,最後看生成的 MainActivity_ViewBinding 類。

public class MainActivity_ViewBinding<T extends MainActivity> implements Unbinder {
    protected T target;
    private View view2131165218;

    @UiThread
    public MainActivity_ViewBinding(final T target, View source) {
        this.target = target;
        View view = Utils.findRequiredView(source, 2131165218, "field 'mButtonOne' and method 'click'");
        target.mButtonOne = (Button)Utils.castView(view, 2131165218, "field 'mButtonOne'", Button.class);
        this.view2131165218 = view;
        view.setOnClickListener(new DebouncingOnClickListener() {
            public void doClick(View p0) {
                target.click();
            }
        });
        target.mButtonTwo = (Button)Utils.findRequiredViewAsType(source, 2131165219, "field 'mButtonTwo'", Button.class);
    }

    @CallSuper
    public void unbind() {
        T target = this.target;
        if(target == null) {
            throw new IllegalStateException("Bindings already cleared.");
        } else {
            target.mButtonOne = null;
            target.mButtonTwo = null;
            this.view2131165218.setOnClickListener((OnClickListener)null);
            this.view2131165218 = null;
            this.target = null;
        }
    }
}

到這裡就很清楚了,所有的 findViewById 和 setOnClickListener 等操作最後都是在這裡實現了。 所有的引數都是從構造器裡傳進來的,進行繫結。

ButterKnife 實現的大致流程:

  • 自定義註解
  • 實現註解處理器
  • 使用註解進行標記,在編譯的時候通過註解處理器生成對應的 xx_ViewBind 類
  • 執行的時候,呼叫 bind 進行繫結,通過反射獲取 對應的 xx_ViewBinding 類的構造器,將 Context 物件和 DecorView 傳遞。
  • 在 xx_ViewBinding 實現類似於 findViewId 或者 setOnClickListener 等方法。

總的來說,ButterKnife 在註解的處理上使用的是 註解處理器,在編譯的時候將註解處理好,從而減少執行的時候對虛擬機器效能的消耗。