如題所示,這篇文章主要講解手把手教學寫個簡易版 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
可是不一樣於註釋在字節碼階段就被擦除,註解能夠選擇保留到什麼階段,和附帶了其它功能:
根據上面的說明,咱們就能夠完善下註解。
由於咱們的註解在運行階段要用到,而且只能註解成員變量,因此,能夠寫成這樣:
@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() 方法裏面寫控件的綁定代碼。
首先,咱們先理清邏輯,只要邏輯沒問題,代碼實現就不在話下:
具體代碼以下:
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 以前,咱們先想下,如何才能作到批量對 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 是一種處理註解的工具,可以在源代碼中查找出註解,而且根據註解自動生成代碼。
首先,咱們要新建一個 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.java
和MyBindView.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();
}
複製代碼
源碼地址: