Android主流三方庫源碼分析(七、深入理解ButterKnife源碼)

前言

成爲一名優秀的Android開發,需要一份完備的知識體系,在這裏,讓我們一起成長爲自己所想的那樣~。

不知不覺,筆者已經對Android主流三方庫中的網絡框架OkHttp、Retrofit,圖片加載框架Glide、數據庫框架GreenDao、響應式編程框架RxJava、內存泄露框架LeakCanary進行了詳細的分析,如果有朋友對這些開源框架的內部實現機制感興趣的話,可以在筆者的個人主頁選擇相應的文章閱讀。這篇,我將會對Android中的依賴注入框架ButterKnife的源碼實現機制進行詳細地講解。

一、簡單示例

首先,我們先來看一下ButterKnife的基本使用(取自Awesome-WanAndroid),如下所示:

public class CollectFragment extends BaseRootFragment<CollectPresenter> implements CollectContract.View {

    @BindView(R.id.normal_view)
    SmartRefreshLayout mRefreshLayout;
    @BindView(R.id.collect_recycler_view)
    RecyclerView mRecyclerView;
    @BindView(R.id.collect_floating_action_btn)
    FloatingActionButton mFloatingActionButton;
    
    @Nullable
    @Override
    public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
        View view = inflater.inflate(getLayoutId(), container, false);
        unBinder = ButterKnife.bind(this, view);
        initView();
        return view;
    }
    
    @OnClick({R.id.collect_floating_action_btn})
    void onClick(View view) {
        switch (view.getId()) {
            case R.id.collect_floating_action_btn:
                mRecyclerView.smoothScrollToPosition(0);
                break;
            default:
                break;
        }
    }
    
    
    @Override
    public void onDestroyView() {
        super.onDestroyView();
        if (unBinder != null && unBinder != Unbinder.EMPTY) {
            unBinder.unbind();
            unBinder = null;
        }
    }

可以看到,我們使用了@BindView()替代了findViewById()方法,然後使用了@OnClick替代了setOnClickListener()方法。ButterKnife的初期版本是通過使用註解+反射這樣的運行時解析的方式實現上述功能的,後面,爲了改善性能,便使用了註解+APT編譯時解析技術並從中生成配套模板代碼的方式來實現。

在開始分析之前,可能有同學對APT不是很瞭解,我這裏普及一下,APT是Annotation Processing Tool的縮寫,即註解處理工具。它的使用步驟通常爲如下三個步驟:

  • 1、首先,聲明註解的生命週期爲CLASS,即@Retention(CLASS)
  • 2、然後,通過繼承AbstractProcessor自定義一個註解處理器
  • 3、最後,在編譯的時候,編譯器會掃描所有帶有你要處理的註解的類,最後再調用AbstractProcessor的process方法,對註解進行處理

下面,我們正式來解剖一下ButterKnife的心臟。

二、源碼分析

1、模板代碼解析

首先,在我們編寫好上述的示例代碼之後,調用 gradle build 命令,在app/build/generated/source/apt下將可以找到APT爲我們生產的配套模板代碼CollectFragment_ViewBinding,如下所示:

public class CollectFragment_ViewBinding implements Unbinder {
    private CollectFragment target;
    
    private View view2131230812;
    
    @UiThread
    public CollectFragment_ViewBinding(final CollectFragment target, View source) {
      this.target = target;
    
      View view;
      // 1
      target.mRefreshLayout = Utils.findRequiredViewAsType(source, R.id.normal_view, "field 'mRefreshLayout'", SmartRefreshLayout.class);
      target.mRecyclerView = Utils.findRequiredViewAsType(source, R.id.collect_recycler_view, "field 'mRecyclerView'", RecyclerView.class);
      view = Utils.findRequiredView(source, R.id.collect_floating_action_btn, "field 'mFloatingActionButton' and method 'onClick'");
      target.mFloatingActionButton = Utils.castView(view, R.id.collect_floating_action_btn, "field 'mFloatingActionButton'", FloatingActionButton.class);
      view2131230812 = view;
      // 2
      view.setOnClickListener(new DebouncingOnClickListener() {
        @Override
        public void doClick(View p0) {
          target.onClick(p0);
        }
      });
    }
    
