ButterKnife解析

本文主要介紹Android之神JakeWharton的一個注解框架,聽說現在面試官現在面試都會問知不知道JakeWharton,不知道的直接略過。廢話不多說,我們接下來就簡單介紹下ButterKnife的實現,希望對大家有所幫助,還有一點,以下故事純屬虛構,如有雷同,純屬巧合。

目錄

  • ButterKnife的起源和工作流程
  • ButterKnife模塊
  • ButterKnife處理器模塊
  • 總結

ButterKnife的起源和工作流程

起源

1.我們平常一般都是在xml文件中寫好布局,然后在Activity中通過下面代碼查找和處理控件

public class MainActivity extends Activity implements View.OnClickListener {
    private TextView mTextview;
    private Button mButton;
    private ImageView mImageview;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        initView();
    }

    private void initView() {
        mTextview = (TextView) findViewById(R.id.textview);
        mButton = (Button) findViewById(R.id.button);
        mImageview = (ImageView) findViewById(R.id.imageview);
        mButton.setOnClickListener(this);
    }

    @Override
    public void onClick(View v) {
        switch (v.getId()) {
            case R.id.button:
                Toast.makeText(this, "羅馬不是一天建成的", Toast.LENGTH_SHORT).show();
                break;
        }
    }
}

2.顯然這些findViewById和setOnClickListener很是礙眼,于是有人想是不是可以把findViewById和setOnClickListener這些操作放外邊兒呢,這樣在MainActivity里邊只需要聲明變量和回調方法,我們就可以只關注我們需要關注的,于是乎就有了如下代碼

public class MainActivity extends Activity {
    TextView mTextview;
    Button mButton;
    ImageView mImageview;
    private TestMainActivity_ViewBinding mBind;

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

    public void onClick() {
        Toast.makeText(this, "羅馬不是一天建成的", Toast.LENGTH_SHORT).show();
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        mBind.unbind();
    }
}
public class TestButterKnife {
    public static TestMainActivity_ViewBinding bind(Activity target) {
        return new TestMainActivity_ViewBinding((MainActivity) target);
    }
}
public class TestMainActivity_ViewBinding {
    private MainActivity target;

    public TestMainActivity_ViewBinding(MainActivity target) {
        this(target, target.getWindow().getDecorView());
    }

    public TestMainActivity_ViewBinding(final MainActivity target, View source) {
        this.target = target;
        target.mTextview = (TextView) source.findViewById(R.id.textview);
        target.mImageview = (ImageView) source.findViewById(R.id.imageview);
        target.mButton = (Button) source.findViewById(R.id.button);
        target.mButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                target.onClick();
            }
        });
    }

    public void unbind() {
        MainActivity target = this.target;
        if (target == null) throw new IllegalStateException("Bindings already cleared.");
        this.target = null;
        target.mTextview = null;
        target.mImageview = null;
        target.mButton.setOnClickListener(null);
        target.mButton = null;
    }
}

3.看到上面的代碼一定有很多人都想吐,我也想說me too,這樣豈不是要碼很多代碼,還要建好多類,有點得不償失的趕腳。這時候Android之神JakeWharton站出來了,發表了一席講話,大體內容是說可以寫個小框架,讓用戶在MainActivity里邊只需要寫點注解,其它的事情交給框架來做,也不需要建那么類,碼很多代碼,于是就有了如下設計

public class MainActivity extends Activity {
    @BindView(R.id.textview)
    TextView mTextview;
    @BindView(R.id.button)
    Button mButton;
    @BindView(R.id.imageview)
    ImageView mImageview;
    private Unbinder mBind;

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

    @OnClick(R.id.button)
    public void onClick() {
        Toast.makeText(this, "羅馬不是一天建成的", Toast.LENGTH_SHORT).show();
    }

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

工作流程

看完上面的設計,肯定有人就會想,這樣的話需要使用運行時注解實現吧,在ButterKnife的bind(this)方法執行的時候通過反射獲取MainActivity中所有的帶有注解的屬性并且獲得注解中的R.id.xxx值,最后還是通過反射調用Activity.findViewById()方法來獲取View,并賦值給Activity中的某個屬性。可是這樣做不會有問題嗎,我們都知道在Activity運行時大量使用反射會影響App的運行性能,造成卡頓等一系列問題。其實這種問題JakeWharton早就想到了,于是乎選用了編譯時注解來實現,也就是Java Annotation Processing技術。下面開始簡單介紹下ButterKnife的工作流程,該庫主要使用編譯時注解和javapoet在編譯時生成輔助類,在運行時通過反射調用輔助類的構造和方法來實現。

大體流程.png

1.編譯時ButterKnifeProcessor類的process()方法處理過程如下

