butterknife注解框架相信很多同學都在用象颖,但是你真的了解它的實現(xiàn)原理嗎佩厚?那么今天讓我們來看看它到底是怎么實現(xiàn)的(注:本文是基于butterknife:8.5.1版本進行分析)。
前言
先來看看一些預備知識
java注解
java有三類注解说订,通過元注解@Retention來標識:
RetentionPolicy.SOURCE:源碼級別解析,例如@Override,@SupportWarnngs,這里注解在編譯成功后就不會再起作用潮瓶,并且不會出現(xiàn)在.class中陶冷。
RetentionPolicy.CLASS:編譯時解析,默認的解析方式毯辅,會保留在最終的class中埂伦,但無法再運行時獲取。
RetentionPolicy.RUNTIME:運行時注解思恐,會保留在最終的class中沾谜,這類注解可以用反射API中getAnnotations()獲取到。
編譯時注解
編譯時注解是注解強大的地方之一胀莹,你可以用它來幫你生成java代碼去處理一些邏輯基跑,避免了用反射解析所帶來的性能的開銷。ButterKnife的核心思想正是用編譯時注解描焰。
ButterKnife解析
從Bind開始
眾所周知媳否,使用butterknife時首先需要在Activity#onCreate方法中添加這么一句代碼
ButterKnife.bind(this);
那么我們就從此方法入手,以下是Butterknife.bind()的源碼:
@NonNull @UiThread
public static Unbinder bind(@NonNull Activity target) {
View sourceView = target.getWindow().getDecorView();
return createBinding(target, sourceView);
}
private static Unbinder createBinding(@NonNull Object target, @NonNull View source) {
Class<?> targetClass = target.getClass();
Constructor<? extends Unbinder> constructor = findBindingConstructorForClass(targetClass);
try {
return constructor.newInstance(target, source);
} catch (IllegalAccessException e) {
//...... 省略
}
}
@Nullable @CheckResult @UiThread
private static Constructor<? extends Unbinder> findBindingConstructorForClass(Class<?> cls) {
Constructor<? extends Unbinder> bindingCtor = BINDINGS.get(cls);
if (bindingCtor != null) {
return bindingCtor;
}
String clsName = cls.getName();
//...... 省略
try {
Class<?> bindingClass = Class.forName(clsName + "_ViewBinding");
bindingCtor = (Constructor<? extends Unbinder>) bindingClass.getConstructor(cls, View.class);
} catch (ClassNotFoundException e) {
//...... 省略
}
BINDINGS.put(cls, bindingCtor);
return bindingCtor;
}
首先,bind方法中調(diào)用了createBinding(...)方法篱竭,而在createBinding()方法中得到了一個Unbinder的構造器力图,并實例化此類〔舯疲可以發(fā)現(xiàn)這個Unbinder的構造器是通過findBindingConstructorForClass這個方法獲取的吃媒,那就進入此方法中瞧瞧÷来可以看到此方法中首先從BINDINGS中獲取unbinder類構造器赘那,如果有的話直接返回,沒有則通過反射得到此類構造器兽泄,先在BINDINGS中存放一份漓概,然后在將其返回。其實這里BINDINGS的作用是緩存通過反射得到的類病梢。避免多次通過反射獲取所帶來的性能開銷胃珍。
bind方法暫時就分析到這里,其實要是查看ButterKnife這個類會發(fā)現(xiàn)蜓陌,bind方法有很多重載方法觅彰,其中有針對View的,有針對Dialog的等等作用都一樣钮热,就是要利用傳入的target來實例化一個類名為target_ViewBinding類的對象填抬。那么target_ViewBinding這個類是從哪里來的呢?作用有是什么呢隧期?
@BindView(id)注解
使用butterknife的好處是不用程序員手動findViewById去找對應的View飒责,而只需增加一個@BindView(id)即可,那么butterknife是怎么通過@BindView注解來將對應的View初始化的呢?
讓我們先來看看此注解的代碼:
@Retention(CLASS) @Target(FIELD)
public @interface BindView {
/** View ID to which the field will be bound. */
@IdRes int value();
}
通過以上代碼可以看到仆潮,BindView注解是一個編譯時注解(@Retention(CLASS))策彤,并且只能作用在屬性上(@Target(FIELD))拢驾。
如果你對java注解有一定的了解彩届,那你就一定知道混埠,編譯時注解最終都是有AbstractProcessor的子類來處理的,那我們就找到對應的類為ButterKnifeProcessor類鹏浅。
先來看看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);
try {
javaFile.writeTo(filer);
} catch (IOException e) {
error(typeElement, "Unable to write binding for type %s: %s", typeElement, e.getMessage());
}
}
return false;
}
這個方法有兩個作用嗅义,第一是解析所有注解findAndParseTargets(env),第二是生成java代碼隐砸。
- 解析所有注解findAndParseTargets(env)
private Map<TypeElement, BindingSet> findAndParseTargets(RoundEnvironment env) {
Map<TypeElement, BindingSet.Builder> builderMap = new LinkedHashMap<>();
Set<TypeElement> erasedTargetNames = new LinkedHashSet<>();
//省略代碼...
// Process each @BindView element.
for (Element element : env.getElementsAnnotatedWith(BindView.class)) {
try {
parseBindView(element, builderMap, erasedTargetNames);
} catch (Exception e) {
logParsingError(element, BindView.class, e);
}
}
//省略代碼...
return bindingMap;
}
private void parseBindView(Element element, Map<TypeElement, BindingSet.Builder> builderMap,
Set<TypeElement> erasedTargetNames) {
TypeElement enclosingElement = (TypeElement) element.getEnclosingElement();
//省略代碼 驗證 1之碗、被注解的屬性不能是private或static。2凰萨、驗證被注解的屬性必須是View的子類
// Assemble information on the field.
int id = element.getAnnotation(BindView.class).value();
BindingSet.Builder builder = builderMap.get(enclosingElement);
if (builder != null) {
String existingBindingName = builder.findExistingBindingName(getId(id));
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 {
builder = getOrCreateBindingBuilder(builderMap, enclosingElement);
}
String name = element.getSimpleName().toString();
TypeName type = TypeName.get(elementType);
boolean required = isFieldRequired(element);
//將被注解的屬性封裝成FieldViewBinding存儲在buildSet中
builder.addField(getId(id), new FieldViewBinding(name, type, required));
// 將綁定變量所在的類添加到待unBind序列中继控。
erasedTargetNames.add(enclosingElement);
}
這部分代碼很容易懂械馆,解析所有被@BindView所注解的Element(肯定是Field),并處理它武通。具體解析代碼在parseBindVIew方法中霹崎。首先校驗屬性的合法性:
- 被注解的屬性不能是private或static
- 被注解的屬性必須是View的子類
- 屬性與id必須一一對應
其次是將屬性封裝成FieldViewBinding對象,存儲在BindingSet中冶忱。那么BindingSet又是個什么玩意呢尾菇?
static final class Builder {
private final TypeName targetTypeName;
private final ClassName bindingClassName;
private BindingSet parentBinding;
private final Map<Id, ViewBinding.Builder> viewIdMap = new LinkedHashMap<>();
//省略代碼
void addField(Id id, FieldViewBinding binding) {
getOrCreateViewBindings(id).setFieldBinding(binding);
}
private ViewBinding.Builder getOrCreateViewBindings(Id id) {
ViewBinding.Builder viewId = viewIdMap.get(id);
if (viewId == null) {
viewId = new ViewBinding.Builder(id);
viewIdMap.put(id, viewId);
}
return viewId;
}
BindingSet build() {
//省略代碼
return new BindingSet(targetTypeName, bindingClassName, isFinal, isView, isActivity, isDialog,
viewBindings.build(), collectionBindings.build(), resourceBindings.build(),
parentBinding);
}
}
這里貼出來了BindingSet的構造器Builder,可以看出此類包裝了被注解的類的信息以及注解的所有的屬性囚枪,viewIdMap中存儲了所有的注解字段派诬,這些字段都被封裝成ViewBinding(其實就是將id和屬性名包裝成一個對象)。最后由BindingSet負責生成java代碼链沼。
static Builder newBuilder(TypeElement enclosingElement) {
//省略代碼
String packageName = getPackage(enclosingElement).getQualifiedName().toString();
String className = enclosingElement.getQualifiedName().toString().substring(
packageName.length() + 1).replace('.', '$');
ClassName bindingClassName = ClassName.get(packageName, className + "_ViewBinding");
boolean isFinal = enclosingElement.getModifiers().contains(Modifier.FINAL);
return new Builder(targetType, bindingClassName, isFinal, isView, isActivity, isDialog);
}
可以看到構造Builder時會傳入包名默赂,類名,而這個類名 className_ViewBinding就是最終被生成的類的名稱括勺。還記得在bind方法中會通過反射去實例化一個target_ViewBinding類的對象吧缆八,這個類其實就是這里通過注解來自動生成的。到這里butterknife的原理就分析完了疾捍,下面我們來看看如何生成java代碼奈辰。
2.使用javapoet生成java代碼
再次介紹一個大殺器 javapoet,square出品的生成.java文件的Java API乱豆。通過清晰的語法來生成一個文件結構奖恰,無敵。
那么我們回到process方法中查看宛裕。
JavaFile javaFile = binding.brewJava(sdk);
javaFile.writeTo(filer);
這里主要邏輯在binding.brewJava()方法中瑟啃。
JavaFile brewJava(int sdk) {
return JavaFile.builder(bindingClassName.packageName(), createType(sdk))
.addFileComment("Generated code from Butter Knife. Do not modify!")
.build();
}
這里builder的參數(shù)第一個是包名。第二個是TypeSpec對象揩尸,就是要生成的類的信息翰守。
private TypeSpec createType(int sdk) {
//獲取一個builder對象,將類的修飾符設置為public疲酌,如果允許是final時,設置為final
TypeSpec.Builder result = TypeSpec.classBuilder(bindingClassName.simpleName())
.addModifiers(PUBLIC);
if (isFinal) {
result.addModifiers(FINAL);
}
......
//添加屬性
if (hasTargetField()) {
result.addField(targetTypeName, "target", PRIVATE);
}
//添加構造器
if (isView) {
result.addMethod(createBindingConstructorForView());
} else if (isActivity) {
result.addMethod(createBindingConstructorForActivity());
} else if (isDialog) {
result.addMethod(createBindingConstructorForDialog());
}
if (!constructorNeedsView()) {
// Add a delegating constructor with a target type + view signature for reflective use.
result.addMethod(createBindingViewDelegateConstructor());
}
result.addMethod(createBindingConstructor(sdk));
//添加一個unbind方法
if (hasViewBindings() || parentBinding == null) {
result.addMethod(createBindingUnbindMethod(result));
}
return result.build();
}
在這個方法中可以看到最終生成的類的相關信息了袁,最主要的給屬性初始化的代碼是在createBindingConstructor(sdk) 這個方法中朗恳。
private MethodSpec createBindingConstructor(int sdk) {
MethodSpec.Builder constructor = MethodSpec.constructorBuilder()
.addAnnotation(UI_THREAD)
.addModifiers(PUBLIC);
......
//綁定View屬性
if (hasViewBindings()) {
......
for (ViewBinding binding : viewBindings) {
addViewBinding(constructor, binding);
}
......
}
......
return constructor.build();
}
private void addViewBinding(MethodSpec.Builder result, ViewBinding binding) {
if (binding.isSingleFieldBinding()) {
// Optimize the common case where there's a single binding directly to a field.
FieldViewBinding fieldBinding = binding.getFieldBinding();
CodeBlock.Builder builder = CodeBlock.builder()
.add("target.$L = ", fieldBinding.getName());
boolean requiresCast = requiresCast(fieldBinding.getType());
if (!requiresCast && !fieldBinding.isRequired()) {
builder.add("source.findViewById($L)", binding.getId().code);
} else {
builder.add("$T.find", UTILS);
builder.add(fieldBinding.isRequired() ? "RequiredView" : "OptionalView");
if (requiresCast) {
builder.add("AsType");
}
builder.add("(source, $L", binding.getId().code);
if (fieldBinding.isRequired() || requiresCast) {
builder.add(", $S", asHumanDescription(singletonList(fieldBinding)));
}
if (requiresCast) {
builder.add(", $T.class", fieldBinding.getRawType());
}
builder.add(")");
}
result.addStatement("$L", builder.build());
return;
}
......
addFieldBinding(result, binding);
addMethodBindings(result, binding);
}
這兩個方法主要就是生成 通過findViewById方法去找到對應的View 的代碼≡芈蹋可以看到注解庫最終還是調(diào)用的findViewById來查找View的粥诫。
到這里ButterKnife注解庫就分析完了。下面貼一段最終生成的代碼瞧瞧:
public class MainActivity_ViewBinding implements Unbinder {
private MainActivity target;
private View view2131427417;
@UiThread
public MainActivity_ViewBinding(MainActivity target) {
this(target, target.getWindow().getDecorView());
}
@UiThread
public MainActivity_ViewBinding(final MainActivity target, View source) {
this.target = target;
View view;
target.txt1 = Utils.findRequiredViewAsType(source, R.id.id1, "field 'txt1'", TextView.class);
view = Utils.findRequiredView(source, R.id.btn, "method 'start'");
view2131427417 = view;
view.setOnClickListener(new DebouncingOnClickListener() {
@Override
public void doClick(View p0) {
target.start();
}
});
}
@Override
@CallSuper
public void unbind() {
MainActivity target = this.target;
if (target == null) throw new IllegalStateException("Bindings already cleared.");
this.target = null;
target.txt1 = null;
view2131427417.setOnClickListener(null);
view2131427417 = null;
}
}
總結:首先通過注解來獲取對應類中所有要初始化的屬性崭庸,通過processor編譯生成對應的.java的類文件怀浆。這個類文件中會在構造器中通過調(diào)用View的findViewById方法去初始化所有的屬性(注意:到這里只是生成了.java類文件谊囚,并沒有和目標類綁定)。這個編譯時生成的類的構造器中需要傳入對應的目標類作為參數(shù)执赡。因此在目標類初始化時需要調(diào)用ButterKnife.bind(this)方法類進行綁定镰踏。在這個bind方法中會通過反射得到編譯時才生成的類的對象,這樣就和目標類進行了綁定沙合,也就是初始化目標類中的被注解的屬性奠伪。