    @Override
    @CallSuper
    public void unbind() {
      CollectFragment target = this.target;
      if (target == null) throw newIllegalStateException("Bindings already     cleared.");
      this.target = null;
    
      target.mRefreshLayout = null;
      target.mRecyclerView = null;
      target.mFloatingActionButton = null;
    
      view2131230812.setOnClickListener(null);
      view2131230812 = null;
    }
}

生成的配套模板CollectFragment_ViewBinding中,在註釋1處,使用了ButterKnife內部的工具類Utils的findRequiredViewAsType()方法來尋找控件。在註釋2處,使用了view的setOnClickListener()方法來添加了一個去抖動的DebouncingOnClickListener,這樣便可以防止重複點擊,在重寫的doClick()方法內部,直接調用了CollectFragment的onClick方法。最後,我們在深入看下Utils的findRequiredViewAsType()方法內部的實現。

public static <T> T findRequiredViewAsType(View source, @IdRes int id, String who,
  Class<T> cls) {
    // 1
    View view = findRequiredView(source, id, who);
    // 2
    return castView(view, id, who, cls);
}

public static View findRequiredView(View source, @IdRes int id, String who) {
    View view = source.findViewById(id);
    if (view != null) {
        return view;
    }
    
    ...
}

public static <T> T castView(View view, @IdRes int id, String who, Class<T> cls) {
    try {
        return cls.cast(view);
    } catch (ClassCastException e) {
        ...
    }
}

在註釋1處,最終也是通過View的findViewById()方法找到相應的控件,在註釋2處,通過相應Class對象的cast方法強轉成對應的控件類型

2、ButterKnife 是怎樣實現代碼注入的

接下來,爲了使用這套模板代碼,我們必須調用ButterKnife的bind()方法實現代碼注入,即自動幫我們執行重複繁瑣的findViewById和setOnClicklistener操作。下面我們來分析下bind()方法是如何實現注入的。

@NonNull @UiThread
public static Unbinder bind(@NonNull Object target, @NonNull View source) {
    return createBinding(target, source);
}

在bind()方法中調用了createBinding(),

