一個例子理解 Butterknife 基本原理

說起 ButterKnife 相信大多人都知道這麼一個框架, 它是一個專注於 Android 系統的 View 注入框架, 簡化了我們的 findViewById, OnClick, getString() 以及加載動畫等操作, 給平時開發帶來了很大的便利. 只是現在這個框架的作者已經不再更新了, 只會修復一些關鍵性的 BUG, 同時建議使用 Googleview binding 了. 但是作爲曾經最流行的框架之一, 還是很有必要學習和研究一下的.

衆所周知 ButterKnife 的便利來自於註解. 那麼既然存在註解, 註解處理器技術的使用是必然的. 現在的框架不同於以往.
以前的框架類似 XUtils 的註解很大程度上是使用反射來解析的, 反射帶來性能消耗還是有的.
但是現在, 大多數的註解框架都是基於 AnnotationProcessor 的編譯時解析實現的.
試想一下, 在程序編譯時就完成了註解解析的工作, 又會給性能帶來什麼影響呢?答案當然是沒影響。
(APT 已不再被作者所維護, 並且 Google 推出了AnnotationProcessor 來替代它,更是集成到了 API 中)

  • 那麼什麼是 AnnotationProcessor呢 ?
    是一個 JavaC 的工具,也就是 Java 編譯源碼到字節碼的一個預編譯工具, 會在代碼編譯的時候調用到. 它有一個抽象類 AbstractProcessor, 只需實現該抽象類, 就可以在預編譯的時候被編譯器調用, 就可以在預編譯的時候完成一下你想完成工作. 比如代碼注入!!

那麼簡單來說 ButterKnife 在編譯時解析註解, 通過使用 AnnotationProcessor 代碼注入. 這是最基本最核心的思想. 那麼今天我們也來按照這個核心思想來寫一個山寨版的 ButterKnife.
先來看一下, 我們最終需要自動生成的文件內容是什麼樣的.

public final class MainActivity_ViewBinding implements Unbinder {
  private MainActivity target;

  MainActivity_ViewBinding(MainActivity target) {
    this.target = target;
    target.tv_name = Utils.findViewById(target, 2131165425);
  }

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

先來分析一波:
首先自動生成的類實現了 Unbinder接口, 並且實現了 unbind 方法.
有一個有參的構造函數,參數爲 Activity, 在構造函數內對傳入的 Activity 內的控件 ID , 進行 findById 操作. 有一個 Activity 類型的變量 target.
OK, 現在就根據上面的這些代碼, 開始愉快的山寨吧.

先創建如下 Module

  1. 創建 App 名字爲 butterknife-app,
  2. 創建 android Module 名字爲 butterknife. 爲 APP 提供 butterknife 綁定操作.
  3. 創建 java Module 名字爲 butterknife-annotation. 存放我們聲明的註解
  4. 創建 java Module 名字爲 butterknife-compiler. 作爲我們的註解處理器.

工程目錄截圖如下


APP 添加依賴

implementation project(path: ':butterknife-annotations')
implementation project(path: ':butterknife'
annotationProcessor project(path: ':butterknife-compiler')

butterknife-compiler 添加依賴

implementation 'com.squareup:javapoet:1.13.0'
annotationProcessor 'com.google.auto.service:auto-service:1.0-rc7'
implementation 'com.google.auto.service:auto-service:1.0-rc7'
implementation project(path: ':butterknife-annotations')
  • javapoetsquare 推出的開源 java 代碼生成框架, 提供 Java Api 生成 .java 源文件. 這個框架功能非常有用, 我們可以很方便的使用它根據註解, 數據庫模式, 協議格式等來對應生成代碼. 通過這種自動化生成代碼的方式, 可以讓我們用更加簡潔優雅的方式要替代繁瑣冗雜的重複工作
  • auto-serviceGoogle 爲我們提供用於 java.util.ServiceLoader 樣式的服務提供者的配置/元數據生成器. 簡單來說就是會爲了加了 @AutoService 註解的類, 自動裝載和實例化,並完成模塊的注入.

OK, 現在開始擼代碼.

1. 編寫註解

先到 butterknife-annotation module 中新建一個註解. em...既然山寨了, 那就連註解名字也一起山寨吧.

package com.butterknife_annotations;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.CLASS) 
public @interface BindView {
    int value();
}

原版中定義了很多註解, 什麼 BindView, BindFont, BindInt, BindString, BindColor ... 擴展了很多很多, 這裏我們就先寫一個簡單又山寨的 BindView 好了.

2. 編寫接口與工具類

butterknife module 中新建

