前提
這篇文章呢主要講的是ButterKnife IOC框架背后的故事凡橱,雖然網(wǎng)上很多這樣的帖子,但是這篇細(xì)致到每個字段都會講解(version=8.5.1台汇,原理都一樣可能版本不同苛骨,有些內(nèi)部實行會有些不一樣)就當(dāng)埋點懸念吧。 @BindView一行代碼到底給我做了哪些事情苟呐。這個框架就是為了給我們省去每次的findViewById這一行讓你枯燥又乏味的代碼塊痒芝,到底他在后面都做了哪些故事呢!下面請聽我侃侃道來...
Annotation
哈哈哈哈上來就講原理牵素,不講原理那怎么才能知道背后的故事啊严衬,你說si不si啊笆呆!畢竟是個IOC框架 肯定要說到JAVA Annotation 這東西大家可以在日常的代碼塊經(jīng)惩剑看到的,這篇文章主要講的是ButterKnife背后的故事呢腰奋,我這里就不會詳細(xì)的解釋Annotation,只說這個框架中用的一些抱怔。如果想了解Annotation呢可以參考一下這篇文章:
傳送門Annotation
不論看沒看這篇文章劣坊,我先說一下怎么自定義注解,了解各基本的大概就可以看懂這篇文章了屈留。
元注解(Retention局冰,Target)
@Rentention
這個注解的意思是注解保留的時間,我們可以有以下三個選擇
1.SOURCE
源碼時保留灌危,這類 Annotation 大都用來校驗康二,比如 Override, Deprecated, SuppressWarnings
2.CLASS
肯定意思是編譯時,就是我們在項目java文件在編譯成class 的時候 apt 會自動解析 但需要做的是
- 自定義類繼承AbstractProcessor
- 重寫其中的process
函數(shù)
這塊可能會有同學(xué)不理解勇蝙,實際是由apt在編譯時自動查找所有繼承來自AbstractProcessor
的類沫勿,然后調(diào)用他們的process 方法去處理(我們這里的ButterKnife在這里就自定義了一個ButterKnifeProcessor
后面會詳細(xì)講解這個類)
3.RUNTIME
運行時保留,程序在運行過程中,使用這些 Annotation, 比如我們常用的 @Test产雹。
@Target
表示注解可以用來修飾哪些元素诫惭。可選值包括 TYPE, METHOD, CONSTRUCTOR, FIELD, PARAMETER 等
ButterKnifeProcessor
由于我們的大神JakeWharton 每一個注解都是ClASS,所有java文件在編譯的時候ButterKnifeProcessor
的process
就會被調(diào)用蔓挖。好現(xiàn)在我們開始解析源碼夕土。
@Override public boolean process(Set<? extends TypeElement> elements, RoundEnvironment env) {
//這一行是根據(jù)env拿到所有帶有相關(guān)注解根據(jù)TypeElement進(jìn)行區(qū)分
Map<TypeElement, BindingSet> bindingMap = findAndParseTargets(env);
//依次遍歷生成相應(yīng)的xxx_ViewBinding文件
for (Map.Entry<TypeElement, BindingSet> entry : bindingMap.entrySet()) {
TypeElement typeElement = entry.getKey();
BindingSet binding = entry.getValue();
JavaFile javaFile = binding.brewJava(sdk);
try {
javaFile.writeTo(filer);
} catch (IOException e) {
error(typeElement, "Unable to write binding for type %s: %s", typeElement, e.getMessage());
}
}
return false;
}
上面的代碼呢也不是太長,首選我們可以看到第一行創(chuàng)建了一個Map集合存放的key = TypeElement 而TypeElement
是由RoundEnvironment
通過
TypeElement enclosingElement = (TypeElement) element.getEnclosingElement();
如果不太明白Elements的意思
作用:Elements是處理Element的工具類,Element代表程序的元素瘟判,例如包怨绣、類或者方法,可以理解成源代碼;TypeElement代表的是源代碼中的類型元素拷获,例如類篮撑、域、方法等;從TypeElement中能獲取類的名字刀诬,但是你獲取不到類的信息咽扇,例如它的父類,這個需要從TypeMirror獲取陕壹,而TypeMirror需要調(diào)用Element的asType()函數(shù)
value = BindingSet 這個類意思是什么呢质欲。我們來看看源碼啊 下面貼出的是BindingSet
他的Builder
static final class Builder {
private final TypeName targetTypeName;
private final ClassName bindingClassName;
private final boolean isFinal;
private final boolean isView;
private final boolean isActivity;
private final boolean isDialog;
private BindingSet parentBinding;
//存儲(@BindView(id))這個id的
private final Map<Id, ViewBinding.Builder> viewIdMap = new LinkedHashMap<>();
private final ImmutableList.Builder<FieldCollectionViewBinding> collectionBindings =
ImmutableList.builder();
private final ImmutableList.Builder<ResourceBinding> resourceBindings = ImmutableList.builder();
private Builder(TypeName targetTypeName, ClassName bindingClassName, boolean isFinal,
boolean isView, boolean isActivity, boolean isDialog) {
this.targetTypeName = targetTypeName;
this.bindingClassName = bindingClassName;
this.isFinal = isFinal;
this.isView = isView;
this.isActivity = isActivity;
this.isDialog = isDialog;
}
為什么貼出他的Builder呢,因為這樣更容易理解這個類干嘛的糠馆,他是保存一個類(當(dāng)前的Activity)里面到底有哪些關(guān)于ButterKnife的注解嘶伟。上面的viewIdMap就是用于存儲(@BindView(id))這個id的,我們在看看Builder這個內(nèi)部類的一些方法可能你會更理解他到底在做哪些事情
//用于@BindView(R.id.test)
void addField(Id id, FieldViewBinding binding) {
getOrCreateViewBindings(id).setFieldBinding(binding);
}
void addFieldCollection(FieldCollectionViewBinding binding) {
collectionBindings.add(binding);
}
//方法的bind
boolean addMethod(
Id id,
ListenerClass listener,
ListenerMethod method,
MethodViewBinding binding) {
ViewBinding.Builder viewBinding = getOrCreateViewBindings(id);
if (viewBinding.hasMethodBinding(listener, method) && !"void".equals(method.returnType())) {
return false;
}
viewBinding.addMethodBinding(listener, method, binding);
return true;
}
//用于@BindBitmap @BindDimen...就是一些資源文件的bind
void addResource(ResourceBinding binding) {
resourceBindings.add(binding);
}
從上面的代碼可以看到這個類BuilderSet
到底干了些什么事吧又碌,就是把你添加注釋的這個類的信息保存下來九昧,后面做判斷,做代碼的生成毕匀。
說了這么多其實就是解釋process()
第一行Map代碼到底是做什么的铸鹰,接下來我們看process()
里面的循環(huán)到底干什么的。上面的代碼塊我也寫了一些注釋皂岔,說是生成對應(yīng)的xxx_ViewBinding
文件的蹋笼。如何生成的呢?細(xì)心的同學(xué)會注意到那個里面的filer
這個東西躁垛,其實這個是在我們初始化的時候的一些工具,下面是ButterKnife初始化的的一些操作
@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) {
env.getMessager()
.printMessage(Kind.WARNING, "Unable to parse supplied minSdk option '"
+ sdk
+ "'. Falling back to API 1 support.");
}
}
//scan java文件每一個Element
elementUtils = env.getElementUtils();
//是用來處理TypeMirror的工具類
typeUtils = env.getTypeUtils();
//用來創(chuàng)建生成輔助文件
filer = env.getFiler();
try {
trees = Trees.instance(processingEnv);
} catch (IllegalArgumentException ignored) {
}
}
就是一些初始化操作剖毯。主要就elementUtils,typeUtils教馆,filer這個三個工具的初始化逊谋,具體干嘛的上面代碼我已經(jīng)寫了注釋了。
這個先告一段落(具體如何生成的我后面會講到)土铺。我們知道在java 文件編譯的時候ButterKnifeProcessor
靠著process()
這個方法生成了隊友的xxx_ViewBinding
文件胶滋。那么問題來了板鬓,我們?nèi)绾伟堰@個文件和我們的添加了注解的文件(xxxActivity.java,后面就用xx代替了)綁定在一起呢。
如何綁定xxx_ViewBinding
相信大家用過BindKnife的人都知道镀钓,要在我們的BaseActivity里面或者當(dāng)前的Activity中bind(setContentView或者OnViewCreated之后做這個操作) 和 unBind一下穗熬。這個就是關(guān)鍵。這里就拿@BindView
做列舉丁溅。廢話不多說上代碼
@NonNull @UiThread
public static Unbinder bind(@NonNull Activity target) {
//獲取最外層View
View sourceView = target.getWindow().getDecorView();
return createBinding(target, sourceView);
}
下面的是上方代碼createBinding的具體實現(xiàn)
private static Unbinder createBinding(@NonNull Object target, @NonNull View source) {
//獲取當(dāng)前這個類
Class<?> targetClass = target.getClass();
if (debug) Log.d(TAG, "Looking up binding for " + targetClass.getName());
//通過這個類然后找到對于的xxx_ViewBinding文件的構(gòu)造方法
Constructor<? extends Unbinder> constructor = findBindingConstructorForClass(targetClass);
if (constructor == null) {
return Unbinder.EMPTY;
}
//noinspection TryWithIdenticalCatches Resolves to API 19+ only type.
try {
//初始化這個xxx_ViewBinding文件
return constructor.newInstance(target, source);
} catch (IllegalAccessException e) {
....
}
}
上面的代碼我已經(jīng)寫了注釋了唤蔗,可以看到最主要的代碼是findBindingConstructorForClass
這個方法找到我們的這個當(dāng)前的這個Activity對于的xxx_ViewBinding 然后獲取他的構(gòu)造方法,然后初始窟赏,那我們進(jìn)入這個方法看看到底做了哪些操作妓柜。
@Nullable @CheckResult @UiThread
private static Constructor<? extends Unbinder> findBindingConstructorForClass(Class<?> cls) {
//從集合中獲取這個xxx_ViewBinding的構(gòu)造函數(shù)(這個map用于緩存用下次就不需要下面的操作來獲取了)
Constructor<? extends Unbinder> bindingCtor = BINDINGS.get(cls);
if (bindingCtor != null) {
if (debug) Log.d(TAG, "HIT: Cached in binding map.");
return bindingCtor;
}
//獲取clsName
String clsName = cls.getName();
//過濾不需要的
if (clsName.startsWith("android.") || clsName.startsWith("java.")) {
if (debug) Log.d(TAG, "MISS: Reached framework class. Abandoning search.");
return null;
}
try {
//通過反射獲取這個xxx_ViewBinding的class然后獲取他的構(gòu)造函數(shù)
//細(xì)心的同學(xué)可以看到這里面接受了兩個參數(shù),一個是這個cls的父類和當(dāng)前最外層的view
Class<?> bindingClass = Class.forName(clsName + "_ViewBinding");
//noinspection unchecked
bindingCtor = (Constructor<? extends Unbinder>) bindingClass.getConstructor(cls, View.class);
if (debug) Log.d(TAG, "HIT: Loaded binding class and constructor.");
} catch (ClassNotFoundException e) {
if (debug) Log.d(TAG, "Not found. Trying superclass " + cls.getSuperclass().getName());
bindingCtor = findBindingConstructorForClass(cls.getSuperclass());
} catch (NoSuchMethodException e) {
throw new RuntimeException("Unable to find binding constructor for " + clsName, e);
}
//上面如果沒有從map集合中獲取涯穷,通過反射回去的會添加到集合中方便下次直接獲取棍掐。就是緩存的意思
BINDINGS.put(cls, bindingCtor);
return bindingCtor;
}
上面的代碼看到了嗎?每行的注釋都有拷况,可以看到他是通過反射的方式拿到這個xxx_ViewBinding文件然后獲取構(gòu)造他的構(gòu)造方法的作煌。然后通過BINDINGS這個集合來做緩存,減少耗時操作畢竟用反射都很耗時的赚瘦。
接下來我們來看看生成的到底是一個什么樣的文件xxx_ViewBinding
public class CameraActivityRep_ViewBinding implements Unbinder {
private CameraActivityRep target;
@UiThread
public CameraActivityRep_ViewBinding(CameraActivityRep target) {
this(target, target.getWindow().getDecorView());
}
@UiThread
public CameraActivityRep_ViewBinding(CameraActivityRep target, View source) {
this.target = target;
//就是findviewById
target.modelPanorama = Utils.findRequiredViewAsType(source, R.id.model_panorama, "field 'modelPanorama'", ImageView.class);
target.modelCapture = Utils.findRequiredViewAsType(source, R.id.model_capture, "field 'modelCapture'", ImageView.class);
}
@Override
@CallSuper
public void unbind() {
CameraActivityRep target = this.target;
if (target == null) throw new IllegalStateException("Bindings already cleared.");
this.target = null;
target.modelPanorama = null;
target.modelCapture = null;
}
看到這里我們終于看到了我們的findViewById在哪里了在他的生成文件的構(gòu)造函數(shù)中進(jìn)行的findViewById粟誓,ButterKnife.bind(this);這個的作用就是findViewById的作用,通過bind的方法獲取生成的xxx_ViewBinding文件起意,然后通過反射獲取構(gòu)造函數(shù)鹰服,到構(gòu)造函數(shù)的初始化。在構(gòu)造函數(shù)里面做了findViewById的操作揽咕。
其實大伙可能說我明明沒看到findViewById就看到了Utils.findRequiredViewAsType(source, R.id.model_panorama, "field 'modelPanorama'", ImageView.class)
這行代碼悲酷,好我們接下來繼續(xù)看這個utils到底干了啥是不是findViewById
public static <T> T findRequiredViewAsType(View source, @IdRes int id, String who,
Class<T> cls) {
//MD我咋還沒看到呢繼續(xù)往下看
View view = findRequiredView(source, id, who);
return castView(view, id, who, cls);
}
MD我咋還沒看到呢繼續(xù)往下看
public static View findRequiredView(View source, @IdRes int id, String who) {
//看到了嗎 看到了吧
View view = source.findViewById(id);
if (view != null) {
return view;
}
String name = getResourceEntryName(source, id);
throw new IllegalStateException("Required view '"
+ name
+ "' with ID "
+ id
+ " for "
+ who
+ " was not found. If this view is optional add '@Nullable' (fields) or '@Optional'"
+ " (methods) annotation.");
}
好了同學(xué)們知道了吧,小伙子隱藏的可真深啊亲善。
mdzz 一句findViewById 引發(fā)了這么多東西 這就是@BindView背后不可告知的秘密有興趣的同學(xué)可以接著往下讀设易,看看他是如何生成xxx_ViewBinding的
如何生成xxx_ViewBinding
我們繼續(xù)回講一下剛剛的ButterKnifeProcessor那個process()
這個方法不知道還記不記得里面的代碼我們就在貼一遍吧
@Override public boolean process(Set<? extends TypeElement> elements, RoundEnvironment env) {
//這一行是根據(jù)env拿到所有帶有相關(guān)注解根據(jù)TypeElement進(jìn)行區(qū)分
Map<TypeElement, BindingSet> bindingMap = findAndParseTargets(env);
//依次遍歷生成相應(yīng)的xxx_ViewBinding文件
for (Map.Entry<TypeElement, BindingSet> entry : bindingMap.entrySet()) {
TypeElement typeElement = entry.getKey();
BindingSet binding = entry.getValue();
JavaFile javaFile = binding.brewJava(sdk);
try {
javaFile.writeTo(filer);
} catch (IOException e) {
error(typeElement, "Unable to write binding for type %s: %s", typeElement, e.getMessage());
}
}
return false;
}
由于上面已經(jīng)講了這個方法里面的一些參數(shù)東西,我這里就不重復(fù)了蛹头,之說一些關(guān)鍵點
1.獲取帶有注解的所有Element然后把每個TypeElement對應(yīng)的BindingSet一一對應(yīng)存儲在Map中
findAndParseTargets(env)
2.生成對應(yīng)得xxx_ViewBinding文件
JavaFile javaFile = binding.brewJava(sdk);
javaFile.writeTo(filer);
我們可以看到這兩點亡嫌,我們先說第一個吧。既然是方法掘而,肯定要往方法里面走了,看看源碼在做一些什么東西于购。
private Map<TypeElement, BindingSet> findAndParseTargets(RoundEnvironment env) {
Map<TypeElement, BindingSet.Builder> builderMap = new LinkedHashMap<>();
Set<TypeElement> erasedTargetNames = new LinkedHashSet<>();
scanForRClasses(env);
......
// 找到每個帶有 @BindView element 添加到集合中.
for (Element element : env.getElementsAnnotatedWith(BindView.class)) {
// we don't SuperficialValidation.validateElement(element)
// so that an unresolved View type can be generated by later processing rounds
try {
parseBindView(element, builderMap, erasedTargetNames);
} catch (Exception e) {
logParsingError(element, BindView.class, e);
}
.......
// 就是把一個<TypeElement, BindingSet.Builder> -> TypeElement, BindingSet
//其實就是把一個activity的所有帶有@bind的注解存在在BindingSet中
//然后返回給process()加工成文件
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注解講吧,其他的都類似的肋僧。把其他的都刪了不然代碼實在太長斑胜,我們可以看到上面的代碼通過env.getElementsAnnotatedWith(BindView.class)
找到帶有@BindView的element然后遍歷循環(huán)控淡,然后接下來他通過一個方法parseBindView
把這些Element做了一個些整理就是把一個Acitvity里面的所有注解對應(yīng)起來。我們來看看到底做了那些事情
private void parseBindView(Element element, Map<TypeElement, BindingSet.Builder> builderMap,
Set<TypeElement> erasedTargetNames) {
//這句代碼的意思就是獲取拿到一個標(biāo)識(xxxAcitivty的意思)
TypeElement enclosingElement = (TypeElement) element.getEnclosingElement();
....
// 獲取@BindView(R.id.test)獲取這個id的
int id = element.getAnnotation(BindView.class).value();
//拿到這個標(biāo)識對應(yīng)的BindingSet止潘,在BindingSet里面有個map存這個act里面有多少@bindview注解
BindingSet.Builder builder = builderMap.get(enclosingElement);
if (builder != null) {
String existingBindingName = builder.findExistingBindingName(getId(id));
//如果發(fā)現(xiàn)這個id已經(jīng)存進(jìn)去了掺炭,直接return
if (existingBindingName != null) {
error(element, "Attempt to use @%s for an already bound ID %d on '%s'. (%s.%s)",
BindView.class.getSimpleName(), id, existingBindingName,
enclosingElement.getQualifiedName(), element.getSimpleName());
return;
}
} else {
//發(fā)現(xiàn)buildSet 的map中并沒有存這個id 那我們就把他添加進(jìn)去
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);
}
上面解釋也寫了很多,我這邊就大概的講一下這個方法干嘛的凭戴,首先呢我們拿到傳進(jìn)來的builderMap涧狮,這個map對應(yīng)的是key = TypeElement(相當(dāng)于當(dāng)前Act的一個標(biāo)識) value = BindingSet.Builder(存儲著這個Act里面的所有注解),然后我們根據(jù)傳進(jìn)來的element 到map中查找看看這個Element對應(yīng)的BuildSet里面是否包含這個id么夫,如果包含了直接返回者冤,沒有的話拿到這個Element對應(yīng)的BuildSet 往里面添加這個id(通過 builder.addField)
這個就是BindView干的一些事情。這里就在總結(jié)一下上面的東西
- 每一個TypeElement相當(dāng)于一個(Activity档痪,fragment涉枫,dialog)
- 每一個BindingSet存儲了TypeElement里面所有包含注解的信息
這里就告一段落了,那我們看看代碼的生成腐螟,拿到BindSet生成對于的xxx_ViewBinding文件
binding.brewJava(sdk).writeTo(filer)
JavaFile brewJava(int sdk) {
return JavaFile.builder(bindingClassName.packageName(), createType(sdk))
//顧名思義添加注釋的意思
.addFileComment("Generated code from Butter Knife. Do not modify!")
.build();
}
public void writeTo(Filer filer) throws IOException {
String fileName = packageName.isEmpty()
? typeSpec.name
: packageName + "." + typeSpec.name;
List<Element> originatingElements = typeSpec.originatingElements;
JavaFileObject filerSourceFile = filer.createSourceFile(fileName,
originatingElements.toArray(new Element[originatingElements.size()]));
try (Writer writer = filerSourceFile.openWriter()) {
writeTo(writer);
} catch (Exception e) {
try {
filerSourceFile.delete();
} catch (Exception ignored) {
}
throw e;
}
}
這里面代碼也挺多了我就不一一進(jìn)去講解了愿汰,這里用的是javapoet來進(jìn)行代碼的寫入的,感興趣的同學(xué)可以看一看多藝技不壓身乐纸。
ending
臥槽寫完了咋感覺頭懵懵的衬廷,但是還是希望這篇文章帶給你的是知識的提升而不是時間的浪費(畢竟寫了幾小時呢)