從手寫ButterKnife到掌握註解、AnnotationProcessor

ButterKnife的應用

如題所示,這篇文章主要講解手把手教學寫個簡易版 ButterKnife,因此,仍是先看看 ButterKnife 怎麼使用,不過大多數人都用過,就不詳細介紹,如有須要,請參考:java

github.com/JakeWharton…android

雖然有人以爲,在 kotlin 中,使用 KTX 工具或者使用 MVVM,已經不多使用 ButterKnife 了,因此以爲不必去研究 ButterKnife,可是咱們學習並不是是爲了使用,而是爲了清楚其原理及構造,就如本文,只是藉手寫 ButterKnife 去了解如何實現註解、annotationProcessor 的使用等。項莊舞劍;意在沛公,在意山水之間。git

好了,很少說,開始個人表演:github

build.gradle添加依賴:api

android {
  ...
  // Butterknife requires Java 8.
  compileOptions {
    sourceCompatibility JavaVersion.VERSION_1_8
    targetCompatibility JavaVersion.VERSION_1_8
  }
}
dependencies {
  implementation 'com.jakewharton:butterknife:10.2.3'
  annotationProcessor 'com.jakewharton:butterknife-compiler:10.2.3'
}
複製代碼

MainActivity中書寫:markdown

public class MainActivity extends AppCompatActivity {
    @BindView(R.id.tv_content)
    TextView tvContent;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
		···
        ButterKnife.bind(this);
        tvContent.setText("修改爲功!");
    }
}
複製代碼

em...運行結果就不發,反正是成功的😂。app

稍微拆解下,ButterKnife 的代碼使用爲兩步:ide

  • @BindView(R.id.tv_content):註解聲明
  • ButterKnife.bind(this);:註解解析

註解聲明

首先,咱們新建一個註解MyBindView.java工具

public @interface MyBindView {
}
複製代碼