  1. Unbinder 接口, 聲明一個 unbind 解綁方法. 待會要讓我們自動生成的 java 文件實現這個接口.
  2. Utils 工具類, 實現我們真正的 findViewById.在註解處理器中調用.
  3. ButterKnife 先空着. 最後再寫.
public interface Unbinder {
    @UiThread
    void unbind();

    Unbinder EMPTY = new Unbinder() {
        @Override
        public void unbind() {

        }
    };
}

//在註解處理器重調用
public class Utils {
    public static <T extends View> T findViewById(Activity activity, int id) {
        return activity.findViewById(id);
    }
}

3. 編寫註解處理器

接下來開始到 butterknife-compiler moudle 中寫我們的註解處理器.
新建一個 javaButterKnifeProcessor, 繼承自 AbstractProcessor, 並重寫如下方法

@AutoService(Processor.class)
public class ButterKnifeProcessor extends AbstractProcessor {
    @Override
    public synchronized void init(ProcessingEnvironment processingEnv) {
        super.init(processingEnv);
    }

    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        return false;
    }

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

    @Override
    public Set<String> getSupportedAnnotationTypes() {
        return super.getSupportedAnnotationTypes();
    }
}
  • init 方法主要做一些初始化的事情, 其中參數 processingEnv 會爲我們提供很多有用的工具類
    • 例如等下需要用到的 Filer, 它用來生成 java 類文件.
    • Elements 註解處理器運行掃描源文件時, 以獲取元素 (Element)相關的信息. Element 有以下幾個子類:
      包 (PackageElement), 類 (TypeElement), 成員變量 (VariableElement), 方法 (ExecutableElement)
  • getSupportedSourceVersion方法 返回當前系統支持的 java 版本
  • getSupportedAnnotationTypes 該方法返回一個 Set<String>, 代表 ButterKnifeProcessor 要處理的註解類的名稱集合,即 ButterKnife 支持的註解
  • process 敲黑板, 劃重點, 這個就是最重要的方法. 在這裏完成了目標類信息的收集並生成對應 java

接着開始寫下面幾個簡單的.

//創建文件的時候需要用到
private Filer mFiler;
private Elements mElementUtils;
//打印輸出
private Messager mMessager;

@Override
public synchronized void init(ProcessingEnvironment processingEnv) {
    super.init(processingEnv);
    mFiler = processingEnv.getFiler();
    mElementUtils = processingEnv.getElementUtils();
    mMessager = processingEnv.getMessager();
}

 //指定處理的版本
@Override
public SourceVersion getSupportedSourceVersion() {
    return SourceVersion.latestSupported();
}

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

//添加所有我們需要處理的註解.
public Set<Class<? extends Annotation>> getSupportedAnnotation() {
    Set<Class<? extends Annotation>> annotation = new LinkedHashSet<>();
    annotation.add(BindView.class);
    //原版本中會添加 N 多註解類到這裏
    //例如 annotation.add(BindString.class)
    return annotation;
}

接下來就是最關鍵的 process 方法了.
OK, 現在開始生成.

/**
 * 獲取每個 Activity 內所有加了需要解析的註解的元素
 * @param elements 我們所需要的註解集合, 因爲目前我們就一個註解, 所以這裏長度爲 1.
 * @return key 是包含我們需要解析的註解所屬的 Activity, value 爲當前 Activit 內所有加了要解析的註解的元素.
 */