  • 獲取所有的標記了@BindView、@OnClick等注解的元素
  • 定義一個key為類級別的元素,value為注解信息的Map集合,根據獲取的元素可以拿到當前類級別的元素和注解標記的信息,然后put進去,這樣Map里邊就保存了所有要生成類和類中的信息
  • 根據Map集合通過 javapoet 生成類似MainActivity_ViewBinding這樣的很多類,包括包名,導包,類名,字段,構造,方法等信息都會生成,可見這個庫的強大,這種庫最難的應該就是導包了吧

2.運行時

  • ButterKnife.bind(this);這個方法會反射創建并返回MainActivity_ViewBinding這樣的實例,MainActivity_ViewBinding的構造就會執行,構造中會進行findViewById和setOnClickListener這些操作,這與起源中第2個模塊里邊的TestMainActivity_ViewBinding代碼很相似
  • 當然通過ButterKnife.bind(this);的返回值,你可以在onDestroy()中調用MainActivity_ViewBinding的unbind()方法來解除綁定釋放資源

ButterKnife模塊

這個模塊主要有三個東東,一個是ButterKnife類,一個Utils類,一個是Unbinder接口。ButterKnife主要是用于運行時反射調用輔助類和返回輔助類實例,Utils類會被生成輔助類調用,Unbinder是生成的輔助類需要實現的接口,下面依次來介紹

ButterKnife類

1.mBind = ButterKnife.bind(this),會走如下代碼

  //UiThread表明這個方法必須在UI線程調用
  @NonNull @UiThread
  public static Unbinder bind(@NonNull Activity target) {
    View sourceView = target.getWindow().getDecorView();
    return createBinding(target, sourceView);
  }

2.接著調用createBinding(target, sourceView)

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;
    }
    //省去了try-catch,這里創建輔助類的實例并返回,注意這里會調用輔助類兩個參數的構造方法
    return constructor.newInstance(target, source);
}

3.下面我們看findBindingConstructorForClass(targetClass),這里采用了享元模式的設計,這種設計在很對框架中都會有,這里維護的是一個Map集合,集合中有這個key對應的value直接返回,沒有才會構建,構建完后會放入Map中,這樣大大減少了內存中對象的數量

  //BINDINGS是一個key為Class,value為構造器的Map
  static final Map<Class<?>, Constructor<? extends Unbinder>> BINDINGS = new LinkedHashMap<>();
  private static Constructor<? extends Unbinder> findBindingConstructorForClass(Class<?> cls) {
     //集合中通過key來獲取構造器
    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();
    //類名不能以android.和java.開頭
    if (clsName.startsWith("android.") || clsName.startsWith("java.")) {
      if (debug) Log.d(TAG, "MISS: Reached framework class. Abandoning search.");
      return null;
    }
    try {
      //可以看到生成的輔助類的類名是原始類名加上_ViewBinding,比如MainActivity_ViewBinding
      Class<?> bindingClass = cls.getClassLoader().loadClass(clsName + "_ViewBinding");
      //創建構造器
      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);
    }
    //創建完后放入Map集合中
    BINDINGS.put(cls, bindingCtor);
    //返回構造器
    return bindingCtor;
  }
Utils類

這里簡單介紹幾個方法,后面生成的輔助類會調用這個類中方法
1.findRequiredViewAsType()

  public static <T> T findRequiredViewAsType(View source, @IdRes int id,String who,Class<T> cls) {
    //這里會調用下面2中的方法拿到一個沒有強轉的View
    View view = findRequiredView(source, id, who);
    //會調用3中的方法根據返回一個強制后的T,T表示@BindView所修飾控件類型,比如TextView、Button等
    return castView(view, id, who, cls);
  }

2.findRequiredView(View source, @IdRes int id, String who)

  public static View findRequiredView(View source, @IdRes int id, String who) {
    //我們期望看到的findViewById在這里
    View view = source.findViewById(id);
    if (view != null) {
      return view;
    }
    //省略異常處理
  }