註解其實就是一種標籤說明,並無實際的特殊代碼做用,就相似於註釋:oop

  • 單行註釋 // 這是單註釋
  • 多行註釋 /這是多行註釋/
  • Javadoc註釋 /*這是javadoc註釋/

可是不一樣於註釋在字節碼階段就被擦除,註解能夠選擇保留到什麼階段,和附帶了其它功能:

  • @Retention
    • RetentionPolicy.SOURCE:僅保留在源碼階段,其它階段被擦除
    • RetentionPolicy.CLASS:保留到字節碼階段,字節碼以後的階段被擦除
    • RetentionPolicy.RUNTIME:保留到運行階段,也就是在代碼運行的時候,還能讀取到註解
  • @Documented
    • 標記註解可以包含在用戶說明文檔中
  • @Target
    • 限制可註解的對象,如只能註解成員變量、方法、類等,詳情請查看 ElementType 類
  • @Inherited
    • 標識該註解可繼承。如 A 有 @Inherited 和 @Retention、@Target,B 沒有註解,可是 B extend A,那麼 B 就會繼承 A 的註解

根據上面的說明,咱們就能夠完善下註解。

由於咱們的註解在運行階段要用到,而且只能註解成員變量,因此,能夠寫成這樣:

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface MyBindView {
}
複製代碼

而後就可使用了:

@MyBindView
    TextView tvContent;
複製代碼

不過,這樣是不夠的,咱們不知道要綁定哪一個 id,因此,咱們要在註解上加上變量,這樣賦值過去,咱們就知道爲哪一個 id 綁定了:

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface MyBindView {
    int viewId();
}
複製代碼

嗯?爲何 viewId 後面有個 () ?這隻能說是寫法的規定,先記住。

咱們再看看使用:

@MyBindView(viewId = R.id.tv_content)
    TextView tvContent;
複製代碼

賦值成功!

em...多出了 viewId,可是去掉又會報錯?爲何 ButterKnife 不須要寫,而我須要?

這個由於要把 viewId 改成 value,假如名稱爲 value 的話,編譯器會自動幫咱們賦值,因此咱們要稍微改下:

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface MyBindView {
    int value();
}
複製代碼
@MyBindView(R.id.tv_content)
    TextView tvContent;
複製代碼

好了,註解聲明的功能已經完成了。咱們再加把勁,把註解解析也幹掉。

註解解析

ButterKnife 是經過如下代碼開始解析的:

ButterKnife.bind(this);
複製代碼

那咱們也新建個類:

public class MyButterKnife {
    public static void bind(Activity activity){
    }
}
複製代碼

OK,完結撒花~~

固然不是,咱們要在 bind() 方法裏面寫控件的綁定代碼。

首先,咱們先理清邏輯,只要邏輯沒問題,代碼實現就不在話下:

  • 獲取該 Activity 的所有成員變量
  • 判斷這個成員變量是否被 MyBindView 進行註解
  • 註解符合的話,就對該成員變量進行 findViewById 賦值

具體代碼以下:

public static void bind(Activity activity) {
        //獲取該 Activity 的所有成員變量
        for (Field field : activity.getClass().getDeclaredFields()) {
            //判斷這個成員變量是否被 MyBindView 進行註解
            MyBindView myBindView = field.getAnnotation(MyBindView.class);
            if (myBindView != null) {
                try {
                    //註解符合的話,就對該成員變量進行 findViewById 賦值
                    //至關於 field = activity.findViewById(myBindView.value())
                    field.set(activity, activity.findViewById(myBindView.value()));
                } catch (IllegalAccessException e) {
                    e.printStackTrace();
                }
            }
        }
    }
複製代碼

運行!

我看了下手機,運行是沒問題的!

雖然,上面的功能實現上是沒有問題的,可是,每一個控件的綁定都要依靠反射,這太耗性能,一個還好,可是正常的 Activity 都不止一個 View,隨着 View 的增長,執行的時間越長,因此,咱們必須尋找新的出路,那就是AnnotationProcessor

AnnotationProcessor前言

在講 AnnotationProcessor 以前,咱們先想下,如何才能作到批量對 View 進行綁定,可是又不會消耗太多性能。

em...

最不消耗性能的方式,不就是直接使用 findViewById 去綁定 View 嗎?既然如此,那麼有沒有什麼方式可以作到在編譯階段就生成好 findViewById 這些代碼,到時使用時,直接調用就好了。

這個想法貌似沒有問題,那咱們先看看生成好的 findViewById 的這些代碼是長什麼樣的。

模擬生成好的代碼樣式:

public class MyButterKnife {
    public static void bind(MainActivity activity) {
        activity.tvContent = activity.findViewById(R.id.tv_content);
    }
}
複製代碼

不過,這樣仍是有問題,由於 bind() 方法裏面應該傳進去的是 Activity,這裏固定寫爲 MainActivity 是不對的,咱們應該要根據所傳進來的 Activity 是什麼,來決定加載哪一個類的 findViewById 的代碼,因此咱們要建立新類,這個類名有固定的形式,那就是原Activity名字+Binding

模擬自動生成的文件:

public class MainActivityBinding {
    public MainActivityBinding(MainActivity activity) {
        activity.tvContent = activity.findViewById(R.id.tv_content);
    }
}
複製代碼

修改後的 MyButterKnife

public class MyButterKnife {
    public static void bind(Activity activity) {
        try {
            //獲取"當前的activity類名+Binding"的class對象
            Class bindingClass = Class.forName(activity.getClass().getCanonicalName() + "Binding");
            //獲取class對象的構造方法,該構造方法的參數爲當前的activity對象
            Constructor constructor = bindingClass.getDeclaredConstructor(activity.getClass());
            //調用構造方法
            constructor.newInstance(activity);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
複製代碼

運行!

我看了下手機,運行依舊是沒問題的!

因此,咱們如今只剩下一個問題了,那就是如何動態生成 MainActivityBinding 這個類了。

這時,就是真正須要用到 AnnotationProcessor。

AnnotationProcessor 是一種處理註解的工具,可以在源代碼中查找出註解,而且根據註解自動生成代碼。

AnnotationProcessor使用

首先,咱們要新建一個 module。

Android Studio --> File --> New Module --> Java or Kotlin Library --> Next -->

至於命名,你們依據本身的狀況:

而後點擊 Finish 按鈕。

首先,讓 MyBindingProcessor 繼承 AbstractProcessor,

  • process():裏面存放着自動生成代碼的代碼。
  • getSupportedAnnotationTypes():返回所支持註解類型。
public class MyBindingProcessor extends AbstractProcessor {
    @Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
        return false;
    }
    @Override
    public Set<String> getSupportedAnnotationTypes() {
        return super.getSupportedAnnotationTypes();
    }
}
複製代碼

不過,要使得 MyBindingProcessor 可以自動被調用,咱們還須要稍微配置下:

項目結構更改

另外,爲了功能解耦以及後續功能開發,咱們須要把MyButterKnife.javaMyBindView.java文件單獨抽取出來,各自使用一個 module 進行存放,目錄結構以下:

app 依賴:

implementation project(path: ':my-reflection')
    annotationProcessor project(':my-processor')
複製代碼

my-reflection 依賴:

api project(path: ':my-annotations')
複製代碼

my-processor 依賴:

implementation project(':my-annotations')
複製代碼

好了,一切準備就緒。

咱們看回MyBindingProcessor.class,增長了註解支持和打印了日誌。畢竟咱們要測試下配置有沒有什麼問題,有的話,返回去看看哪裏錯漏,沒有再繼續寫下去。

public class MyBindingProcessor extends AbstractProcessor {
    @Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
        //測試輸出
        System.out.println("配置成功!");
        return false;
    }
    @Override
    public Set<String> getSupportedAnnotationTypes() {
        //只支持 MyBindView 註解
        return Collections.singleton(MyBindView.class.getCanonicalName());
    }
}
複製代碼

測試配置

打開 Terminal,輸入./gradlew :app:compileDebugJava ,查看輸出有沒有配置成功!,有的話,就是配置成功了!!!

代碼生成

因爲用了新的庫,要多加一個依賴:

implementation 'com.squareup:javapoet:1.12.1'
複製代碼

大量代碼涌入,警報!警報!!

不過也不用擔憂,我基本都註釋過了,很容易能看懂,即便看不懂也不要緊,主要是知道整套流程和邏輯,到時須要真正去作的時候,再慢慢研究,畢竟作個 Demo 和作個上線項目,是兩回事。

@Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
        //獲取所有的類
        for (Element element : roundEnvironment.getRootElements()) {
            //獲取類的包名
            String packageStr = element.getEnclosingElement().toString();
            //獲取類的名字
            String classStr = element.getSimpleName().toString();
            //構建新的類的名字:原類名 + Binding
            ClassName className = ClassName.get(packageStr, classStr + "Binding");
            //構建新的類的構造方法
            MethodSpec.Builder constructorBuilder = MethodSpec.constructorBuilder()
                    .addModifiers(Modifier.PUBLIC)
                    .addParameter(ClassName.get(packageStr, classStr), "activity");
            //判斷是否要生成新的類,假如該類裏面 MyBindView 註解,那麼就不須要新生成
            boolean hasBuild = false;
            //獲取類的元素,例如類的成員變量、方法、內部類等
            for (Element enclosedElement : element.getEnclosedElements()) {
                //僅獲取成員變量
                if (enclosedElement.getKind() == ElementKind.FIELD) {
                    //判斷是否被 MyBindView 註解
                    MyBindView bindView = enclosedElement.getAnnotation(MyBindView.class);
                    if (bindView != null) {
                        //設置須要生成類
                        hasBuild = true;
                        //在構造方法中加入代碼
                        constructorBuilder.addStatement("activity.$N = activity.findViewById($L)",
                                enclosedElement.getSimpleName(), bindView.value());
                    }
                }
            }
            //是否須要生成
            if (hasBuild) {
                try {
                    //構建新的類
                    TypeSpec builtClass = TypeSpec.classBuilder(className)
                            .addModifiers(Modifier.PUBLIC)
                            .addMethod(constructorBuilder.build())
                            .build();
                    //生成 Java 文件
                    JavaFile.builder(packageStr, builtClass)
                            .build().writeTo(filer);
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
        return false;
    }
複製代碼

運行看了下,仍是沒有問題的,咱們看看 build 目錄有沒有文件生成:

完結撒花,✿✿ヽ(°▽°)ノ✿

em...忽然記起一件事,由於自動生成代碼時在編譯時間段執行的,因此註解在編譯後,也就是字節碼階段是不須要存在的,因此,將其改成保留在源碼階段便可。

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.SOURCE)
public @interface MyBindView {
    int value();
}
複製代碼

源碼地址:

github.com/bjsdm/MyBut…

相關文章
相關標籤/搜索