private  Map<Element, List<Element>> getAllElements(Set<? extends Element> elements){
    Map<Element, List<Element>> elementMap = new LinkedHashMap<>();
    for (Element element : elements) {
        //來自那個 Activity=
        Element enclosingElement = element.getEnclosingElement();
        //以 Activity 爲 Key, 先取一次,看 Map 中是否已存在
        List<Element> viewBindElement = elementMap.get(enclosingElement);
        if (viewBindElement == null) {
            //沒有存在就重新創建
            viewBindElement = new ArrayList<>();
            //存入到 Map 中. key 爲 Activity 名字, value 爲 集合
            elementMap.put(enclosingElement, viewBindElement);
        }
        //存到集合, 同時也會更新 Map 中對應的集合
        viewBindElement.add(element);
    }
    return elementMap;
}

@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
    //獲取所有 Activity 中加了 bindView 註解的 元素, 需要整理爲一個 Activity 對應一個自己內部的元素集合
    Set<? extends Element> elements = roundEnv.getElementsAnnotatedWith(BindView.class);
    Map<Element, List<Element>> elementMap = getAllElements(elements);
    return false;
}

通過 roundEnv.getElementsAnnotatedWith(BindView.class) 可以拿到所有加了 @BindView 註解的控件名字, 來自哪個 Activity. 但是一個 Activity 中可能有很多很多加了註解的控件, 那麼我們需要整理成爲一個 Map, 對每個 Activity 進行歸類. 因爲後面,我們需要爲每個 Activity 都生成一個文件.

那麼接下來就需要開始遍歷這個 Map, 開始爲每個 Activity 都生成一個 java 文件. 有多少個 key 就生成多少個.

for (Map.Entry<Element, List<Element>> entry : elementMap.entrySet()) {
    Element enclosingElement = entry.getKey();
    List<Element> viewBindElements = entry.getValue();
    //獲得類名
    String activityClassNameStr = enclosingElement.getSimpleName().toString();
    mMessager.printMessage(Diagnostic.Kind.NOTE, "------->" + activityClassNameStr);
    //獲取我們要實現的接口.
    ClassName unbinderClassName = ClassName.get("com.butterknife", "Unbinder");
    //獲得對應類名的對象
    ClassName activityClassName = ClassName.bestGuess(activityClassNameStr);
    
    //開始生成類名, 繼承接口對象, 以及字段
    //public final class MainActivity_ViewBinding implements Unbinder, 生成出來的樣子是這樣的
    TypeSpec.Builder classBuilder = buildClass(activityClassNameStr, unbinderClassName, activityClassName);
    
    //生成要實現的 unbinder 方法
    MethodSpec.Builder unbinderMethodBuilder = buildMethod(activityClassName);    
   
    //生成構造函數
    MethodSpec.Builder constructorMethodBuilder = buildConstructor(activityClassName,viewBindElements,unbinderMethodBuilder);

    //將方法添加到類中
    classBuilder.addMethod(unbinderMethodBuilder.build());
    //將構造函數添加到類中
    classBuilder.addMethod(constructorMethodBuilder.build());

  //開始生成類文件
  try {
      String packageName = mElementUtils.getPackageOf(enclosingElement).getQualifiedName().toString();
      mMessager.printMessage(Diagnostic.Kind.NOTE, "------->" + packageName);
      JavaFile.builder(packageName, classBuilder.build())
              .addFileComment("自動生成")
              .build().writeTo(mFiler);
  } catch (IOException e) {
      e.printStackTrace();
  }    
}

過程比較簡單, 就是依次生成類, 方法, 如果需要構造函數的話, 也需要生成. 最後都添加到類的構造器中. 最後生成.
下面是三個生成的方法 buildClass , buildMethod, buildConstructor.

private TypeSpec.Builder buildClass(String activityClassNameStr, ClassName unbinderClassName, ClassName activityClassName) {
   return TypeSpec.classBuilder(activityClassNameStr + "_ViewBinding")
            //生成類的訪問修飾符爲 public final
            .addModifiers(Modifier.FINAL, Modifier.PUBLIC)
            //生成的類實現接口
            .addSuperinterface(unbinderClassName)
            //添加字段
            .addField(activityClassName, "target", Modifier.PRIVATE);
}