3.castView(View view, @IdRes int id, String who, Class<T> cls)

  public static <T> T castView(View view, @IdRes int id, String who, Class<T> cls) {
      //省略try-catch,這里調用Class類的cast方法進行強轉,我們稍微看下這個方法
      return cls.cast(view);
  }
    //Class類中的cast方法
    public T cast(Object obj) {
        if (obj != null && !isInstance(obj))
            throw new ClassCastException(cannotCastMsg(obj));
        //這里強轉了下然后返回
        return (T) obj;
    }
Unbinder接口

這個接口中就一個方法和一個空實現的成員變量

public interface Unbinder {
  @UiThread void unbind();
  //這個成員變量會在構造器為null的異常情況下直接返回,可以在上面ButterKnife類中的createBinding方法中看到
  Unbinder EMPTY = new Unbinder() {
    @Override public void unbind() {}
  };
}

ButterKnife處理器模塊

這個模塊中我們只關注ButterKnifeProcessor這個類,該類繼承了AbstractProcessor抽象類,下面我們看它重寫的幾個方法

1.首先我們看@AutoService注解,這是一個其他注解處理器中引入的注解。它是Google開發的,用來生成META-INF/services/javax.annotation.processing.Processor文件的,我們直接用即可

@AutoService(Processor.class)
public final class ButterKnifeProcessor extends AbstractProcessor {
      //省略
}

2.接著我們看init(ProcessingEnvironment env),注解處理器工具APT會調用這個方法并傳入ProcessingEnvironment ,我理解的是該方法是提供給我們用來訪問APT的環境

  private int sdk = 1;
  private static final String OPTION_SDK_INT = "butterknife.minSdk";
  private static final String OPTION_DEBUGGABLE = "butterknife.debuggable";
  private boolean debuggable = true;
  private Elements elementUtils;
  private Types typeUtils;
  private Filer filer;
  private Trees trees;
  @Override
  public synchronized void init(ProcessingEnvironment env) {
    super.init(env);
    //env.getOptions()拿到的是傳遞給注釋處理工具的Map選項
    String sdk = env.getOptions().get(OPTION_SDK_INT);
    if (sdk != null) {
        //省去try-catch
        this.sdk = Integer.parseInt(sdk);
    }
    debuggable = !"false".equals(env.getOptions().get(OPTION_DEBUGGABLE));
    //獲取元素實用工具
    elementUtils = env.getElementUtils();
    //獲取類型實用工具
    typeUtils = env.getTypeUtils();
    //獲取用來創建文件的Filter
    filer = env.getFiler();
    try {
      trees = Trees.instance(processingEnv);
    } catch (IllegalArgumentException ignored) {
    }
  }

3.我們看getSupportedSourceVersion(),這個方法用來指定你使用的Java版本

  @Override 
  public SourceVersion getSupportedSourceVersion() {
    //通常這樣返回就行了,它會根據你的環境返回對應的版本枚舉
    return SourceVersion.latestSupported();
  }

4.接下來就是getSupportedAnnotationTypes()方法,指定支持的注解

  @Override 
  public Set<String> getSupportedAnnotationTypes() {
    Set<String> types = new LinkedHashSet<>();
    for (Class<? extends Annotation> annotation : getSupportedAnnotations()) {
      //annotation.getCanonicalName()為獲取全類名,即包名+類名
      types.add(annotation.getCanonicalName());
    }
    return types;
  }
  private Set<Class<? extends Annotation>> getSupportedAnnotations() {
    Set<Class<? extends Annotation>> annotations = new LinkedHashSet<>();
    annotations.add(BindAnim.class);
    annotations.add(BindArray.class);
    annotations.add(BindBitmap.class);
    annotations.add(BindBool.class);
    annotations.add(BindColor.class);
    annotations.add(BindDimen.class);
    annotations.add(BindDrawable.class);
    annotations.add(BindFloat.class);
    annotations.add(BindFont.class);
    annotations.add(BindInt.class);
    annotations.add(BindString.class);
    annotations.add(BindView.class);
    annotations.add(BindViews.class);
    annotations.addAll(LISTENERS);
    return annotations;
  }

