說起 ButterKnife 相信大多人都知道這麼一個框架, 它是一個專注於 Android
系統的 View
注入框架, 簡化了我們的 findViewById, OnClick, getString()
以及加載動畫等操作, 給平時開發帶來了很大的便利. 只是現在這個框架的作者已經不再更新了, 只會修復一些關鍵性的 BUG
, 同時建議使用 Google
的 view binding 了. 但是作爲曾經最流行的框架之一, 還是很有必要學習和研究一下的.
衆所周知 ButterKnife
的便利來自於註解. 那麼既然存在註解, 註解處理器技術的使用是必然的. 現在的框架不同於以往.
以前的框架類似 XUtils
的註解很大程度上是使用反射來解析的, 反射帶來性能消耗還是有的.
但是現在, 大多數的註解框架都是基於 AnnotationProcessor
的編譯時解析實現的.
試想一下, 在程序編譯時就完成了註解解析的工作, 又會給性能帶來什麼影響呢?答案當然是沒影響。
(APT
已不再被作者所維護, 並且 Google
推出了AnnotationProcessor
來替代它,更是集成到了 API
中)
- 那麼什麼是
AnnotationProcessor
呢 ?
是一個JavaC
的工具,也就是Java
編譯源碼到字節碼的一個預編譯工具, 會在代碼編譯的時候調用到. 它有一個抽象類AbstractProcessor
, 只需實現該抽象類, 就可以在預編譯的時候被編譯器調用, 就可以在預編譯的時候完成一下你想完成工作. 比如代碼注入!!
那麼簡單來說 ButterKnife
在編譯時解析註解, 通過使用 AnnotationProcessor
代碼注入. 這是最基本最核心的思想. 那麼今天我們也來按照這個核心思想來寫一個山寨版的 ButterKnife
.
先來看一下, 我們最終需要自動生成的文件內容是什麼樣的.
public final class MainActivity_ViewBinding implements Unbinder {
private MainActivity target;
MainActivity_ViewBinding(MainActivity target) {
this.target = target;
target.tv_name = Utils.findViewById(target, 2131165425);
}
@Override
@CallSuper
public final void unbind() {
MainActivity target = this.target;
if (target == null) throw new IllegalStateException("Bindings already cleared. ");;
target.tv_name = null;
}
}
先來分析一波:
首先自動生成的類實現了 Unbinder
接口, 並且實現了 unbind
方法.
有一個有參的構造函數,參數爲 Activity
, 在構造函數內對傳入的 Activity
內的控件 ID , 進行 findById
操作. 有一個 Activity
類型的變量 target
.
OK, 現在就根據上面的這些代碼, 開始愉快的山寨吧.
先創建如下 Module
- 創建
App
名字爲butterknife-app
, - 創建
android Module
名字爲butterknife
. 爲 APP 提供butterknife
綁定操作. - 創建
java Module
名字爲butterknife-annotation
. 存放我們聲明的註解 - 創建
java Module
名字爲butterknife-compiler
. 作爲我們的註解處理器.
工程目錄截圖如下
爲 APP
添加依賴
implementation project(path: ':butterknife-annotations')
implementation project(path: ':butterknife'
annotationProcessor project(path: ':butterknife-compiler')
爲 butterknife-compiler
添加依賴
implementation 'com.squareup:javapoet:1.13.0'
annotationProcessor 'com.google.auto.service:auto-service:1.0-rc7'
implementation 'com.google.auto.service:auto-service:1.0-rc7'
implementation project(path: ':butterknife-annotations')
-
javapoet
是square
推出的開源java
代碼生成框架, 提供Java Api
生成.java
源文件. 這個框架功能非常有用, 我們可以很方便的使用它根據註解, 數據庫模式, 協議格式等來對應生成代碼. 通過這種自動化生成代碼的方式, 可以讓我們用更加簡潔優雅的方式要替代繁瑣冗雜的重複工作 -
auto-service 是
Google
爲我們提供用於java.util.ServiceLoader
樣式的服務提供者的配置/元數據生成器. 簡單來說就是會爲了加了@AutoService
註解的類, 自動裝載和實例化,並完成模塊的注入.
OK, 現在開始擼代碼.
1. 編寫註解
先到 butterknife-annotation module
中新建一個註解. em...既然山寨了, 那就連註解名字也一起山寨吧.
package com.butterknife_annotations;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.CLASS)
public @interface BindView {
int value();
}
原版中定義了很多註解, 什麼
BindView, BindFont, BindInt, BindString, BindColor ...
擴展了很多很多, 這裏我們就先寫一個簡單又山寨的BindView
好了.
2. 編寫接口與工具類
到 butterknife module
中新建
-
Unbinder
接口, 聲明一個unbind
解綁方法. 待會要讓我們自動生成的java
文件實現這個接口. -
Utils
工具類, 實現我們真正的findViewById
.在註解處理器中調用. -
ButterKnife
先空着. 最後再寫.
public interface Unbinder {
@UiThread
void unbind();
Unbinder EMPTY = new Unbinder() {
@Override
public void unbind() {
}
};
}
//在註解處理器重調用
public class Utils {
public static <T extends View> T findViewById(Activity activity, int id) {
return activity.findViewById(id);
}
}
3. 編寫註解處理器
接下來開始到 butterknife-compiler moudle
中寫我們的註解處理器.
新建一個 java
類 ButterKnifeProcessor
, 繼承自 AbstractProcessor
, 並重寫如下方法
@AutoService(Processor.class)
public class ButterKnifeProcessor extends AbstractProcessor {
@Override
public synchronized void init(ProcessingEnvironment processingEnv) {
super.init(processingEnv);
}
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
return false;
}
@Override
public SourceVersion getSupportedSourceVersion() {
return super.getSupportedSourceVersion();
}
@Override
public Set<String> getSupportedAnnotationTypes() {
return super.getSupportedAnnotationTypes();
}
}
-
init
方法主要做一些初始化的事情, 其中參數processingEnv
會爲我們提供很多有用的工具類- 例如等下需要用到的
Filer
, 它用來生成java
類文件. -
Elements
註解處理器運行掃描源文件時, 以獲取元素 (Element
)相關的信息.Element
有以下幾個子類:
包 (PackageElement
), 類 (TypeElement
), 成員變量 (VariableElement
), 方法 (ExecutableElement
)
- 例如等下需要用到的
-
getSupportedSourceVersion
方法 返回當前系統支持的java
版本 -
getSupportedAnnotationTypes
該方法返回一個Set<String>
, 代表ButterKnifeProcessor
要處理的註解類的名稱集合,即ButterKnife
支持的註解 -
process
敲黑板, 劃重點, 這個就是最重要的方法. 在這裏完成了目標類信息的收集並生成對應java
類
接着開始寫下面幾個簡單的.
//創建文件的時候需要用到
private Filer mFiler;
private Elements mElementUtils;
//打印輸出
private Messager mMessager;
@Override
public synchronized void init(ProcessingEnvironment processingEnv) {
super.init(processingEnv);
mFiler = processingEnv.getFiler();
mElementUtils = processingEnv.getElementUtils();
mMessager = processingEnv.getMessager();
}
//指定處理的版本
@Override
public SourceVersion getSupportedSourceVersion() {
return SourceVersion.latestSupported();
}
@Override
public Set<String> getSupportedAnnotationTypes() {
Set<String> types = new LinkedHashSet<>();
for (Class<? extends Annotation> annnotation : getSupportedAnnotation()) {
types.add(annnotation.getCanonicalName());
}
return types;
}
//添加所有我們需要處理的註解.
public Set<Class<? extends Annotation>> getSupportedAnnotation() {
Set<Class<? extends Annotation>> annotation = new LinkedHashSet<>();
annotation.add(BindView.class);
//原版本中會添加 N 多註解類到這裏
//例如 annotation.add(BindString.class)
return annotation;
}
接下來就是最關鍵的 process
方法了.
OK, 現在開始生成.
/**
* 獲取每個 Activity 內所有加了需要解析的註解的元素
* @param elements 我們所需要的註解集合, 因爲目前我們就一個註解, 所以這裏長度爲 1.
* @return key 是包含我們需要解析的註解所屬的 Activity, value 爲當前 Activit 內所有加了要解析的註解的元素.
*/
private Map<Element, List<Element>> getAllElements(Set<? extends Element> elements){
Map<Element, List<Element>> elementMap = new LinkedHashMap<>();
for (Element element : elements) {
//來自那個 Activity=
Element enclosingElement = element.getEnclosingElement();
//以 Activity 爲 Key, 先取一次,看 Map 中是否已存在
List<Element> viewBindElement = elementMap.get(enclosingElement);
if (viewBindElement == null) {
//沒有存在就重新創建
viewBindElement = new ArrayList<>();
//存入到 Map 中. key 爲 Activity 名字, value 爲 集合
elementMap.put(enclosingElement, viewBindElement);
}
//存到集合, 同時也會更新 Map 中對應的集合
viewBindElement.add(element);
}
return elementMap;
}
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
//獲取所有 Activity 中加了 bindView 註解的 元素, 需要整理爲一個 Activity 對應一個自己內部的元素集合
Set<? extends Element> elements = roundEnv.getElementsAnnotatedWith(BindView.class);
Map<Element, List<Element>> elementMap = getAllElements(elements);
return false;
}
通過 roundEnv.getElementsAnnotatedWith(BindView.class)
可以拿到所有加了 @BindView
註解的控件名字, 來自哪個 Activity
. 但是一個 Activity
中可能有很多很多加了註解的控件, 那麼我們需要整理成爲一個 Map
, 對每個 Activity
進行歸類. 因爲後面,我們需要爲每個 Activity
都生成一個文件.
那麼接下來就需要開始遍歷這個 Map
, 開始爲每個 Activity
都生成一個 java
文件. 有多少個 key
就生成多少個.
for (Map.Entry<Element, List<Element>> entry : elementMap.entrySet()) {
Element enclosingElement = entry.getKey();
List<Element> viewBindElements = entry.getValue();
//獲得類名
String activityClassNameStr = enclosingElement.getSimpleName().toString();
mMessager.printMessage(Diagnostic.Kind.NOTE, "------->" + activityClassNameStr);
//獲取我們要實現的接口.
ClassName unbinderClassName = ClassName.get("com.butterknife", "Unbinder");
//獲得對應類名的對象
ClassName activityClassName = ClassName.bestGuess(activityClassNameStr);
//開始生成類名, 繼承接口對象, 以及字段
//public final class MainActivity_ViewBinding implements Unbinder, 生成出來的樣子是這樣的
TypeSpec.Builder classBuilder = buildClass(activityClassNameStr, unbinderClassName, activityClassName);
//生成要實現的 unbinder 方法
MethodSpec.Builder unbinderMethodBuilder = buildMethod(activityClassName);
//生成構造函數
MethodSpec.Builder constructorMethodBuilder = buildConstructor(activityClassName,viewBindElements,unbinderMethodBuilder);
//將方法添加到類中
classBuilder.addMethod(unbinderMethodBuilder.build());
//將構造函數添加到類中
classBuilder.addMethod(constructorMethodBuilder.build());
//開始生成類文件
try {
String packageName = mElementUtils.getPackageOf(enclosingElement).getQualifiedName().toString();
mMessager.printMessage(Diagnostic.Kind.NOTE, "------->" + packageName);
JavaFile.builder(packageName, classBuilder.build())
.addFileComment("自動生成")
.build().writeTo(mFiler);
} catch (IOException e) {
e.printStackTrace();
}
}
過程比較簡單, 就是依次生成類, 方法, 如果需要構造函數的話, 也需要生成. 最後都添加到類的構造器中. 最後生成.
下面是三個生成的方法 buildClass , buildMethod, buildConstructor
.
private TypeSpec.Builder buildClass(String activityClassNameStr, ClassName unbinderClassName, ClassName activityClassName) {
return TypeSpec.classBuilder(activityClassNameStr + "_ViewBinding")
//生成類的訪問修飾符爲 public final
.addModifiers(Modifier.FINAL, Modifier.PUBLIC)
//生成的類實現接口
.addSuperinterface(unbinderClassName)
//添加字段
.addField(activityClassName, "target", Modifier.PRIVATE);
}
private MethodSpec.Builder buildMethod(ClassName activityClassName) {
//生成類實現類 unbinder 方法
ClassName callSuper = ClassName.get("androidx.annotation", "CallSuper");
return MethodSpec.methodBuilder("unbind")
.addModifiers(Modifier.FINAL, Modifier.PUBLIC)
.addAnnotation(Override.class)
.addAnnotation(callSuper)
.addStatement("$T target = this.target", activityClassName)
.addStatement("if (target == null) throw new IllegalStateException(\"Bindings already cleared. \");");
}
private MethodSpec.Builder buildConstructor(ClassName activityClassName, List<Element> viewBindElements, MethodSpec.Builder unbinderMethodBuilder) {
MethodSpec.Builder constructorMethodBuilder = MethodSpec.constructorBuilder()
.addParameter(activityClassName, "target")
.addStatement("this.target = target");
for (Element viewBindElement : viewBindElements) {
//獲得在 Activity 中聲明的控件名字
String filedName = viewBindElement.getSimpleName().toString();
//拿到工具類的對象
ClassName utilsClassName = ClassName.get("com.butterknife", "Utils");
//拿到在 Activity 中註解中傳入的參數 ID .
int resId = viewBindElement.getAnnotation(BindView.class).value();
constructorMethodBuilder.addStatement("target.$N = $T.findViewById(target, $L)", filedName, utilsClassName, resId);
//最終生成的結果如下
//target.tv_name = Utils.findViewByid(mainactivity, R.id.tv_name);
//在 unbind 方法中,將控件全賦值爲 Null
unbinderMethodBuilder.addStatement("target.$N = null", filedName);
}
return constructorMethodBuilder;
}
最後一步, Activity
在使用的時候需要進行綁定. 需要傳入當前 Activity
對象. 綁定的目的是什麼呢? 就是根據傳入的當前 Activity
然後調用生成文件的構造方法.
OK, 我們繼續到 butterknife module
中的 ButterKnife.java
添加方法 bind(Activity activity)
public class ButterKnife {
public static Unbinder bind(Activity activity) {
try {
//唯一需要的反射, 反射自動生成類的構造函數
Class<? extends Unbinder> bindClassName = (Class<? extends Unbinder>) Class.forName(activity.getClass().getName() + "_ViewBinding");
//調用自動生成類的構造函數
Constructor<? extends Unbinder> bindConstructor = bindClassName.getDeclaredConstructor(activity.getClass());
Unbinder unbinder = bindConstructor.newInstance(activity);
return unbinder;
} catch (Exception e) {
e.printStackTrace();
}
return Unbinder.EMPTY;
}
}
最後在項目中 Build --> Clean Project --> Make Project
, 在我們 APP
工程的目錄下就能看到自動生成的代碼文件了.
迫不及待的來使用一把
public class MainActivity extends AppCompatActivity {
@BindView(R.id.tv_name)
TextView mTvName;
private Unbinder mUnbinder;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mUnbinder = ButterKnife.bind(this);
mTvName.setText("666");
}
@Override
protected void onDestroy() {
super.onDestroy();
mUnbinder.unbind();
}
}
我這邊運行成功了, 你們呢?
OK, 到這裏, 是不是對整個流程有個大致的印象了呢.
- 我們在我們聲明的控件上添加註釋.
- 在
MainActivity
中調用ButterKnife.bind(this)
傳入當前Activity
. - 編譯的時候自動生成
MainActivity_ViewBinding
文件. - 運行的時候,執行
ButterKnife.bind(this)
, 在ButterKnife.bind()
方法中, 反射獲取到自動生成的MainActivity_ViewBinding
文件實例, 調用自動生成文件的構造方法. 在構造方法內執行findViewById
. 這樣就獲取到啦.
好了, 就先山寨到這裏吧. 其中還有一些沒弄的, 比如加註解的控件不能聲明爲 private
, 比如我們拿到的 ID 不是 R.id.xxx
, 而是一堆數字的樣子. 還有很多很多. 但是也算是基本完成了最核心最基礎的功能. 並且也算是弄清楚了基本流程.