上篇文章說到,ButterKnife是如何通過bind()方法來實現繫結試圖、設定監聽的。這一節給大家繼續說說ButterKnife是如何生成ViewBinding的。
APT
類似於ButterKnife這種注入框架以前也有很多的,像xutils、afinal,包括Dagger(Dagger2重構前的版本),但是這些框架都是依賴於執行時註解和反射,嚴重影響效率。後來的框架(ButterKnife、Dagger2等)都是依賴於編譯時註解,被稱為APT(Android Processing Tool)技術。
APT的實現原理是這樣的:定義需要的註解類的@Retention為CLASS,定義自己的Procesor繼承自AbstractProcessor類,那麼在程式碼編譯時,編譯器會自動呼叫Processor類的process方法。
Element
在正式分析之前,先簡單瞭解一下Element類。
Elemet被稱為元素類,它有以下幾個常用的子類。
- VariableElement //一般代表成員變數
- ExecutableElement //一般代表類中的方法
- TypeElement //一般代表代表類
- PackageElement //一般代表Package
ButterKnifeProcessor
在ButterKnife中,類ButterKnifeProcessor繼承自AbstractProcessor,提供給我們使用的註解的@Retention都為CLASS。
找到ButterKnifeProcessor類,程式碼較多,我們需要關注的只有4個方法
- init() //初始化需要的資料
- getSupportedAnnotationTypes() //返回支援的註解型別
- getSupportedOptions //返回支援的原始碼版本
- process() //邏輯處理,重點關注
先來看看init()
@Override
public synchronized void init(ProcessingEnvironment env) {
super.init(env);
//獲取sdk版本
String sdk = env.getOptions().get(OPTION_SDK_INT);
this.sdk = Integer.parseInt(sdk);
//操作Element的工具類
elementUtils = env.getElementUtils();
//操作TypeMirror的工具類
typeUtils = env.getTypeUtils();
//用來建立輔助檔案的
filer = env.getFiler();
}複製程式碼
註釋已經解釋的很詳細了,init()方法就是用來建立一些輔助類。getSupportedAnnotationTypes() 和 getSupportedOptions中的程式碼也很簡單,這裡就不細看了。
接著就看看它的process方法。
@Override
public boolean process(Set<? extends TypeElement> elements, RoundEnvironment env) {
Map<TypeElement, BindingSet> bindingMap = findAndParseTargets(env);
for (Map.Entry<TypeElement, BindingSet> entry : bindingMap.entrySet()) {
TypeElement typeElement = entry.getKey();
BindingSet binding = entry.getValue();
JavaFile javaFile = binding.brewJava(sdk);
javaFile.writeTo(filer);
}
return false;
}複製程式碼
首先一進這個函式就執行findAndParseTargets(env),來看看都做了那些工作:
private Map<TypeElement, BindingSet> findAndParseTargets(RoundEnvironment env) {
Map<TypeElement, BindingSet.Builder> builderMap = new LinkedHashMap<>();
Set<TypeElement> erasedTargetNames = new LinkedHashSet<>();
scanForRClasses(env);
...
// Process each @BindView element.
for (Element element : env.getElementsAnnotatedWith(BindView.class)) {
parseBindView(element, builderMap, erasedTargetNames);
}
...
// Associate superclass binders with their subclass binders. This is a queue-based tree walk
// which starts at the roots (superclasses) and walks to the leafs (subclasses).
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 {
// Has a superclass binding but we haven't built it yet. Re-enqueue for later.
entries.addLast(entry);
}
}
}
return bindingMap;
}複製程式碼
這裡處理各個註解的程式碼較多,不一一貼出了,就以我們的例子中的BindView註解為例,其他的註解也是類似的。
上來先建立了兩個容器,builderMap和erasedTargetNames,然後開始遍歷所有被BindView註解的Element。(BindingSet類後文會有介紹)
在迴圈中,只呼叫了一個方法,parseBindView(element, builderMap, erasedTargetNames),注意這個方法並沒有返回值。
private void parseBindView(Element element,Map<TypeElement,BindingSet.Builder> builderMap,Set<TypeElement> erasedTargetNames){
//獲取外圍類元素
TypeElement enclosingElement = (TypeElement) element.getEnclosingElement();
//獲取檢視的ID
int id = element.getAnnotation(BindView.class).value();
//在builderMap中查詢BindingSet.Builder
BindingSet.Builder builder = builderMap.get(enclosingElement);
if (builder != null) {
//查詢此ID是否有對應的資料
String existingBindingName = builder.findExistingBindingName(getId(id));
//如果有就返回
if (existingBindingName != null) {
return;
}
}else{//建立一個BindingSet.Builder,並儲存到builderMap中
builder = getOrCreateBindingBuilder(builderMap,enclosingElement);
}
String name = element.getSimpleName().toString();
TypeName type = TypeName.get(elementType);
boolean required = isFieldRequired(element);
builder.addField(getId(id),new FieldViewBinding(name,type,required));
//Add the type-erased version to the valid binding targets set.
erasedTargetNames.add(enclosingElement);
}複製程式碼
先獲取外圍類元素,再獲取需要繫結的View的ID,接著是一些防止重複的判斷,這裡我們是第一次進入,肯定不會重複了。通過getOrCreateBindingBuilder(builderMap,enclosingElement)方法建立一個BindingSet.Builder,並儲存到builderMap中。最後再將一些資訊儲存到builder中去。
回過頭來接著看findAndParseTargets()方法,
Deque<Map.Entry<TypeElement, BindingSet.Builder>> entries = new ArrayDeque<>(builderMap.entrySet());
Map<TypeElement, BindingSet> bindingMap = new LinkedHashMap<>();複製程式碼
將builder的entrySet()儲存到一個佇列中。接著又建立一個Map
//遍歷佇列
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) {
//呼叫builder.build();
bindingMap.put(type, builder.build());
} else {
BindingSet parentBinding = bindingMap.get(parentType);
if (parentBinding != null) {
builder.setParent(parentBinding);
//呼叫builder.build();
bindingMap.put(type, builder.build());
} else {
// Has a superclass binding but we haven't built it yet. Re-enqueue for later.
entries.addLast(entry);
}
}
}複製程式碼
這一步主要是呼叫了builder.build()方法將builderMap資料儲存到bindingMap中,if-else判斷主要是為了提高健壯性,就不帶大家細看了。最後將bindingMap返回到process()方法中。
for (Map.Entry<TypeElement, BindingSet> entry : bindingMap.entrySet()) {
TypeElement typeElement = entry.getKey();
BindingSet binding = entry.getValue();
JavaFile javaFile = binding.brewJava(sdk);
javaFile.writeTo(filer);複製程式碼
這一步,遍歷取出相關資訊,並生成檔案(MainActivity_ViewBinding類),這裡使用的是JavaPoet庫去生成Java原始檔的,你當然也可以使用別的方式去生成。
關於BindingSet
有的小夥伴可能會糾結於BindingSet類到底是什麼,其實BindingSet類的作用很簡單,一是通過BindingSet.builder收集資訊,二是封裝JavaPoet的程式碼。這種收集資訊和生成Java原始碼的方式,在ButterKnife不同的版本中也是不一樣的,我們瞭解一下就可以了。
流程圖
通過下面的流程圖,加深理解一下生成MainActivity_ViewBinding的過程
ButterKnife執行的整個流程
到這裡,我們就可以很清楚的瞭解到ButterKnife實現的整個流程了。
- 在MainActivity中通過註解,標記button和onClick方法
- 編譯時執行ButterKnifePrcessor的Process方法,生成MainActivity_ViewBinding類
- 執行時,執行Bind()方法完成試圖和監聽的繫結
理清楚了,是不是很簡單。