5.最后來詳細介紹下process(),這個方法相當于每個處理器的main()函數,在這里可以寫你的掃描、處理注解以及生成Java文件的代碼,因此這個方法是整個處理器模塊的關鍵

  @Override 
  public boolean process(Set<? extends TypeElement> elements, RoundEnvironment env) {
    //根據env獲取一個Map集合,其中key為類級別的元素,value為BindingSet
    //BindingSet是一個自定義類,其中含有當前類中所有的注解信息
    Map<TypeElement, BindingSet> bindingMap = findAndParseTargets(env);
    for (Map.Entry<TypeElement, BindingSet> entry : bindingMap.entrySet()) {
      TypeElement typeElement = entry.getKey();
      BindingSet binding = entry.getValue();
      //注意下面的兩行代碼就是生成輔助類啦,由于需要用到JavaPoet庫,后面會詳細講解
      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;
  }
首先我們來看findAndParseTargets(env)是如何構建Map集合的
  private Map<TypeElement, BindingSet> findAndParseTargets(RoundEnvironment env) {
    //key為類級別的元素,value為BindingSet的內部類Builder,看到Builder我們應該都很敏感,這是典型的
    //建造者模式,這種模式特別常見,不管是在第三方庫OkHttp中,還是在Android提供的對話框API中都可以看見
    //它的身影,對于設計模式我們這里不再詳細展開
    Map<TypeElement, BindingSet.Builder> builderMap = new LinkedHashMap<>();
    //Set集合中保存了所有類級別的元素
    Set<TypeElement> erasedTargetNames = new LinkedHashSet<>();
    scanForRClasses(env);
    //由于定義的注解很多,這里省略了其他注解,只關注@BindView
    for (Element element : env.getElementsAnnotatedWith(BindView.class)) {
      try {
        //解析@BindView注解到buildMap中
        parseBindView(element, builderMap, erasedTargetNames);
      } catch (Exception e) {
        logParsingError(element, BindView.class, e);
      }
    }
    //builderMap是key為類級別的元素,value為BindingSet的內部類Builder,這里會轉為bindingMap,即
    //把value通過builder()方法構建成BindingSet,這就是典型的建造者模式
    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();
      //注意這是在while循環里邊,這個方法中會根據當前類級別的元素查找父元素,如果沒有就返回null,如果
      //有父元素就返回父元素,從這里可以看出來我們的注解是支持繼承的
      TypeElement parentType = findParentType(type, erasedTargetNames);
      if (parentType == null) {
        //父元素為null,直接put當前元素
        bindingMap.put(type, builder.build());
      } else {
        //從bindingMap中先獲取父元素的BindingSet
        BindingSet parentBinding = bindingMap.get(parentType);
        if (parentBinding != null) {
          //父元素的BindingSet不為null,就需要為當前元素設置它的父元素
          builder.setParent(parentBinding);
          //然后put到bindingMap中
          bindingMap.put(type, builder.build());
        } else {
          //父元素的BindingSet是null,就先把當前的Entry放到隊尾,繼續下一次循環
          entries.addLast(entry);
        }
      }
    }
    //返回key為類級別的元素,value為BindingSet的集合
    return bindingMap;
  }
  • 接下來我們看上面方法中的parseBindView()和findParentType(),我們先看findParentType()是如何根據當前類級別的元素查找父元素
  private TypeElement findParentType(TypeElement typeElement, Set<TypeElement> parents) {
    TypeMirror type;
    while (true) {
      //根據當前類級別的元素查找父元素
      type = typeElement.getSuperclass();
      if (type.getKind() == TypeKind.NONE) {
        //父元素為null,直接返回null
        return null;
      }
      //parents集合中保存的是包含有我們自定義注解的類級別的元素,找到
      //父元素并且父元素是個有效的即在集合里,就返回父元素
      typeElement = (TypeElement) ((DeclaredType) type).asElement();
      if (parents.contains(typeElement)) {
        return typeElement;
      }
    }
  }
  • 再先看parseBindView()是如何解析@BindView注解到buildMap中的
  private void parseBindView(Element element, Map<TypeElement, BindingSet.Builder> builderMap,
      Set<TypeElement> erasedTargetNames) {
    //獲取當前元素的類級別的元素
    TypeElement enclosingElement = (TypeElement) element.getEnclosingElement();
    //生成的代碼是不是可以訪問,由于我們是通過target.mTextview來訪問的,所以這個方法會校驗@BindView修飾的
    //字段是不是private、static的,@BindView是不是在類里邊,如果在類里邊那么類是不是private的
    boolean hasError = isInaccessibleViaGeneratedCode(BindView.class, "fields", element)
        || isBindingInWrongPackage(BindView.class, element);
    //下面幾行代碼主要是校驗@BindView修飾的字段類型,必須是一個View或者是一個接口,否則就毫無意義
    //獲取@BindView修飾的字段類型,比如TextView類型
    TypeMirror elementType = element.asType();
    if (elementType.getKind() == TypeKind.TYPEVAR) {
      TypeVariable typeVariable = (TypeVariable) elementType;
      elementType = typeVariable.getUpperBound();
    }
    Name qualifiedName = enclosingElement.getQualifiedName();
    Name simpleName = element.getSimpleName();
    //校驗修飾的類型是不是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的值,即R.id.xxx
    int id = element.getAnnotation(BindView.class).value();
    //享元模式的設計,根據當前元素的類級別的元素先從Map中獲取BindingSet的內部類Builder
    BindingSet.Builder builder = builderMap.get(enclosingElement);
    //把int類型的id包裝成了QualifiedId類
    QualifiedId qualifiedId = elementToQualifiedId(element, id);
    if (builder != null) {
       //getId(qualifiedId)把QualifiedId類中的id又封裝成了Id類,校驗Id類是否已經存在
      String existingBindingName = builder.findExistingBindingName(getId(qualifiedId));
      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 {
      //builder為空,說明當前類還沒有對應的value,需要new一個出來,并放到builderMap中
      builder = getOrCreateBindingBuilder(builderMap, enclosingElement);
    }
    //@BindView修飾的字段的簡單名稱,即變量名mTextView
    String name = simpleName.toString();
    //@BindView修飾的字段的類型,比如TextView
    TypeName type = TypeName.get(elementType);
    //是否必須是字段
    boolean required = isFieldRequired(element);
    //調用BindingSet的內部類Builder中的addField方法封裝解析的信息
    builder.addField(getId(qualifiedId), new FieldViewBinding(name, type, required));
    //給所有含有自定義注解的類組成的Set集合中添加元素
    erasedTargetNames.add(enclosingElement);
  }
  private BindingSet.Builder getOrCreateBindingBuilder(
      Map<TypeElement, BindingSet.Builder> builderMap, TypeElement enclosingElement) {
    BindingSet.Builder builder = builderMap.get(enclosingElement);
    if (builder == null) {
      //builder為null,就構建一個BindingSet.Builder,放入集合中,并返回
      builder = BindingSet.newBuilder(enclosingElement);
      builderMap.put(enclosingElement, builder);
    }
    return builder;
  }
  static Builder newBuilder(TypeElement enclosingElement) {
    TypeMirror typeMirror = enclosingElement.asType();
    //類級別的元素是不是View的子類
    boolean isView = isSubtypeOfType(typeMirror, VIEW_TYPE);
    //類級別的元素是不是Activity的子類
    boolean isActivity = isSubtypeOfType(typeMirror, ACTIVITY_TYPE);
    //類級別的元素是不是Dialog的子類
    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();
     //substring截取后一般是MainActivity這樣的字符串,然后replace的話會沒有效果,如果有.則用$代替
    String className = enclosingElement.getQualifiedName().toString().substring(
        packageName.length() + 1).replace('.', '$');
    //這里可以看到要生成的包名,要生成的類名,格式如MainActivity_ViewBinding
    ClassName bindingClassName = ClassName.get(packageName, className + "_ViewBinding");
    //當前類是不是被final修飾
    boolean isFinal = enclosingElement.getModifiers().contains(Modifier.FINAL);
    //直接通過構造new一個Builder,第一個參數表示是不是一個class,第二個參數中含有要生成的類名和包名,其它都是布爾類型的參數
    return new Builder(targetType, bindingClassName, isFinal, isView, isActivity, isDialog);
  }

總結下,在parseBindView()方法中解析出來的信息都放在BindingSet的內部類Builder中,包含的信息有包名、原始類名、要生成的類名、R.id.xxx、是不是字段、變量名mTextView、變量類型TextView等信息。而BindingSet的內部類Builder又會通過調用build()方法構建一個BindSet實例并把這些信息傳遞過去,這就是典型的建造者模式,在BindSet中有了這些信息,下面我們就可以生成輔助類啦。

最后我們看binding.brewJava(sdk, debuggable)是如何生成輔助類的

1.回到最初的process()方法,我們講完了如何構建Map集合,接著我們講解如何生成輔助類

  @Override 
  public boolean process(Set<? extends TypeElement> elements, RoundEnvironment env) {
    //根據env獲取一個Map集合,其中key為類級別的元素,value為BindingSet
    //BindingSet是一個自定義類,其中含有當前類中所有的注解信息
    Map<TypeElement, BindingSet> bindingMap = findAndParseTargets(env);
    for (Map.Entry<TypeElement, BindingSet> entry : bindingMap.entrySet()) {
      TypeElement typeElement = entry.getKey();
      BindingSet binding = entry.getValue();
      //注意下面的兩行代碼就是生成輔助類啦,由于需要用到JavaPoet庫,后面會詳細講解
      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;
  }

2.需要用到javapoet,因此打開這個庫我們稍微看下它是如何使用的

  • 比如我們想要生成一個普通的java類,其中有main方法,方法中有syso
  package com.example.helloworld;
  public final class HelloWorld {
    public static void main(String[] args) {
      System.out.println("Hello, JavaPoet!");
    }
  }
  • 那我們就可以調用下面的方法來實現,可以看到又是建造者builder模式,真是無處不見啊,呼哈
  //方法級別的,定義方法名
  MethodSpec main = MethodSpec.methodBuilder("main")
       //定義修飾符
      .addModifiers(Modifier.PUBLIC, Modifier.STATIC)
       //定義返回值
      .returns(void.class)
       //定義方法參數
      .addParameter(String[].class, "args")
       //定義方法中的syso
      .addStatement("$T.out.println($S)", System.class, "Hello, JavaPoet!")
      .build();

  //類級別的,定義類名
  TypeSpec helloWorld = TypeSpec.classBuilder("HelloWorld")
      //添加修飾符
      .addModifiers(Modifier.PUBLIC, Modifier.FINAL)
      //把上面定義的main添加到類中
      .addMethod(main)
      .build();

   //JavaFile中保存了所有的信息,包名和上面定義的類信息,方法信息
  JavaFile javaFile = JavaFile.builder("com.example.helloworld", helloWorld)
      .build();

  javaFile.writeTo(System.out);

3.簡單看了javapoet的使用,繼續看我們的代碼,而我們知道生成輔助類的所有信息都在JavaFile中,我們看JavaFile是如何生成的,也就是上面的brewJava()

  JavaFile javaFile = binding.brewJava(sdk, debuggable);
  JavaFile brewJava(int sdk, boolean debuggable) {
    //bindingClassName是ClassName類型,它是對包名和類名的封裝,這里可以看到獲取包名,類和類里邊的信息是
    //通過createType()方法獲取的,也就是TypeSpec
    return JavaFile.builder(bindingClassName.packageName(), createType(sdk, debuggable))
        .addFileComment("Generated code from Butter Knife. Do not modify!")
        .build();
  }
  private TypeSpec createType(int sdk, boolean debuggable) {
    //類級別的,定義類名,bindingClassName是ClassName類型,它是對包名和類名的封裝
    //上面獲取的是包名,這里獲取的是輔助類的類名
    TypeSpec.Builder result = TypeSpec.classBuilder(bindingClassName.simpleName())
        .addModifiers(PUBLIC);
    if (isFinal) {
      result.addModifiers(FINAL);
    }
    //判斷有沒有父類,即生成的輔助類是不是也要從父類繼承
    if (parentBinding != null) {
      //有父類,那么生成的輔助類也要從父類繼承
      result.superclass(parentBinding.bindingClassName);
    } else {
      //沒有父類,就讓輔助類實現Unbinder
      result.addSuperinterface(UNBINDER);
    }
    //設置類似這樣的字段private MainActivity target;
    if (hasTargetField()) {
      result.addField(targetTypeName, "target", PRIVATE);
    }
    //判斷是不是View、Activity、Dialog的子類,來生成不同構造
    if (isView) {
      result.addMethod(createBindingConstructorForView());
    } else if (isActivity) {
      //這里我們只關注這個構造的生成,這個方法用來生成一個參數MainActivity target的構造
      result.addMethod(createBindingConstructorForActivity());
    } else if (isDialog) {
      result.addMethod(createBindingConstructorForDialog());
    }
    if (!constructorNeedsView()) {
      result.addMethod(createBindingViewDelegateConstructor());
    }
    //前面是只有一個參數MainActivity target構造
    //這里是兩個參數final MainActivity target, View source的構造
    result.addMethod(createBindingConstructor(sdk, debuggable));
    if (hasViewBindings() || parentBinding == null) {
      //沒有父類,那么我們前面實現了Unbinder接口,需要實現它里面的unbind方法
      result.addMethod(createBindingUnbindMethod(result));
    }
    return result.build();
  }

4.上面方法中調用的三個方法我們還沒看,其中有一個參數的構造、兩個參數的構造、unbind()方法,接下來我們挨著看

  • createBindingConstructorForActivity()生成一個參數MainActivity target的構造
  private MethodSpec createBindingConstructorForActivity() {
    MethodSpec.Builder builder = MethodSpec.constructorBuilder()
         //添加@UiThread注解
        .addAnnotation(UI_THREAD)
        .addModifiers(PUBLIC)
        //添加構造中的參數,如MainActivity target
        .addParameter(targetTypeName, "target");
    if (constructorNeedsView()) {
      builder.addStatement("this(target, target.getWindow().getDecorView())");
    } else {
      //這里是一個參數的構造不需要View,構造中添加this(target, target)
      builder.addStatement("this(target, target)");
    }
    return builder.build();
  }
  • createBindingConstructor(sdk, debuggable)生成兩個參數final MainActivity target, View source的構造
private MethodSpec createBindingConstructor(int sdk, boolean debuggable) {
    MethodSpec.Builder constructor = MethodSpec.constructorBuilder()
         //添加@UiThread注解
        .addAnnotation(UI_THREAD)
        .addModifiers(PUBLIC);
    //判斷是否有方法綁定,即是否有onClick()這樣的回調方法
    if (hasMethodBindings()) {
      //由于是在匿名內部類方法里邊調用構造函數的第一個參數,需要加上final修飾
      constructor.addParameter(targetTypeName, "target", FINAL);
    } else {
      //沒有方法綁定,只需要添加構造函數中的第一個參數MainActivity target
      constructor.addParameter(targetTypeName, "target");
    }
    
    if (constructorNeedsView()) {
      //構造函數的第二個參數就是View source
      constructor.addParameter(VIEW, "source");
    } else {
      constructor.addParameter(CONTEXT, "context");
    }
    //有一些警告的話添加java提供的@SuppressWarnings注解
    if (hasUnqualifiedResourceBindings()) {
      constructor.addAnnotation(AnnotationSpec.builder(SuppressWarnings.class)
          .addMember("value", "$S", "ResourceType")
          .build());
    }
    if (hasOnTouchMethodBindings()) {
      constructor.addAnnotation(AnnotationSpec.builder(SUPPRESS_LINT)
          .addMember("value", "$S", "ClickableViewAccessibility")
          .build());
    }
    //有父類的情況下,需要根據情況添加如下代碼
    if (parentBinding != null) {
      if (parentBinding.constructorNeedsView()) {
        constructor.addStatement("super(target, source)");
      } else if (constructorNeedsView()) {
        constructor.addStatement("super(target, source.getContext())");
      } else {
        constructor.addStatement("super(target, context)");
      }
      constructor.addCode("\n");
    }
    if (hasTargetField()) {
      //會執行這里,添加this.target = target,并且換行
      constructor.addStatement("this.target = target");
      constructor.addCode("\n");
    }
    //判斷是不是有View綁定,肯定有@BindView
    if (hasViewBindings()) {
      if (hasViewLocal()) {
        //在構造函數中定義一個局部變量View view,用來view.setOnClickListener()
        constructor.addStatement("$T view", VIEW);
      }
      for (ViewBinding binding : viewBindings) {
        //這是在for循環里面,這個方法會間接調用findViewById和強轉的操作,接下來主要看這個方法
        addViewBinding(constructor, binding, debuggable);
      }
      for (FieldCollectionViewBinding binding : collectionBindings) {
        constructor.addStatement("$L", binding.render(debuggable));
      }
      //資源綁定不為空,才會換行
      if (!resourceBindings.isEmpty()) {
        constructor.addCode("\n");
      }
    }
    //資源的綁定不為空才會執行
    if (!resourceBindings.isEmpty()) {
      if (constructorNeedsView()) {
        constructor.addStatement("$T context = source.getContext()", CONTEXT);
      }
      if (hasResourceBindingsNeedingResource(sdk)) {
        constructor.addStatement("$T res = context.getResources()", RESOURCES);
      }
      for (ResourceBinding binding : resourceBindings) {
        constructor.addStatement("$L", binding.render(sdk));
      }
    }
    return constructor.build();
  }
private void addViewBinding(MethodSpec.Builder result, ViewBinding binding, boolean debuggable) {
    if (binding.isSingleFieldBinding()) {
      FieldViewBinding fieldBinding = binding.getFieldBinding();
      //先寫target.mTextview = ,這里可以看到是直接通過.調用的,因此我們寫的字段不能是private的
      CodeBlock.Builder builder = CodeBlock.builder()
          .add("target.$L = ", fieldBinding.getName());
      boolean requiresCast = requiresCast(fieldBinding.getType());
      if (!debuggable || (!requiresCast && !fieldBinding.isRequired())) {
        builder.add("source.findViewById($L)", binding.getId().code);
      } else {
        //下面的代碼會組合成一行代碼,Utils中的findRequiredViewAsType()會進行find和強轉操作,上面介紹過了
        //Utils.findRequiredViewAsType(source, R.id.textview, "field 'mTextview'", TextView.class);
        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;
    }
  • createBindingUnbindMethod(result)生成unbind()方法,即實現Unbinder接口中的unbind()方法
private MethodSpec createBindingUnbindMethod(TypeSpec.Builder bindingClass) {
    //定義unbind()方法,并且添加@Override注解
    MethodSpec.Builder result = MethodSpec.methodBuilder("unbind")
        .addAnnotation(Override.class)
        .addModifiers(PUBLIC);
    if (!isFinal && parentBinding == null) {
      //@CallSuper是android提供的注解,表示調用父類
      result.addAnnotation(CALL_SUPER);
    }
    if (hasTargetField()) {
      if (hasFieldBindings()) {
        //添加MainActivity target = this.target
        result.addStatement("$T target = this.target", targetTypeName);
      }
      //添加if (target == null) throw new IllegalStateException("Bindings already cleared.")
      result.addStatement("if (target == null) throw new $T($S)", IllegalStateException.class,
          "Bindings already cleared.");
      //添加this.target = null
      result.addStatement("$N = null", hasFieldBindings() ? "this.target" : "target");
      //換行
      result.addCode("\n");
      for (ViewBinding binding : viewBindings) {
        if (binding.getFieldBinding() != null) {
          //循環添加類似這樣的代碼來解除綁定,target.mTextview = null;
          result.addStatement("target.$L = null", binding.getFieldBinding().getName());
        }
      }
      for (FieldCollectionViewBinding binding : collectionBindings) {
        result.addStatement("target.$L = null", binding.name);
      }
    }
     //有綁定方法,如設置監聽事件回調方法才會走這個if
    if (hasMethodBindings()) {
      result.addCode("\n");
      for (ViewBinding binding : viewBindings) {
        addFieldAndUnbindStatement(bindingClass, result, binding);
      }
    }
    if (parentBinding != null) {
      //有父類才會換行,并且添加super.unbind()
      result.addCode("\n");
      result.addStatement("super.unbind()");
    }
    return result.build();
  }

總結下,至此生成輔助類的代碼就簡單介紹完了,我們只是簡單介紹了下@BindView,其它的注解都沒介紹,有興趣的讀者可以自己去扒Butterknife源碼,我們可以在\app\build\generated\source\apt\debug\目錄下找到編譯時生成的輔助類,下面我們列出一個簡單的

package com.zeit.butter;

import android.support.annotation.CallSuper;
import android.support.annotation.UiThread;
import android.view.View;
import android.widget.Button;
import android.widget.ImageView;
import android.widget.TextView;

import butterknife.Unbinder;
import butterknife.internal.DebouncingOnClickListener;
import butterknife.internal.Utils;

public class MainActivity_ViewBinding implements Unbinder {
  private MainActivity target;

  private View view2131427415;

  @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.mTextview = Utils.findRequiredViewAsType(source, R.id.textview, "field 'mTextview'", TextView.class);
    view = Utils.findRequiredView(source, R.id.button, "field 'mButton' and method 'onClick'");
    target.mButton = Utils.castView(view, R.id.button, "field 'mButton'", Button.class);
    view2131427415 = view;
    view.setOnClickListener(new DebouncingOnClickListener() {
      @Override
      public void doClick(View p0) {
        target.onClick();
      }
    });
    target.mImageview = Utils.findRequiredViewAsType(source, R.id.imageview, "field 'mImageview'", ImageView.class);
  }

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

    target.mTextview = null;
    target.mButton = null;
    target.mImageview = null;

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

總結

JakeWharton的Butterknife,俗稱"黃油刀",可以幫助我們提高開發效率,尤其是配合AS插件的使用,下面推薦一個插件Android-Butterknife-Zelezny。而閱讀源碼可以讓我們知其然知其所以然,也可以學到很多設計模式。建議閱讀第三方庫或Android提供的API等源碼之前,先從設計模式學起,推薦《Android源碼設計模式解析與實戰》。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容