本文主要介紹Android之神JakeWharton的一個注解框架,聽說現在面試官現在面試都會問知不知道JakeWharton,不知道的直接略過。廢話不多說,我們接下來就簡單介紹下ButterKnife的實現,希望對大家有所幫助,還有一點,以下故事純屬虛構,如有雷同,純屬巧合。
目錄
- ButterKnife的起源和工作流程
- ButterKnife模塊
- ButterKnife處理器模塊
- 總結
ButterKnife的起源和工作流程
起源
1.我們平常一般都是在xml文件中寫好布局,然后在Activity中通過下面代碼查找和處理控件
public class MainActivity extends Activity implements View.OnClickListener {
private TextView mTextview;
private Button mButton;
private ImageView mImageview;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
initView();
}
private void initView() {
mTextview = (TextView) findViewById(R.id.textview);
mButton = (Button) findViewById(R.id.button);
mImageview = (ImageView) findViewById(R.id.imageview);
mButton.setOnClickListener(this);
}
@Override
public void onClick(View v) {
switch (v.getId()) {
case R.id.button:
Toast.makeText(this, "羅馬不是一天建成的", Toast.LENGTH_SHORT).show();
break;
}
}
}
2.顯然這些findViewById和setOnClickListener很是礙眼,于是有人想是不是可以把findViewById和setOnClickListener這些操作放外邊兒呢,這樣在MainActivity里邊只需要聲明變量和回調方法,我們就可以只關注我們需要關注的,于是乎就有了如下代碼
public class MainActivity extends Activity {
TextView mTextview;
Button mButton;
ImageView mImageview;
private TestMainActivity_ViewBinding mBind;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mBind = TestButterKnife.bind(this);
}
public void onClick() {
Toast.makeText(this, "羅馬不是一天建成的", Toast.LENGTH_SHORT).show();
}
@Override
protected void onDestroy() {
super.onDestroy();
mBind.unbind();
}
}
public class TestButterKnife {
public static TestMainActivity_ViewBinding bind(Activity target) {
return new TestMainActivity_ViewBinding((MainActivity) target);
}
}
public class TestMainActivity_ViewBinding {
private MainActivity target;
public TestMainActivity_ViewBinding(MainActivity target) {
this(target, target.getWindow().getDecorView());
}
public TestMainActivity_ViewBinding(final MainActivity target, View source) {
this.target = target;
target.mTextview = (TextView) source.findViewById(R.id.textview);
target.mImageview = (ImageView) source.findViewById(R.id.imageview);
target.mButton = (Button) source.findViewById(R.id.button);
target.mButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
target.onClick();
}
});
}
public void unbind() {
MainActivity target = this.target;
if (target == null) throw new IllegalStateException("Bindings already cleared.");
this.target = null;
target.mTextview = null;
target.mImageview = null;
target.mButton.setOnClickListener(null);
target.mButton = null;
}
}
3.看到上面的代碼一定有很多人都想吐,我也想說me too,這樣豈不是要碼很多代碼,還要建好多類,有點得不償失的趕腳。這時候Android之神JakeWharton站出來了,發表了一席講話,大體內容是說可以寫個小框架,讓用戶在MainActivity里邊只需要寫點注解,其它的事情交給框架來做,也不需要建那么類,碼很多代碼,于是就有了如下設計
public class MainActivity extends Activity {
@BindView(R.id.textview)
TextView mTextview;
@BindView(R.id.button)
Button mButton;
@BindView(R.id.imageview)
ImageView mImageview;
private Unbinder mBind;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mBind = ButterKnife.bind(this);
}
@OnClick(R.id.button)
public void onClick() {
Toast.makeText(this, "羅馬不是一天建成的", Toast.LENGTH_SHORT).show();
}
@Override
protected void onDestroy() {
super.onDestroy();
mBind.unbind();
}
}
工作流程
看完上面的設計,肯定有人就會想,這樣的話需要使用運行時注解實現吧,在ButterKnife的bind(this)方法執行的時候通過反射獲取MainActivity中所有的帶有注解的屬性并且獲得注解中的R.id.xxx值,最后還是通過反射調用Activity.findViewById()方法來獲取View,并賦值給Activity中的某個屬性。可是這樣做不會有問題嗎,我們都知道在Activity運行時大量使用反射會影響App的運行性能,造成卡頓等一系列問題。其實這種問題JakeWharton早就想到了,于是乎選用了編譯時注解來實現,也就是Java Annotation Processing技術。下面開始簡單介紹下ButterKnife的工作流程,該庫主要使用編譯時注解和javapoet在編譯時生成輔助類,在運行時通過反射調用輔助類的構造和方法來實現。
1.編譯時ButterKnifeProcessor類的process()方法處理過程如下
- 獲取所有的標記了@BindView、@OnClick等注解的元素
- 定義一個key為類級別的元素,value為注解信息的Map集合,根據獲取的元素可以拿到當前類級別的元素和注解標記的信息,然后put進去,這樣Map里邊就保存了所有要生成類和類中的信息
- 根據Map集合通過 javapoet 生成類似MainActivity_ViewBinding這樣的很多類,包括包名,導包,類名,字段,構造,方法等信息都會生成,可見這個庫的強大,這種庫最難的應該就是導包了吧
2.運行時
- ButterKnife.bind(this);這個方法會反射創建并返回MainActivity_ViewBinding這樣的實例,MainActivity_ViewBinding的構造就會執行,構造中會進行findViewById和setOnClickListener這些操作,這與起源中第2個模塊里邊的TestMainActivity_ViewBinding代碼很相似
- 當然通過ButterKnife.bind(this);的返回值,你可以在onDestroy()中調用MainActivity_ViewBinding的unbind()方法來解除綁定釋放資源
ButterKnife模塊
這個模塊主要有三個東東,一個是ButterKnife類,一個Utils類,一個是Unbinder接口。ButterKnife主要是用于運行時反射調用輔助類和返回輔助類實例,Utils類會被生成輔助類調用,Unbinder是生成的輔助類需要實現的接口,下面依次來介紹
ButterKnife類
1.mBind = ButterKnife.bind(this),會走如下代碼
//UiThread表明這個方法必須在UI線程調用
@NonNull @UiThread
public static Unbinder bind(@NonNull Activity target) {
View sourceView = target.getWindow().getDecorView();
return createBinding(target, sourceView);
}
2.接著調用createBinding(target, sourceView)
private static Unbinder createBinding(@NonNull Object target, @NonNull View source) {
Class<?> targetClass = target.getClass();
if (debug) Log.d(TAG, "Looking up binding for " + targetClass.getName());
//主要調用如下方法,該方法會返回輔助類的構造器
Constructor<? extends Unbinder> constructor = findBindingConstructorForClass(targetClass);
if (constructor == null) {
return Unbinder.EMPTY;
}
//省去了try-catch,這里創建輔助類的實例并返回,注意這里會調用輔助類兩個參數的構造方法
return constructor.newInstance(target, source);
}
3.下面我們看findBindingConstructorForClass(targetClass),這里采用了享元模式的設計,這種設計在很對框架中都會有,這里維護的是一個Map集合,集合中有這個key對應的value直接返回,沒有才會構建,構建完后會放入Map中,這樣大大減少了內存中對象的數量
//BINDINGS是一個key為Class,value為構造器的Map
static final Map<Class<?>, Constructor<? extends Unbinder>> BINDINGS = new LinkedHashMap<>();
private static Constructor<? extends Unbinder> findBindingConstructorForClass(Class<?> cls) {
//集合中通過key來獲取構造器
Constructor<? extends Unbinder> bindingCtor = BINDINGS.get(cls);
if (bindingCtor != null) {
if (debug) Log.d(TAG, "HIT: Cached in binding map.");
//構造器不為空,說明之前創建過就會直接返回
return bindingCtor;
}
String clsName = cls.getName();
//類名不能以android.和java.開頭
if (clsName.startsWith("android.") || clsName.startsWith("java.")) {
if (debug) Log.d(TAG, "MISS: Reached framework class. Abandoning search.");
return null;
}
try {
//可以看到生成的輔助類的類名是原始類名加上_ViewBinding,比如MainActivity_ViewBinding
Class<?> bindingClass = cls.getClassLoader().loadClass(clsName + "_ViewBinding");
//創建構造器
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;
}
Utils類
這里簡單介紹幾個方法,后面生成的輔助類會調用這個類中方法
1.findRequiredViewAsType()
public static <T> T findRequiredViewAsType(View source, @IdRes int id,String who,Class<T> cls) {
//這里會調用下面2中的方法拿到一個沒有強轉的View
View view = findRequiredView(source, id, who);
//會調用3中的方法根據返回一個強制后的T,T表示@BindView所修飾控件類型,比如TextView、Button等
return castView(view, id, who, cls);
}
2.findRequiredView(View source, @IdRes int id, String who)
public static View findRequiredView(View source, @IdRes int id, String who) {
//我們期望看到的findViewById在這里
View view = source.findViewById(id);
if (view != null) {
return view;
}
//省略異常處理
}
3.castView(View view, @IdRes int id, String who, Class<T> cls)
public static <T> T castView(View view, @IdRes int id, String who, Class<T> cls) {
//省略try-catch,這里調用Class類的cast方法進行強轉,我們稍微看下這個方法
return cls.cast(view);
}
//Class類中的cast方法
public T cast(Object obj) {
if (obj != null && !isInstance(obj))
throw new ClassCastException(cannotCastMsg(obj));
//這里強轉了下然后返回
return (T) obj;
}
Unbinder接口
這個接口中就一個方法和一個空實現的成員變量
public interface Unbinder {
@UiThread void unbind();
//這個成員變量會在構造器為null的異常情況下直接返回,可以在上面ButterKnife類中的createBinding方法中看到
Unbinder EMPTY = new Unbinder() {
@Override public void unbind() {}
};
}
ButterKnife處理器模塊
這個模塊中我們只關注ButterKnifeProcessor這個類,該類繼承了AbstractProcessor抽象類,下面我們看它重寫的幾個方法
1.首先我們看@AutoService注解,這是一個其他注解處理器中引入的注解。它是Google開發的,用來生成META-INF/services/javax.annotation.processing.Processor文件的,我們直接用即可
@AutoService(Processor.class)
public final class ButterKnifeProcessor extends AbstractProcessor {
//省略
}
2.接著我們看init(ProcessingEnvironment env),注解處理器工具APT會調用這個方法并傳入ProcessingEnvironment ,我理解的是該方法是提供給我們用來訪問APT的環境
private int sdk = 1;
private static final String OPTION_SDK_INT = "butterknife.minSdk";
private static final String OPTION_DEBUGGABLE = "butterknife.debuggable";
private boolean debuggable = true;
private Elements elementUtils;
private Types typeUtils;
private Filer filer;
private Trees trees;
@Override
public synchronized void init(ProcessingEnvironment env) {
super.init(env);
//env.getOptions()拿到的是傳遞給注釋處理工具的Map選項
String sdk = env.getOptions().get(OPTION_SDK_INT);
if (sdk != null) {
//省去try-catch
this.sdk = Integer.parseInt(sdk);
}
debuggable = !"false".equals(env.getOptions().get(OPTION_DEBUGGABLE));
//獲取元素實用工具
elementUtils = env.getElementUtils();
//獲取類型實用工具
typeUtils = env.getTypeUtils();
//獲取用來創建文件的Filter
filer = env.getFiler();
try {
trees = Trees.instance(processingEnv);
} catch (IllegalArgumentException ignored) {
}
}
3.我們看getSupportedSourceVersion(),這個方法用來指定你使用的Java版本
@Override
public SourceVersion getSupportedSourceVersion() {
//通常這樣返回就行了,它會根據你的環境返回對應的版本枚舉
return SourceVersion.latestSupported();
}
4.接下來就是getSupportedAnnotationTypes()方法,指定支持的注解
@Override
public Set<String> getSupportedAnnotationTypes() {
Set<String> types = new LinkedHashSet<>();
for (Class<? extends Annotation> annotation : getSupportedAnnotations()) {
//annotation.getCanonicalName()為獲取全類名,即包名+類名
types.add(annotation.getCanonicalName());
}
return types;
}
private Set<Class<? extends Annotation>> getSupportedAnnotations() {
Set<Class<? extends Annotation>> annotations = new LinkedHashSet<>();
annotations.add(BindAnim.class);
annotations.add(BindArray.class);
annotations.add(BindBitmap.class);
annotations.add(BindBool.class);
annotations.add(BindColor.class);
annotations.add(BindDimen.class);
annotations.add(BindDrawable.class);
annotations.add(BindFloat.class);
annotations.add(BindFont.class);
annotations.add(BindInt.class);
annotations.add(BindString.class);
annotations.add(BindView.class);
annotations.add(BindViews.class);
annotations.addAll(LISTENERS);
return annotations;
}
5.最后來詳細介紹下process(),這個方法相當于每個處理器的main()函數,在這里可以寫你的掃描、處理注解以及生成Java文件的代碼,因此這個方法是整個處理器模塊的關鍵
@Override
public boolean process(Set<? extends TypeElement> elements, RoundEnvironment env) {
//根據env獲取一個Map集合,其中key為類級別的元素,value為BindingSet
//BindingSet是一個自定義類,其中含有當前類中所有的注解信息
Map<TypeElement, BindingSet> bindingMap = findAndParseTargets(env);
for (Map.Entry<TypeElement, BindingSet> entry : bindingMap.entrySet()) {
TypeElement typeElement = entry.getKey();
BindingSet binding = entry.getValue();
//注意下面的兩行代碼就是生成輔助類啦,由于需要用到JavaPoet庫,后面會詳細講解
JavaFile javaFile = binding.brewJava(sdk, debuggable);
try {
javaFile.writeTo(filer);
} catch (IOException e) {
error(typeElement, "Unable to write binding for type %s: %s", typeElement, e.getMessage());
}
}
return false;
}
首先我們來看findAndParseTargets(env)是如何構建Map集合的
private Map<TypeElement, BindingSet> findAndParseTargets(RoundEnvironment env) {
//key為類級別的元素,value為BindingSet的內部類Builder,看到Builder我們應該都很敏感,這是典型的
//建造者模式,這種模式特別常見,不管是在第三方庫OkHttp中,還是在Android提供的對話框API中都可以看見
//它的身影,對于設計模式我們這里不再詳細展開
Map<TypeElement, BindingSet.Builder> builderMap = new LinkedHashMap<>();
//Set集合中保存了所有類級別的元素
Set<TypeElement> erasedTargetNames = new LinkedHashSet<>();
scanForRClasses(env);
//由于定義的注解很多,這里省略了其他注解,只關注@BindView
for (Element element : env.getElementsAnnotatedWith(BindView.class)) {
try {
//解析@BindView注解到buildMap中
parseBindView(element, builderMap, erasedTargetNames);
} catch (Exception e) {
logParsingError(element, BindView.class, e);
}
}
//builderMap是key為類級別的元素,value為BindingSet的內部類Builder,這里會轉為bindingMap,即
//把value通過builder()方法構建成BindingSet,這就是典型的建造者模式
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();
//注意這是在while循環里邊,這個方法中會根據當前類級別的元素查找父元素,如果沒有就返回null,如果
//有父元素就返回父元素,從這里可以看出來我們的注解是支持繼承的
TypeElement parentType = findParentType(type, erasedTargetNames);
if (parentType == null) {
//父元素為null,直接put當前元素
bindingMap.put(type, builder.build());
} else {
//從bindingMap中先獲取父元素的BindingSet
BindingSet parentBinding = bindingMap.get(parentType);
if (parentBinding != null) {
//父元素的BindingSet不為null,就需要為當前元素設置它的父元素
builder.setParent(parentBinding);
//然后put到bindingMap中
bindingMap.put(type, builder.build());
} else {
//父元素的BindingSet是null,就先把當前的Entry放到隊尾,繼續下一次循環
entries.addLast(entry);
}
}
}
//返回key為類級別的元素,value為BindingSet的集合
return bindingMap;
}
- 接下來我們看上面方法中的parseBindView()和findParentType(),我們先看findParentType()是如何根據當前類級別的元素查找父元素
private TypeElement findParentType(TypeElement typeElement, Set<TypeElement> parents) {
TypeMirror type;
while (true) {
//根據當前類級別的元素查找父元素
type = typeElement.getSuperclass();
if (type.getKind() == TypeKind.NONE) {
//父元素為null,直接返回null
return null;
}
//parents集合中保存的是包含有我們自定義注解的類級別的元素,找到
//父元素并且父元素是個有效的即在集合里,就返回父元素
typeElement = (TypeElement) ((DeclaredType) type).asElement();
if (parents.contains(typeElement)) {
return typeElement;
}
}
}
- 再先看parseBindView()是如何解析@BindView注解到buildMap中的
private void parseBindView(Element element, Map<TypeElement, BindingSet.Builder> builderMap,
Set<TypeElement> erasedTargetNames) {
//獲取當前元素的類級別的元素
TypeElement enclosingElement = (TypeElement) element.getEnclosingElement();
//生成的代碼是不是可以訪問,由于我們是通過target.mTextview來訪問的,所以這個方法會校驗@BindView修飾的
//字段是不是private、static的,@BindView是不是在類里邊,如果在類里邊那么類是不是private的
boolean hasError = isInaccessibleViaGeneratedCode(BindView.class, "fields", element)
|| isBindingInWrongPackage(BindView.class, element);
//下面幾行代碼主要是校驗@BindView修飾的字段類型,必須是一個View或者是一個接口,否則就毫無意義
//獲取@BindView修飾的字段類型,比如TextView類型
TypeMirror elementType = element.asType();
if (elementType.getKind() == TypeKind.TYPEVAR) {
TypeVariable typeVariable = (TypeVariable) elementType;
elementType = typeVariable.getUpperBound();
}
Name qualifiedName = enclosingElement.getQualifiedName();
Name simpleName = element.getSimpleName();
//校驗修飾的類型是不是View的子類或接口,否則結束
if (!isSubtypeOfType(elementType, VIEW_TYPE) && !isInterface(elementType)) {
if (elementType.getKind() == TypeKind.ERROR) {
note(element, "@%s field with unresolved type (%s) "
+ "must elsewhere be generated as a View or interface. (%s.%s)",
BindView.class.getSimpleName(), elementType, qualifiedName, simpleName);
} else {
error(element, "@%s fields must extend from View or be an interface. (%s.%s)",
BindView.class.getSimpleName(), qualifiedName, simpleName);
hasError = true;
}
}
if (hasError) {
return;
}
//獲取@BindView的值,即R.id.xxx
int id = element.getAnnotation(BindView.class).value();
//享元模式的設計,根據當前元素的類級別的元素先從Map中獲取BindingSet的內部類Builder
BindingSet.Builder builder = builderMap.get(enclosingElement);
//把int類型的id包裝成了QualifiedId類
QualifiedId qualifiedId = elementToQualifiedId(element, id);
if (builder != null) {
//getId(qualifiedId)把QualifiedId類中的id又封裝成了Id類,校驗Id類是否已經存在
String existingBindingName = builder.findExistingBindingName(getId(qualifiedId));
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為空,說明當前類還沒有對應的value,需要new一個出來,并放到builderMap中
builder = getOrCreateBindingBuilder(builderMap, enclosingElement);
}
//@BindView修飾的字段的簡單名稱,即變量名mTextView
String name = simpleName.toString();
//@BindView修飾的字段的類型,比如TextView
TypeName type = TypeName.get(elementType);
//是否必須是字段
boolean required = isFieldRequired(element);
//調用BindingSet的內部類Builder中的addField方法封裝解析的信息
builder.addField(getId(qualifiedId), new FieldViewBinding(name, type, required));
//給所有含有自定義注解的類組成的Set集合中添加元素
erasedTargetNames.add(enclosingElement);
}
private BindingSet.Builder getOrCreateBindingBuilder(
Map<TypeElement, BindingSet.Builder> builderMap, TypeElement enclosingElement) {
BindingSet.Builder builder = builderMap.get(enclosingElement);
if (builder == null) {
//builder為null,就構建一個BindingSet.Builder,放入集合中,并返回
builder = BindingSet.newBuilder(enclosingElement);
builderMap.put(enclosingElement, builder);
}
return builder;
}
static Builder newBuilder(TypeElement enclosingElement) {
TypeMirror typeMirror = enclosingElement.asType();
//類級別的元素是不是View的子類
boolean isView = isSubtypeOfType(typeMirror, VIEW_TYPE);
//類級別的元素是不是Activity的子類
boolean isActivity = isSubtypeOfType(typeMirror, ACTIVITY_TYPE);
//類級別的元素是不是Dialog的子類
boolean isDialog = isSubtypeOfType(typeMirror, DIALOG_TYPE);
TypeName targetType = TypeName.get(typeMirror);
if (targetType instanceof ParameterizedTypeName) {
targetType = ((ParameterizedTypeName) targetType).rawType;
}
//根據當前類級別的元素獲取包名
String packageName = getPackage(enclosingElement).getQualifiedName().toString();
//substring截取后一般是MainActivity這樣的字符串,然后replace的話會沒有效果,如果有.則用$代替
String className = enclosingElement.getQualifiedName().toString().substring(
packageName.length() + 1).replace('.', '$');
//這里可以看到要生成的包名,要生成的類名,格式如MainActivity_ViewBinding
ClassName bindingClassName = ClassName.get(packageName, className + "_ViewBinding");
//當前類是不是被final修飾
boolean isFinal = enclosingElement.getModifiers().contains(Modifier.FINAL);
//直接通過構造new一個Builder,第一個參數表示是不是一個class,第二個參數中含有要生成的類名和包名,其它都是布爾類型的參數
return new Builder(targetType, bindingClassName, isFinal, isView, isActivity, isDialog);
}
總結下,在parseBindView()方法中解析出來的信息都放在BindingSet的內部類Builder中,包含的信息有包名、原始類名、要生成的類名、R.id.xxx、是不是字段、變量名mTextView、變量類型TextView等信息。而BindingSet的內部類Builder又會通過調用build()方法構建一個BindSet實例并把這些信息傳遞過去,這就是典型的建造者模式,在BindSet中有了這些信息,下面我們就可以生成輔助類啦。
最后我們看binding.brewJava(sdk, debuggable)是如何生成輔助類的
1.回到最初的process()方法,我們講完了如何構建Map集合,接著我們講解如何生成輔助類
@Override
public boolean process(Set<? extends TypeElement> elements, RoundEnvironment env) {
//根據env獲取一個Map集合,其中key為類級別的元素,value為BindingSet
//BindingSet是一個自定義類,其中含有當前類中所有的注解信息
Map<TypeElement, BindingSet> bindingMap = findAndParseTargets(env);
for (Map.Entry<TypeElement, BindingSet> entry : bindingMap.entrySet()) {
TypeElement typeElement = entry.getKey();
BindingSet binding = entry.getValue();
//注意下面的兩行代碼就是生成輔助類啦,由于需要用到JavaPoet庫,后面會詳細講解
JavaFile javaFile = binding.brewJava(sdk, debuggable);
try {
javaFile.writeTo(filer);
} catch (IOException e) {
error(typeElement, "Unable to write binding for type %s: %s", typeElement, e.getMessage());
}
}
return false;
}
2.需要用到javapoet,因此打開這個庫我們稍微看下它是如何使用的
- 比如我們想要生成一個普通的java類,其中有main方法,方法中有syso
package com.example.helloworld;
public final class HelloWorld {
public static void main(String[] args) {
System.out.println("Hello, JavaPoet!");
}
}
- 那我們就可以調用下面的方法來實現,可以看到又是建造者builder模式,真是無處不見啊,呼哈
//方法級別的,定義方法名
MethodSpec main = MethodSpec.methodBuilder("main")
//定義修飾符
.addModifiers(Modifier.PUBLIC, Modifier.STATIC)
//定義返回值
.returns(void.class)
//定義方法參數
.addParameter(String[].class, "args")
//定義方法中的syso
.addStatement("$T.out.println($S)", System.class, "Hello, JavaPoet!")
.build();
//類級別的,定義類名
TypeSpec helloWorld = TypeSpec.classBuilder("HelloWorld")
//添加修飾符
.addModifiers(Modifier.PUBLIC, Modifier.FINAL)
//把上面定義的main添加到類中
.addMethod(main)
.build();
//JavaFile中保存了所有的信息,包名和上面定義的類信息,方法信息
JavaFile javaFile = JavaFile.builder("com.example.helloworld", helloWorld)
.build();
javaFile.writeTo(System.out);
3.簡單看了javapoet的使用,繼續看我們的代碼,而我們知道生成輔助類的所有信息都在JavaFile中,我們看JavaFile是如何生成的,也就是上面的brewJava()
JavaFile javaFile = binding.brewJava(sdk, debuggable);
JavaFile brewJava(int sdk, boolean debuggable) {
//bindingClassName是ClassName類型,它是對包名和類名的封裝,這里可以看到獲取包名,類和類里邊的信息是
//通過createType()方法獲取的,也就是TypeSpec
return JavaFile.builder(bindingClassName.packageName(), createType(sdk, debuggable))
.addFileComment("Generated code from Butter Knife. Do not modify!")
.build();
}
private TypeSpec createType(int sdk, boolean debuggable) {
//類級別的,定義類名,bindingClassName是ClassName類型,它是對包名和類名的封裝
//上面獲取的是包名,這里獲取的是輔助類的類名
TypeSpec.Builder result = TypeSpec.classBuilder(bindingClassName.simpleName())
.addModifiers(PUBLIC);
if (isFinal) {
result.addModifiers(FINAL);
}
//判斷有沒有父類,即生成的輔助類是不是也要從父類繼承
if (parentBinding != null) {
//有父類,那么生成的輔助類也要從父類繼承
result.superclass(parentBinding.bindingClassName);
} else {
//沒有父類,就讓輔助類實現Unbinder
result.addSuperinterface(UNBINDER);
}
//設置類似這樣的字段private MainActivity target;
if (hasTargetField()) {
result.addField(targetTypeName, "target", PRIVATE);
}
//判斷是不是View、Activity、Dialog的子類,來生成不同構造
if (isView) {
result.addMethod(createBindingConstructorForView());
} else if (isActivity) {
//這里我們只關注這個構造的生成,這個方法用來生成一個參數MainActivity target的構造
result.addMethod(createBindingConstructorForActivity());
} else if (isDialog) {
result.addMethod(createBindingConstructorForDialog());
}
if (!constructorNeedsView()) {
result.addMethod(createBindingViewDelegateConstructor());
}
//前面是只有一個參數MainActivity target構造
//這里是兩個參數final MainActivity target, View source的構造
result.addMethod(createBindingConstructor(sdk, debuggable));
if (hasViewBindings() || parentBinding == null) {
//沒有父類,那么我們前面實現了Unbinder接口,需要實現它里面的unbind方法
result.addMethod(createBindingUnbindMethod(result));
}
return result.build();
}
4.上面方法中調用的三個方法我們還沒看,其中有一個參數的構造、兩個參數的構造、unbind()方法,接下來我們挨著看
- createBindingConstructorForActivity()生成一個參數MainActivity target的構造
private MethodSpec createBindingConstructorForActivity() {
MethodSpec.Builder builder = MethodSpec.constructorBuilder()
//添加@UiThread注解
.addAnnotation(UI_THREAD)
.addModifiers(PUBLIC)
//添加構造中的參數,如MainActivity target
.addParameter(targetTypeName, "target");
if (constructorNeedsView()) {
builder.addStatement("this(target, target.getWindow().getDecorView())");
} else {
//這里是一個參數的構造不需要View,構造中添加this(target, target)
builder.addStatement("this(target, target)");
}
return builder.build();
}
- createBindingConstructor(sdk, debuggable)生成兩個參數final MainActivity target, View source的構造
private MethodSpec createBindingConstructor(int sdk, boolean debuggable) {
MethodSpec.Builder constructor = MethodSpec.constructorBuilder()
//添加@UiThread注解
.addAnnotation(UI_THREAD)
.addModifiers(PUBLIC);
//判斷是否有方法綁定,即是否有onClick()這樣的回調方法
if (hasMethodBindings()) {
//由于是在匿名內部類方法里邊調用構造函數的第一個參數,需要加上final修飾
constructor.addParameter(targetTypeName, "target", FINAL);
} else {
//沒有方法綁定,只需要添加構造函數中的第一個參數MainActivity target
constructor.addParameter(targetTypeName, "target");
}
if (constructorNeedsView()) {
//構造函數的第二個參數就是View source
constructor.addParameter(VIEW, "source");
} else {
constructor.addParameter(CONTEXT, "context");
}
//有一些警告的話添加java提供的@SuppressWarnings注解
if (hasUnqualifiedResourceBindings()) {
constructor.addAnnotation(AnnotationSpec.builder(SuppressWarnings.class)
.addMember("value", "$S", "ResourceType")
.build());
}
if (hasOnTouchMethodBindings()) {
constructor.addAnnotation(AnnotationSpec.builder(SUPPRESS_LINT)
.addMember("value", "$S", "ClickableViewAccessibility")
.build());
}
//有父類的情況下,需要根據情況添加如下代碼
if (parentBinding != null) {
if (parentBinding.constructorNeedsView()) {
constructor.addStatement("super(target, source)");
} else if (constructorNeedsView()) {
constructor.addStatement("super(target, source.getContext())");
} else {
constructor.addStatement("super(target, context)");
}
constructor.addCode("\n");
}
if (hasTargetField()) {
//會執行這里,添加this.target = target,并且換行
constructor.addStatement("this.target = target");
constructor.addCode("\n");
}
//判斷是不是有View綁定,肯定有@BindView
if (hasViewBindings()) {
if (hasViewLocal()) {
//在構造函數中定義一個局部變量View view,用來view.setOnClickListener()
constructor.addStatement("$T view", VIEW);
}
for (ViewBinding binding : viewBindings) {
//這是在for循環里面,這個方法會間接調用findViewById和強轉的操作,接下來主要看這個方法
addViewBinding(constructor, binding, debuggable);
}
for (FieldCollectionViewBinding binding : collectionBindings) {
constructor.addStatement("$L", binding.render(debuggable));
}
//資源綁定不為空,才會換行
if (!resourceBindings.isEmpty()) {
constructor.addCode("\n");
}
}
//資源的綁定不為空才會執行
if (!resourceBindings.isEmpty()) {
if (constructorNeedsView()) {
constructor.addStatement("$T context = source.getContext()", CONTEXT);
}
if (hasResourceBindingsNeedingResource(sdk)) {
constructor.addStatement("$T res = context.getResources()", RESOURCES);
}
for (ResourceBinding binding : resourceBindings) {
constructor.addStatement("$L", binding.render(sdk));
}
}
return constructor.build();
}
private void addViewBinding(MethodSpec.Builder result, ViewBinding binding, boolean debuggable) {
if (binding.isSingleFieldBinding()) {
FieldViewBinding fieldBinding = binding.getFieldBinding();
//先寫target.mTextview = ,這里可以看到是直接通過.調用的,因此我們寫的字段不能是private的
CodeBlock.Builder builder = CodeBlock.builder()
.add("target.$L = ", fieldBinding.getName());
boolean requiresCast = requiresCast(fieldBinding.getType());
if (!debuggable || (!requiresCast && !fieldBinding.isRequired())) {
builder.add("source.findViewById($L)", binding.getId().code);
} else {
//下面的代碼會組合成一行代碼,Utils中的findRequiredViewAsType()會進行find和強轉操作,上面介紹過了
//Utils.findRequiredViewAsType(source, R.id.textview, "field 'mTextview'", TextView.class);
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;
}
- createBindingUnbindMethod(result)生成unbind()方法,即實現Unbinder接口中的unbind()方法
private MethodSpec createBindingUnbindMethod(TypeSpec.Builder bindingClass) {
//定義unbind()方法,并且添加@Override注解
MethodSpec.Builder result = MethodSpec.methodBuilder("unbind")
.addAnnotation(Override.class)
.addModifiers(PUBLIC);
if (!isFinal && parentBinding == null) {
//@CallSuper是android提供的注解,表示調用父類
result.addAnnotation(CALL_SUPER);
}
if (hasTargetField()) {
if (hasFieldBindings()) {
//添加MainActivity target = this.target
result.addStatement("$T target = this.target", targetTypeName);
}
//添加if (target == null) throw new IllegalStateException("Bindings already cleared.")
result.addStatement("if (target == null) throw new $T($S)", IllegalStateException.class,
"Bindings already cleared.");
//添加this.target = null
result.addStatement("$N = null", hasFieldBindings() ? "this.target" : "target");
//換行
result.addCode("\n");
for (ViewBinding binding : viewBindings) {
if (binding.getFieldBinding() != null) {
//循環添加類似這樣的代碼來解除綁定,target.mTextview = null;
result.addStatement("target.$L = null", binding.getFieldBinding().getName());
}
}
for (FieldCollectionViewBinding binding : collectionBindings) {
result.addStatement("target.$L = null", binding.name);
}
}
//有綁定方法,如設置監聽事件回調方法才會走這個if
if (hasMethodBindings()) {
result.addCode("\n");
for (ViewBinding binding : viewBindings) {
addFieldAndUnbindStatement(bindingClass, result, binding);
}
}
if (parentBinding != null) {
//有父類才會換行,并且添加super.unbind()
result.addCode("\n");
result.addStatement("super.unbind()");
}
return result.build();
}
總結下,至此生成輔助類的代碼就簡單介紹完了,我們只是簡單介紹了下@BindView,其它的注解都沒介紹,有興趣的讀者可以自己去扒Butterknife源碼,我們可以在\app\build\generated\source\apt\debug\目錄下找到編譯時生成的輔助類,下面我們列出一個簡單的
package com.zeit.butter;
import android.support.annotation.CallSuper;
import android.support.annotation.UiThread;
import android.view.View;
import android.widget.Button;
import android.widget.ImageView;
import android.widget.TextView;
import butterknife.Unbinder;
import butterknife.internal.DebouncingOnClickListener;
import butterknife.internal.Utils;
public class MainActivity_ViewBinding implements Unbinder {
private MainActivity target;
private View view2131427415;
@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.mTextview = Utils.findRequiredViewAsType(source, R.id.textview, "field 'mTextview'", TextView.class);
view = Utils.findRequiredView(source, R.id.button, "field 'mButton' and method 'onClick'");
target.mButton = Utils.castView(view, R.id.button, "field 'mButton'", Button.class);
view2131427415 = view;
view.setOnClickListener(new DebouncingOnClickListener() {
@Override
public void doClick(View p0) {
target.onClick();
}
});
target.mImageview = Utils.findRequiredViewAsType(source, R.id.imageview, "field 'mImageview'", ImageView.class);
}
@Override
@CallSuper
public void unbind() {
MainActivity target = this.target;
if (target == null) throw new IllegalStateException("Bindings already cleared.");
this.target = null;
target.mTextview = null;
target.mButton = null;
target.mImageview = null;
view2131427415.setOnClickListener(null);
view2131427415 = null;
}
}
總結
JakeWharton的Butterknife,俗稱"黃油刀",可以幫助我們提高開發效率,尤其是配合AS插件的使用,下面推薦一個插件Android-Butterknife-Zelezny。而閱讀源碼可以讓我們知其然知其所以然,也可以學到很多設計模式。建議閱讀第三方庫或Android提供的API等源碼之前,先從設計模式學起,推薦《Android源碼設計模式解析與實戰》。