private MethodSpec.Builder buildMethod(ClassName activityClassName) {
    //生成類實現類 unbinder 方法
    ClassName callSuper = ClassName.get("androidx.annotation", "CallSuper");
    return MethodSpec.methodBuilder("unbind")
            .addModifiers(Modifier.FINAL, Modifier.PUBLIC)
            .addAnnotation(Override.class)
            .addAnnotation(callSuper)
            .addStatement("$T target = this.target", activityClassName)
            .addStatement("if (target == null) throw new IllegalStateException(\"Bindings already cleared. \");");
}

private MethodSpec.Builder buildConstructor(ClassName activityClassName, List<Element> viewBindElements, MethodSpec.Builder unbinderMethodBuilder) {
    MethodSpec.Builder constructorMethodBuilder = MethodSpec.constructorBuilder()
            .addParameter(activityClassName, "target")
            .addStatement("this.target = target");

    for (Element viewBindElement : viewBindElements) {
        //獲得在 Activity 中聲明的控件名字
        String filedName = viewBindElement.getSimpleName().toString();
        //拿到工具類的對象
        ClassName utilsClassName = ClassName.get("com.butterknife", "Utils");
        //拿到在 Activity 中註解中傳入的參數 ID .
        int resId = viewBindElement.getAnnotation(BindView.class).value();
        constructorMethodBuilder.addStatement("target.$N = $T.findViewById(target, $L)", filedName, utilsClassName, resId);
        //最終生成的結果如下
        //target.tv_name = Utils.findViewByid(mainactivity, R.id.tv_name);
        //在 unbind 方法中,將控件全賦值爲 Null
        unbinderMethodBuilder.addStatement("target.$N = null", filedName);
    }
    return constructorMethodBuilder;
}

最後一步, Activity 在使用的時候需要進行綁定. 需要傳入當前 Activity 對象. 綁定的目的是什麼呢? 就是根據傳入的當前 Activity 然後調用生成文件的構造方法.
OK, 我們繼續到 butterknife module 中的 ButterKnife.java 添加方法 bind(Activity activity)

public class ButterKnife {
    public static Unbinder bind(Activity activity) {
        try {
            //唯一需要的反射, 反射自動生成類的構造函數
            Class<? extends Unbinder> bindClassName = (Class<? extends Unbinder>) Class.forName(activity.getClass().getName() + "_ViewBinding");
            //調用自動生成類的構造函數
            Constructor<? extends Unbinder> bindConstructor = bindClassName.getDeclaredConstructor(activity.getClass());
            Unbinder unbinder = bindConstructor.newInstance(activity);
            return unbinder;
        } catch (Exception e) {
            e.printStackTrace();
        }
        return Unbinder.EMPTY;
    }
}

最後在項目中 Build --> Clean Project --> Make Project, 在我們 APP 工程的目錄下就能看到自動生成的代碼文件了.

迫不及待的來使用一把

public class MainActivity extends AppCompatActivity {
    @BindView(R.id.tv_name)
    TextView mTvName;
    private Unbinder mUnbinder;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        mUnbinder = ButterKnife.bind(this);
        mTvName.setText("666");
    }
    @Override
    protected void onDestroy() {
        super.onDestroy();
        mUnbinder.unbind();
    }
}

我這邊運行成功了, 你們呢?

OK, 到這裏, 是不是對整個流程有個大致的印象了呢.

  1. 我們在我們聲明的控件上添加註釋.
  2. MainActivity 中調用 ButterKnife.bind(this) 傳入當前 Activity.
  3. 編譯的時候自動生成 MainActivity_ViewBinding 文件.
  4. 運行的時候,執行 ButterKnife.bind(this), 在 ButterKnife.bind() 方法中, 反射獲取到自動生成的 MainActivity_ViewBinding 文件實例, 調用自動生成文件的構造方法. 在構造方法內執行 findViewById. 這樣就獲取到啦.

 
好了, 就先山寨到這裏吧. 其中還有一些沒弄的, 比如加註解的控件不能聲明爲 private, 比如我們拿到的 ID 不是 R.id.xxx, 而是一堆數字的樣子. 還有很多很多. 但是也算是基本完成了最核心最基礎的功能. 並且也算是弄清楚了基本流程.

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章