@NonNull @UiThread
public static Unbinder bind(@NonNull Object target, @NonNull View source) {
    Class<?> targetClass = target.getClass();
    // 1
    Constructor<? extends Unbinder> constructor = findBindingConstructorForClass(targetClass);

    if (constructor == null) {
        return Unbinder.EMPTY;
    }

    
    try {
        // 2
        return constructor.newInstance(target, source);
    // 3
    } catch (IllegalAccessException e) {
    ...
}

首先,在註釋1處,通過 findBindingConstructorForClass() 方法從 Class 中查找 constructor,這裏constructor即上文生成的CollectFragment_ViewBinding類。然後,在註釋2處,利用反射來新建 constructor 對象。最後,如果新建 constructor 對象失敗,則會在註釋3後面捕獲一系列對應的異常進行自定義異常拋出處理。

下面,我們來詳細分析下
findBindingConstructorForClass() 方法的實現邏輯。

@VisibleForTesting
static final Map<Class<?>, Constructor<? extends Unbinder>> BINDINGS = new LinkedHashMap<>();

@Nullable @CheckResult @UiThread
private static Constructor<? extends Unbinder> findBindingConstructorForClass(Class<?> cls) {
    // 1
    Constructor<? extends Unbinder> bindingCtor = BINDINGS.get(cls);
    if (bindingCtor != null || BINDINGS.containsKey(cls)) {
        return bindingCtor;
    }
    
    // 2
    String clsName = cls.getName();
    if (clsName.startsWith("android.") || clsName.startsWith("java.")
    || clsName.startsWith("androidx.")) {
        return null;
    }
    
    try {
        // 3
        Class<?> bindingClass = cls.getClassLoader().loadClass(clsName + "_ViewBinding");
        bindingCtor = (Constructor<? extends Unbinder>) bindingClass.getConstructor(cls, View.class);
    } catch (ClassNotFoundException e) {
        // 4
        bindingCtor = findBindingConstructorForClass(cls.getSuperclass());
    } catch (NoSuchMethodException e) {
        throw new RuntimeException("Unable to find binding constructor for " + clsName, e);
    }
    
    // 5
    BINDINGS.put(cls, bindingCtor);
    return bindingCtor;
}

這裏,我把多餘的log代碼刪除並把代碼格式優化了一下,可以看到,findBindingConstructorForClass() 這個方法中的邏輯瞬間清晰不少,這裏建議以後大家自己在分析源碼的時候可以進行這樣的優化重整,會帶來不少好處。

重新看到 findBindingConstructorForClass() 方法,在註釋1處,我們首先從緩存BINDINGS中獲取CollectFragment類對象對應的模塊類CollectFragment_ViewBinding的構造器對象,這裏的BINDINGS是一個LinkedHashMap對象,它保存了上述兩者的映射關係。在註釋2處,如果是 android,androidx,java 原生的文件,不進行處理。在註釋3處,先通過CollectFragment類對象的類加載器加載出對應的模塊類CollectFragment_ViewBinding的類對象,再通過自身的getConstructor()方法獲得相應的構造對象。如果在步驟3中加載不出對應的模板類對象,則會在註釋4處使用類似遞歸的方法重新執行findBindingConstructorForClass()方法。最後,如果找到了bindingCtor模板構造對象,則將它保存在BINDINGS這個LinkedHashMap對象中。

這裏總結一下findBindingConstructorForClass()方法的處理:

  • 1、首先從緩存BINDINGS中獲取CollectFragment類對象對應的模塊類CollectFragment_ViewBinding的構造器對象,獲取不到,則繼續執行下面的操作
  • 2、如果不是android,androidx,java 原生的文件,再進行後面的處理
  • 3、通過CollectFragment類對象的類加載器加載出對應的模塊類CollectFragment_ViewBinding的類對象,再通過自身的getConstructor()方法獲得相應的構造對象,如果獲取不到,會拋出異常,在異常的處理中,我們會從當前 class 文件的父類中再去查找。如果找到了,最後會將bindingCtor對象緩存進在BINDINGS對象中

3、ButterKnife是如何在編譯時生成代碼的?

在編譯的時候,ButterKnife會通過自定義的註解處理器ButterKnifeProcessor的process方法,對編譯器掃描到的要處理的類中的註解進行處理,然後,通過javapoet這個庫來動態生成綁定事件或者控件的模板代碼,最後在運行的時候,直接調用bind方法完成綁定即可。

首先,我們先來分析下ButterKnifeProcessor的重寫的入口方法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) {
           ...
        }
    }

    typeUtils = env.getTypeUtils();
    filer = env.getFiler();
    ...
}

可以看到,ProcessingEnviroment對象提供了兩大工具類 typeUtils和filer。typeUtils的作用是用來處理TypeMirror,而Filer則是用來創建生成輔助文件

接着,我們再來看看被重寫的getSupportedAnnotationTypes()方法,這個方法的作用主要是用於指定ButterknifeProcessor註冊了哪些註解的。

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

這裏面首先創建了一個LinkedHashSet對象,然後將getSupportedAnnotations()方法返回的支持註解集合進行遍歷一一併添加到types中返回。

接着我們看下getSupportedAnnotations()方法,

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;
}

可以看到,這裏註冊了一系列的Bindxxx註解類和監聽列表LISTENERS,接着看一下LISTENERS中包含的監聽方法:

private static final List<Class<? extends Annotation>> LISTENERS = Arrays.asList(
    OnCheckedChanged.class, 
    OnClick.class, 
    OnEditorAction.class, 
    OnFocusChange.class, 
    OnItemClick.class, 
    OnItemLongClick.class, 
    OnItemSelected.class, 
    OnLongClick.class, 
    OnPageChange.class, 
    OnTextChanged.class, 
    OnTouch.class 
);

最後,我們來分析下整個ButterKnifeProcessor中最關鍵的方法process()。

