android注解Butterknife的使用及代碼分析

Paste_Image.png

大家好,今天老衲給大家帶來的是Android另一款注解框架,ButterKnife的使用介紹及代碼分析。

使用方式:

  1. 導入Butterknife的jar包。
    不需要修改配置文件有木有,超級簡單有木有,→_→
  2. 添加AndroidStudio插件(可選,需要依賴ButterKnife的jar包)
    下載一個插件Android ButterKnife Zelezny來配合Butterknife自動生成View。


    JfQ73eI.gif

注意,需要綁定的View或者資源的聲明必須是public,不能是private或者static,至于原因,我們會在下面的分析中講到

Butterknife常用的注解:
Butterknife支持Activity,Fragment,View,Dialog,ViewHolder類內部的View綁定


@Bind
TextView mTextView//最常用的注解,用來綁定View,避免findViewById,也可以用在ViewHolder里,必須是public

@Bind({ R.id.first_name, R.id.middle_name, R.id.last_name })
List<EditText> nameViews//綁定多個view,只能用List不能用ArrayList

@OnClick(R.id.submit)
public void submit(View view) {...}//綁定點擊事件,支持多個id綁定同一個方法

@OnItemSelected(R.id.list_view)
void onItemSelected(int position) {...}//selected事件

@OnItemClick(R.id.example_list) 
void onItemClick(int position) {...}//itemClick事件

@OnFocusChange(R.id.example) 
void onFocusChanged(boolean focused){...}//焦點改變監聽

@OnItemLongClick(R.id.example_list) 
boolean onItemLongClick(int position){...}//長按監聽

@OnPageChange(R.id.example_pager) 
void onPageSelected(int position){...}//Viewpager切換監聽

@OnTextChanged(R.id.example) 
void onTextChanged(CharSequence text)//內容改變監聽

@BindInt//用來綁定Integer類型的resource ID
@BindString//用來綁定string.xml里的字符串
@BindDrawable//用來綁定圖片
@BindColor//用來綁定顏色
@BindDimen//用來綁定dimens

ButterKnife所提供的注解的著重點放在了View的處理上,減少了開發時View處理的時間,相對于AndroidAnnotation來說,功能較為的單一。

Butterknife的實現流程

概述:Butterknife在編譯時刻利用APT分析程序代碼,掃描每一個有注解的類,找出類中帶有注解的字段
@Bind生成ViewBinding的子類,
監聽類的生成ListenerBinding的子類,
通過Java的FilerAPI生成多個包含注入代碼的輔助類,程序中調用ButterKnife.bind()方法時加載這些輔助類實現依賴注入。

1.綁定XML布局

為Android的View綁定ID或者方法

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

    text.setText("HELLO , WORLD");
}
2.ButterKnife.bind內部的處理
static void bind(@NonNull Object target, @NonNull Object source, @NonNull Finder finder) {
//當前的activity,dialog,fragment,View等
Class<?> targetClass = target.getClass();
try {
    //1.創建一個類的實例 , 加入到緩存,并返回該實例
    ViewBinder<Object> viewBinder = findViewBinderForClass(targetClass);

    //2.調用了實例中的bind方法
    viewBinder.bind(finder, target, source);

} catch (Exception e) {
  throw new RuntimeException("Unable to bind views for " + targetClass.getName(), e);
}}
2.1.創建類的實例。

舉個栗子:
現在我在activity中執行ButterKnife.Bind方法。他會去尋找XXXActivity$$ViewBinder這個類,通過反射加載并創建類的實例對象。至于為什么要找這個類,這個會在下面分析。我們先來看下接下來的操作

private static ViewBinder<Object> findViewBinderForClass(Class<?> cls)
    throws IllegalAccessException, InstantiationException {
ViewBinder<Object> viewBinder = BINDERS.get(cls);
if (viewBinder != null) {
  //首先從緩存中判斷是否存在對應的ViewBinder,如果有直接返回
  if (debug) Log.d(TAG, "HIT: Cached in view binder map.");
  return viewBinder;
}
String clsName = cls.getName();
if (clsName.startsWith("android.") || clsName.startsWith("java.")) {
  //ButterKnife提供的注解只支持在應用程序使用,如果掃描的是framework層的類,則返回NOP_VIEW_BINDER
  if (debug) 
    Log.d(TAG, "MISS: Reached framework class. Abandoning search.");
  return NOP_VIEW_BINDER;
}
try {
  //根據反射原理,構造了類的實例,其實就是各種監聽的生成類,詳情如下圖
  Class<?> viewBindingClass = Class.forName(clsName + "$$ViewBinder");
  viewBinder = (ViewBinder<Object>) viewBindingClass.newInstance();
  if (debug) 
        Log.d(TAG, "HIT: Loaded view binder class.");
} catch (ClassNotFoundException e) {
  if (debug) 
        Log.d(TAG, "Not found. Trying superclass " + cls.getSuperclass().getName());
  //如果查找不到就遞歸去查找SuperClass$$ViewBinder這個類。
  viewBinder = findViewBinderForClass(cls.getSuperclass());
}
  //添加到緩存中
  BINDERS.put(cls, viewBinder);
return viewBinder;
}
2.2.執行類的實例的Bind方法。這里我們通過反編譯APK逆推下

