ButterKnife 從入門到精通 - 原始碼級分析(二)

GodZ發表於2017-04-18

ButterKnife 從入門到精通 - 原始碼級分析(二)
上篇文章說到,ButterKnife是如何通過bind()方法來實現繫結試圖、設定監聽的。這一節給大家繼續說說ButterKnife是如何生成ViewBinding的。

APT

類似於ButterKnife這種注入框架以前也有很多的,像xutils、afinal,包括Dagger(Dagger2重構前的版本),但是這些框架都是依賴於執行時註解和反射,嚴重影響效率。後來的框架(ButterKnife、Dagger2等)都是依賴於編譯時註解,被稱為APT(Android Processing Tool)技術。
APT的實現原理是這樣的:定義需要的註解類的@Retention為CLASS,定義自己的Procesor繼承自AbstractProcessor類,那麼在程式碼編譯時,編譯器會自動呼叫Processor類的process方法。

Element

在正式分析之前,先簡單瞭解一下Element類。
Elemet被稱為元素類,它有以下幾個常用的子類。

  • VariableElement //一般代表成員變數
  • ExecutableElement //一般代表類中的方法
  • TypeElement //一般代表代表類
  • PackageElement //一般代表Package

ButterKnifeProcessor

在ButterKnife中,類ButterKnifeProcessor繼承自AbstractProcessor,提供給我們使用的註解的@Retention都為CLASS。
找到ButterKnifeProcessor類,程式碼較多,我們需要關注的只有4個方法

  • init() //初始化需要的資料
  • getSupportedAnnotationTypes() //返回支援的註解型別
  • getSupportedOptions //返回支援的原始碼版本
  • process() //邏輯處理,重點關注

先來看看init()

@Override 
public synchronized void init(ProcessingEnvironment env) {
    super.init(env);
    //獲取sdk版本
    String sdk = env.getOptions().get(OPTION_SDK_INT);
    this.sdk = Integer.parseInt(sdk);
    //操作Element的工具類
    elementUtils = env.getElementUtils();
    //操作TypeMirror的工具類
    typeUtils = env.getTypeUtils();
    //用來建立輔助檔案的
    filer = env.getFiler();
}複製程式碼

註釋已經解釋的很詳細了,init()方法就是用來建立一些輔助類。getSupportedAnnotationTypes() 和 getSupportedOptions中的程式碼也很簡單,這裡就不細看了。

接著就看看它的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);
        javaFile.writeTo(filer);
}
    return false;
}複製程式碼

首先一進這個函式就執行findAndParseTargets(env),來看看都做了那些工作:

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)) {
        parseBindView(element, builderMap, erasedTargetNames);
    }

     ...
    // Associate superclass binders with their subclass binders. This is a queue-based tree walk
    // which starts at the roots (superclasses) and walks to the leafs (subclasses).
    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();

        TypeElement parentType = findParentType(type, erasedTargetNames);
        if (parentType == null) {
            bindingMap.put(type, builder.build());
        } else {
            BindingSet parentBinding = bindingMap.get(parentType);
        if (parentBinding != null) {
            builder.setParent(parentBinding);
            bindingMap.put(type, builder.build());
        } else {
            // Has a superclass binding but we haven't built it yet. Re-enqueue for later.
            entries.addLast(entry);
        }
      }
    }
    return bindingMap;
}複製程式碼

這裡處理各個註解的程式碼較多,不一一貼出了,就以我們的例子中的BindView註解為例,其他的註解也是類似的。
上來先建立了兩個容器,builderMap和erasedTargetNames,然後開始遍歷所有被BindView註解的Element。(BindingSet類後文會有介紹)
在迴圈中,只呼叫了一個方法,parseBindView(element, builderMap, erasedTargetNames),注意這個方法並沒有返回值。

private void parseBindView(Element element,Map<TypeElement,BindingSet.Builder> builderMap,Set<TypeElement> erasedTargetNames){
    //獲取外圍類元素
    TypeElement enclosingElement = (TypeElement) element.getEnclosingElement();
    //獲取檢視的ID
    int id = element.getAnnotation(BindView.class).value();
    //在builderMap中查詢BindingSet.Builder
    BindingSet.Builder builder = builderMap.get(enclosingElement);
    if (builder != null) {
        //查詢此ID是否有對應的資料
        String existingBindingName = builder.findExistingBindingName(getId(id));
        //如果有就返回
        if (existingBindingName != null) {
            return;
        }
    }else{//建立一個BindingSet.Builder,並儲存到builderMap中
        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));

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

先獲取外圍類元素,再獲取需要繫結的View的ID,接著是一些防止重複的判斷,這裡我們是第一次進入,肯定不會重複了。通過getOrCreateBindingBuilder(builderMap,enclosingElement)方法建立一個BindingSet.Builder,並儲存到builderMap中。最後再將一些資訊儲存到builder中去。
回過頭來接著看findAndParseTargets()方法,

Deque<Map.Entry<TypeElement, BindingSet.Builder>> entries = new ArrayDeque<>(builderMap.entrySet());
Map<TypeElement, BindingSet> bindingMap = new LinkedHashMap<>();複製程式碼

將builder的entrySet()儲存到一個佇列中。接著又建立一個Map容器。

//遍歷佇列
while (!entries.isEmpty()) {
    Map.Entry<TypeElement, BindingSet.Builder> entry = entries.removeFirst();
    //
    TypeElement type = entry.getKey();
    BindingSet.Builder builder = entry.getValue();

    TypeElement parentType = findParentType(type, erasedTargetNames);
    if (parentType == null) {
        //呼叫builder.build();
        bindingMap.put(type, builder.build());
    } else {
        BindingSet parentBinding = bindingMap.get(parentType);
        if (parentBinding != null) {
            builder.setParent(parentBinding);
            //呼叫builder.build();
            bindingMap.put(type, builder.build());
        } else {
            // Has a superclass binding but we haven't built it yet. Re-enqueue for later.
            entries.addLast(entry);
        }
    }
}複製程式碼

這一步主要是呼叫了builder.build()方法將builderMap資料儲存到bindingMap中,if-else判斷主要是為了提高健壯性,就不帶大家細看了。最後將bindingMap返回到process()方法中。

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

    JavaFile javaFile = binding.brewJava(sdk);
    javaFile.writeTo(filer);複製程式碼

這一步,遍歷取出相關資訊,並生成檔案(MainActivity_ViewBinding類),這裡使用的是JavaPoet庫去生成Java原始檔的,你當然也可以使用別的方式去生成。

關於BindingSet

有的小夥伴可能會糾結於BindingSet類到底是什麼,其實BindingSet類的作用很簡單,一是通過BindingSet.builder收集資訊,二是封裝JavaPoet的程式碼。這種收集資訊和生成Java原始碼的方式,在ButterKnife不同的版本中也是不一樣的,我們瞭解一下就可以了。

流程圖

通過下面的流程圖,加深理解一下生成MainActivity_ViewBinding的過程

ButterKnife 從入門到精通 - 原始碼級分析(二)
流程圖

ButterKnife執行的整個流程

到這裡,我們就可以很清楚的瞭解到ButterKnife實現的整個流程了。

  1. 在MainActivity中通過註解,標記button和onClick方法
  2. 編譯時執行ButterKnifePrcessor的Process方法,生成MainActivity_ViewBinding類
  3. 執行時,執行Bind()方法完成試圖和監聽的繫結

理清楚了,是不是很簡單。

相關文章