@Override public boolean process(Set<? extends TypeElement> elements, RoundEnvironment env) {
    // 1
    Map<TypeElement, BindingSet> bindingMap = findAndParseTargets(env);

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

        // 2
        JavaFile javaFile = binding.brewJava(sdk, debuggable);
        try {
            javaFile.writeTo(filer);
        } catch (IOException e) {
           ...
        }
    }

    return false;
}

首先,在註釋1處通過findAndParseTargets()方法,知名見義,它應該就是找到並解析註解目標的關鍵方法了,繼續看看它內部的處理:

private Map<TypeElement, BindingSet> findAndParseTargets(RoundEnvironment env) {
    Map<TypeElement, BindingSet.Builder> builderMap = new LinkedHashMap<>();
    Set<TypeElement> erasedTargetNames = new LinkedHashSet<>();

    // 1、一系列處理每一個@Bindxxx元素的for循環代碼塊
    ...

    // Process each @BindView element.
    for (Element element : env.getElementsAnnotatedWith(BindView.class)) {
        try {
        // 2
        parseBindView(element, builderMap, erasedTargetNames);
        } catch (Exception e) {
            logParsingError(element, BindView.class, e);
        }
    }

    // Process each @BindViews element.
    ...

    // Process each annotation that corresponds to a listener.
    for (Class<? extends Annotation> listener : LISTENERS) {
        findAndParseListener(env, listener, builderMap, erasedTargetNames);
    }

    // 2
    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 {
            entries.addLast(entry);
            }
        }
    }
    return bindingMap;
}

findAndParseTargets()方法的代碼非常多,我這裏儘可能做了精簡。首先,在註釋1處,掃描並處理所有具有@Bindxxx註解和符合LISTENERS監聽方法集合的代碼,然後在每一個@Bindxxx對應的for循環代碼中的parseBindxxx()或findAndParseListener()方法中將解析出的信息放入builderMap這個LinkedHashMap對象中,其中builderMap是一個key爲TypeElement,value爲BindingSet.Builder的映射集合,這個 BindSet 是指的一個類型請求的所有綁定的集合。在註釋3處,首先使用上面的builderMap對象去構建了一個entries對象,它是一個雙向隊列,能實現兩端存取的操作。接着,又新建了一個key爲TypeElement,value爲BindingSet的LinkedHashMap對象,最後使用了一個while循環從entries的第一個元素開始,這裏會判斷當前元素類型是否有父類,如果沒有,直接構建builder放入bindingMap中,如果有,則將parentBinding添加到BindingSet.Builder這個建造者對象中,然後再創建BindingSet再添加到bindingMap中。

接着,我們分析下注釋2處parseBindView是如何對每一個@BindView註解的元素進行處理。