先介紹下ViewBinder,他是一個接口。所有使用過ButterKnife中@Bind注解的類都會生成一個中間工具類用來實現View綁定或者數據綁定的業務邏輯,這個中間工具類會繼承原始類(Activity,Fragment)并實現ViewBinder接口,ViewBinder中有一個抽象方法bind,這便是ButterKnife需要來替我們實現的用來綁定數據方法。如下
溫馨提示:ButterKnife官網也有類似的介紹,如果下面的看不懂可以去官網查看。

/** 
* Created by alexshaw on 16-3-26. 
*/
public class MainActivity extends AppCompatActivity 
{    
    @Bind(R.id.text)    
    TextView mText;    
    @Bind(R.id.confirm)    
    Button mConfirm;    
    @Bind(R.id.cancle)    
    Button mCancle;    
    @BindString(R.string.hello)    
    String mString;    
    @Bind(R.id.icon)    
    ImageView mIcon;            
    @BindDrawable(R.drawable.ic_launcher)    
    Drawable drawable;    
    @Override    
  protected void onCreate(Bundle savedInstanceState) {     
      super.onCreate(savedInstanceState);  
      setContentView(R.layout.activity_main);        
      ButterKnife.bind(this);        
      mText.setText(mString);        
      mIcon.setImageDrawable(drawable);    
  }    
    @OnClick(R.id.confirm)    
    public void onConfirm(View view) {        mText.setText("確認");    }   
    @OnClick(R.id.cancle)    
    public void onCancle(View view) {        mText.setText("取消");    }}

如上代碼如果編譯成APK,生成的新代碼會如何呢?如下圖
首先是MAinActivity(因篇幅問題,這里只展示重要的代碼,見諒)

Paste_Image.png

這樣我們就可以看到onCreate中調用了ButterKnife.Bind方法。我們之前分析了Bind方法的業務邏輯,無非兩步

  1. 查找XXX$$ViewBinder并創建實例
  2. 調用實例的bind方法
    此時我們在看下ButterKnife幫助我們生成的中間類
Paste_Image.png

接下來就是分析MainActivity$$ViewBinder這個類了

Paste_Image.png

簡單介紹下bind方法的參數,第二個參數和第三個參數是相同的,都是Activity或者Fragment等類的實例,第一個參數為Finder,他是一個枚舉類型,提供了一系列用來查找指定ID的View或者資源的方法。

這時再來看bind方法,我們就可以理解了。paramT就是被綁定的類(Activity,Fragment)它內部的屬性通過Find這個類來查找并賦值。在這里我們就可以解釋文章開始時遺留下的問題了。為什么@Bind注解綁定的變量必須聲明為public。

原理我們介紹了,接下來。我們就要介紹下實現流程了。

Butterknife的處理流程

但凡涉及到注解的處理,都需要找AbstractProcessor或者是其實現類 , Butterknife的實現入口是ButterKnifeProcessor類,該類繼承自AbstractProcessor并重寫了process方法來處理添加了注解的Java類。

1. 注解處理的入口

@Override 
public boolean process(Set<? extends TypeElement> elements, RoundEnvironment env) {

//存儲BindingClass的集合,重點是findAndParseTargets方法
Map<TypeElement, BindingClass> targetClassMap = findAndParseTargets(env);

for (Map.Entry<TypeElement, BindingClass> entry : targetClassMap.entrySet()) {
  TypeElement typeElement = entry.getKey();
  BindingClass bindingClass = entry.getValue();

  try {
    bindingClass.brewJava().writeTo(filer);
  } catch (IOException e) {
    error(typeElement, "Unable to write view binder for type %s: %s", typeElement,
        e.getMessage());
  }
}
return true;
}

findAndParseTargets方法是用來查找出所有注解標注過的元素他的業務邏輯如下:(因代碼是在太長,這里只顯示主要代碼,其他業務邏輯用注釋表示)