private void parseBindView(Element element, Map<TypeElement, BindingSet.Builder> builderMap,
  Set<TypeElement> erasedTargetNames) {
    TypeElement enclosingElement = (TypeElement) element.getEnclosingElement();

    // 1、首先驗證生成的常見代碼限制
    ...

    // 2、驗證目標類型是否繼承自View。
    ...
    
    // 3
    int id = element.getAnnotation(BindView.class).value();
    BindingSet.Builder builder = builderMap.get(enclosingElement);
    Id resourceId = elementToId(element, BindView.class, id);
    if (builder != null) {
        String existingBindingName = builder.findExistingBindingName(resourceId);
        if (existingBindingName != null) {
            ...
            return;
        }
    } else {
        // 4
        builder = getOrCreateBindingBuilder(builderMap, enclosingElement);
    }

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

    // 5
    builder.addField(resourceId, new     FieldViewBinding(name, type, required));

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

首先,在註釋1、2處均是一些驗證處理操作,如果不符合則會return。然後,我們看到註釋3處,這裏獲取了BindView要綁定的View的id,然後先從builderMap中獲取BindingSet.Builder對象,如果存在,直接return。如果不存在,則會在註釋4處的
getOrCreateBindingBuilder()方法生成一個。我們看一下getOrCreateBindingBuilder()方法:

private BindingSet.Builder getOrCreateBindingBuilder(
  Map<TypeElement, BindingSet.Builder> builderMap, TypeElement enclosingElement) {
    BindingSet.Builder builder = builderMap.get(enclosingElement);
    if (builder == null) {
        builder = BindingSet.newBuilder(enclosingElement);
        builderMap.put(enclosingElement, builder);
    }
    return builder;
}

可以看到,這裏會再次從buildMap中獲取BindingSet.Builder對象,如果沒有則直接調用BindingSet的newBuilder()方法新建一個BindingSet.Builder對象並保存在builderMap中,然後,再將新建的builder對象返回。

回到parseBindView()方法的註釋5處,這裏根據view的信息生成一個FieldViewBinding,最後添加到上邊生成的builder對象中。

最後,再回到我們的process()方法中,現在所有的綁定的集合數據都放在了bindingMap對象中,這裏使用for循環取出每一個BindingSet對象,調用它的brewJava()方法,看看它內部的處理:

JavaFile brewJava(int sdk, boolean debuggable) {
    TypeSpec bindingConfiguration = createType(sdk, debuggable);
    return JavaFile.builder(bindingClassName.packageName(), bindingConfiguration)
    .addFileComment("Generated code from Butter Knife. Do not modify!")
    .build();
}

private TypeSpec createType(int sdk, boolean debuggable) {
    TypeSpec.Builder result = TypeSpec.classBuilder(bindingClassName.simpleName())
    .addModifiers(PUBLIC);
    if (isFinal) {
        result.addModifiers(FINAL);
    }

    if (parentBinding != null) {
        result.superclass(parentBinding.bindingClassName);
    } else {
        result.addSuperinterface(UNBINDER);
    }

    if (hasTargetField()) {
        result.addField(targetTypeName, "target", PRIVATE);
    }

    if (isView) {
        result.addMethod(createBindingConstructorForView());
    } else if (isActivity) {
        result.addMethod(createBindingConstructorForActivity());
    } else if (isDialog) {
        result.addMethod(createBindingConstructorForDialog());
    }
    if (!constructorNeedsView()) {
        // Add a delegating constructor with a target type + view signature for reflective use.
        result.addMethod(createBindingViewDelegateConstructor());
    }
    result.addMethod(createBindingConstructor(sdk, debuggable));

    if (hasViewBindings() || parentBinding == null) {
        result.addMethod(createBindingUnbindMethod(result));
    }

    return result.build();
}

在createType()方法裏面使用了java中的javapoet技術生成了一個bindingConfiguration對象,很顯然,它裏面保存了所有的綁定配置信息。然後,通過javapoet的builder構造器將上面得到的bindingConfiguration對象構建生成一個JavaFile對象,最終,通過javaFile.writeTo(filer)生成了java源文件

至此,ButterKnife的源碼分析就結束了。

三、總結

從上面的源碼分析來看,ButterKnife的執行流程總體可以分爲如下兩步:

  • 1、在編譯的時候掃描註解,並通過自定義的ButterKnifeProcessor做相應的處理解析得到bindingMap對象,最後,調用 javapoet 庫生成java模板代碼
  • 2、當我們調用 ButterKnife的bind()方法的時候,它會根據類的全限定類型,找到相應的模板代碼,並在其中完成 findViewById 和 setOnClick ,setOnLongClick 等操作

接下來,筆者會對Android中的依賴注入框架Dagger2的源碼實現流程進行詳細的講解,敬請期待~

參考鏈接:

1、ButterKnife V10.0.0 源碼

2、Android進階之光

3、ButterKnife源碼分析

4、butterknife 源碼分析

Contanct Me

● 微信:

歡迎關注我的微信:bcce5360

● 微信羣:

微信羣如果不能掃碼加入,麻煩大家想進微信羣的朋友們,加我微信拉你進羣。

● QQ羣:

2千人QQ羣,Awesome-Android學習交流羣,QQ羣號:959936182, 歡迎大家加入~

About me

很感謝您閱讀這篇文章,希望您能將它分享給您的朋友或技術羣,這對我意義重大。

希望我們能成爲朋友,在 Github掘金上一起分享知識。

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