private Map<TypeElement, BindingClass>findAndParseTargets(RoundEnvironment 
    env) {

  // 遍歷每一個@Bind元素
  for (Element element : env.getElementsAnnotatedWith(Bind.class)) {
    // 校驗是否合法
    if (!SuperficialValidation.validateElement(element))
      continue;
    try {
      parseBind(element, targetClassMap, erasedTargetNames);
    } catch (Exception e) {
      logParsingError(element, Bind.class, e);
    }
  }
  // 遍歷每一個監聽的注解元素
  for (Class<? extends Annotation> listener : LISTENERS) {
    findAndParseListener(env, listener, targetClassMap,   erasedTargetNames);
  }
    // 遍歷 @BindArray 元素.
    // 遍歷 @BindBool 元素.
    // 遍歷 @BindColor 元素.
    // 遍歷 @BindDimen 元素.
    // 遍歷 @BindDrawable 元素.
    // 遍歷 @BindInt 元素.
    // 遍歷 @BindString 元素.
    // 遍歷 @Unbinder 元素.
 }

2. 接下來就是解析Bind注解元素和各種監聽類注解元素的邏輯處理

/**
 * @Bind注解的處理
 * 
 * @param element
 * @param targetClassMap
 * @param erasedTargetNames
 */
//遍歷每一個被注解標注過得屬性或方法
private void parseBindOne(Element element, Map<TypeElement, BindingClass> targetClassMap,
  Set<TypeElement> erasedTargetNames) {
//第一步:進行校驗...判斷目標字段的定義類型是否是View的子類型或者是一個接口類型,
  檢查目標字段的可訪問性,是否只綁定了一個ID,balabala...

//第二步:判斷集合中是否有元素對應的bindingClass
BindingClass bindingClass = targetClassMap.get(enclosingElement);
if (bindingClass != null) {//如果有:判斷是否重復綁定
  //通過ID拿到ViewBinders
  ViewBindings viewBindings = bindingClass.getViewBinding(id);
  if (viewBindings != null) {
    Iterator<FieldViewBinding> iterator = viewBindings.getFieldBindings().iterator();
    if (iterator.hasNext()) {
      FieldViewBinding existingBinding = iterator.next();
      error(...);
      return;
    }
  }
} else {//如果沒:創建一個BindingClass并添加到集合里去
  bindingClass = getOrCreateTargetClass(targetClassMap, enclosingElement);
}

String name = element.getSimpleName().toString();
TypeName type = TypeName.get(elementType);
boolean required = isFieldRequired(element);

FieldViewBinding binding = new FieldViewBinding(name, type, required);
bindingClass.addField(id, binding);

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

/**
 * 解析監聽注解
 * @param annotationClass
 * @param element
 * @param targetClassMap
 * @param erasedTargetNames
 * @throws Exception
 */
private void parseListenerAnnotation(Class<? extends Annotation> annotationClass, Element element,
  Map<TypeElement, BindingClass> targetClassMap, Set<TypeElement> erasedTargetNames)
  throws Exception {
//第一步做各種判斷...
//返回類型是否是int[]
//是否是private或者static
//ID是否重復
//監聽注解類是否存在
//ID是否合法
//balabala太長了..愁死我了...
//重點!!!!!!!!!!!!
Parameter[] parameters = Parameter.NONE;
if (!methodParameters.isEmpty()) {
    //方法的參數的判斷及處理,后面會用到
}
//通過將方法名稱,參數,required組合成一個MethodViewBinding并傳入bindingClass,
//最后將bindClass傳入集合中
MethodViewBinding binding = new MethodViewBinding(name, Arrays.asList(parameters), required);
BindingClass bindingClass = getOrCreateTargetClass(targetClassMap, enclosingElement);
for (int id : ids) {
  if (!bindingClass.addMethod(id, listener, method, binding)) {
    error(element, "Multiple listener methods with return value specified for ID %d. (%s.%s)",
        id, enclosingElement.getQualifiedName(), element.getSimpleName());
    return;
  }
}
erasedTargetNames.add(enclosingElement);


  /**  
     * 創建targetClass,即XXX$$ViewBinder類,這里確定了XXX的名字類型等信息
     * @param targetClassMap   
     * @param enclosingElement   
     * @return   
     */
  private BindingClass getOrCreateTargetClass(Map<TypeElement, BindingClass> targetClassMap,    TypeElement enclosingElement) {  
      BindingClass bindingClass = targetClassMap.get(enclosingElement); 
      if (bindingClass == null) {    
        //類或者接口的全名
        String targetType = enclosingElement.getQualifiedName().toString();  
        // BINDING_CLASS_SUFFIX = "$$ViewBinder",新生成的類的名字      
        String classPackage = getPackageName(enclosingElement);
        //包名,類名,完全限定名稱  
        String className = getClassName(enclosingElement, classPackage) + BINDING_CLASS_SUFFIX;      
        bindingClass = new BindingClass(classPackage, className, targetType);    
        targetClassMap.put(enclosingElement, bindingClass);  }  
        return bindingClass;
    }

3. 屬性和方法都解析完了,targetClassMap集合中的數據也齊全了。剩下的就是依靠數據生成新的中間類文件了。

@Override 
public boolean process(Set<? extends TypeElement> elements, RoundEnvironment env) {

Map<TypeElement, BindingClass> targetClassMap = findAndParseTargets(env);
//遍歷map并生成文件
for (Map.Entry<TypeElement, BindingClass> entry : targetClassMap.entrySet()) {
    bindingClass.brewJava().writeTo(filer);
    }
return true;
}

來瞅一眼文件生成的方法。bindingClass.brewJava()方法

  JavaFile brewJava() {
//添加類名 public class XXX extends XXX implement XXX
TypeSpec.Builder result = TypeSpec.classBuilder(className)
    .addModifiers(PUBLIC)
    .addTypeVariable(TypeVariableName.get("T", ClassName.bestGuess(targetClass)));

if (parentViewBinder != null) {
  result.superclass(ParameterizedTypeName.get(ClassName.bestGuess(parentViewBinder),
      TypeVariableName.get("T")));
} else {
  result.addSuperinterface(ParameterizedTypeName.get(VIEW_BINDER, TypeVariableName.get("T")));
}
//添加方法
result.addMethod(createBindMethod());

if (hasUnbinder()) {
  // Create unbinding class.
  result.addType(createUnbinderClass());
  //sss
  createUnbinderInternalAccessMethods(result);
}

return JavaFile.builder(classPackage, result.build())
    .addFileComment("Generated code from Butter Knife. Do not modify!")
    .build();

}

addMethod方法中的代碼

 /**
 * 創建最終反編譯得到的class文件中的bind方法的代碼
 * 
 */    
private MethodSpec createBindMethod() {
MethodSpec.Builder result = MethodSpec.methodBuilder("bind")//方法名
    .addAnnotation(Override.class)//添加override注解
    .addModifiers(PUBLIC) //public
    .addParameter(FINDER, "finder", FINAL)//param1
    .addParameter(TypeVariableName.get("T"), "target", FINAL)//param2
    .addParameter(Object.class, "source");//param3

    balabala...

    //查看viewIDMap集合是否有數據,即是否有綁定ID的View,如果有則加進去
if (!viewIdMap.isEmpty() || !collectionBindings.isEmpty()) {
    result.addStatement("$T view", VIEW);
  //添加bind方法里的代碼:“view = finder.findOptionalView(source, $L, null)”
  for (ViewBindings bindings : viewIdMap.values()) {
    addViewBindings(result, bindings);
  }
  //綁定集合
  for (Map.Entry<FieldCollectionViewBinding, int[]> entry : collectionBindings.entrySet()) {
    emitCollectionBinding(result, entry.getKey(), entry.getValue());
  }
}
balabala....

最后將生成的這些字符串寫到java文件中。然后就可以編譯成class文件了。。
最后使用FilerAPI創建輔助類文件,BindingClass的brewJava()方法根據模型“醞釀”Java代碼,之后使用Java IO流把代碼寫入文件。

AndroidAnnotation(AA)與ButterKnife的比較,

AA的分析如果沒看的話建議先讀一下老衲的上一篇AA注解的介紹與流程分析

  1. 首先從功能上來說,AA提供的注解數量遠多于ButterKnife,功能也是無所不包(View的綁定,線程,監聽,動畫,balabala...)而ButterKnife僅僅提供針對View的注解。
  2. 其次從兩類框架的實現流程上來說,AA在一開始就已經生成了新的代碼XXXActivity_,后續的執行都是依賴于新的代碼。生成的方法和代碼量較多。ButterKnife在編譯時也是會生成新的中間工具類,代碼量相對于AA來說略少,但是新增了類文件。并且,在運行時,需要通過一點點反射的技術來實現整體的邏輯。
  3. 第三,從上手成都上來說,AA的前期工作略麻煩一些,并且后期需要手動修改類名(XXX的后面加上下劃線)ButterKnife則需要在類中添加ButterKnife.Bind方法來使用綁定功能。AA稍微麻煩一丟丟。

好了,ButterKnife的使用介紹,流程分析以及它和AA之間的比較已經寫完,如果有什么意見或者不對的地方,請大家指正。
接下來想要給帶來的是第三款注解框架Dagger2的使用及流程分析,以及現階段流行的圖片加載資源庫的分析,但是,由于老衲智商最近一直沒上線→_→,導致Dagger2的環境配了一個多星期還沒配好。所以推出時間可能會略晚,希望大家見諒。

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

推薦閱讀更多精彩內容