上一期我們已經(jīng)把butterknife-annotations中的注解變量都已經(jīng)定義好了拴清,分別為BindView师痕、OnClick與Keep。
如果你是第一次進入本系列文章震肮,強烈推薦跳到文章末尾查看上篇文章称龙,要不然你可能會有點云里霧里。
如果在代碼中引用的話戳晌,它將與開源庫ButterKnife的操作類似鲫尊。
class MainActivity : AppCompatActivity() {
@BindView(R.id.public_service, R.string.public_service)
lateinit var sName: TextView
@BindView(R.id.personal_wx, R.string.personal_wx)
lateinit var sPhone: TextView
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
Butterknife.bind(this)
}
@OnClick(R.id.public_service)
fun nameClick(view: View) {
Toast.makeText(this, getString(R.string.public_service_click_toast), Toast.LENGTH_LONG).show()
}
@OnClick(R.id.personal_wx)
fun phoneClick(view: View) {
Toast.makeText(this, getString(R.string.personal_wx_click_toast), Toast.LENGTH_LONG).show()
}
}
使用@BindView來綁定我的View,使用@OnClick來綁定View的點擊事件沦偎。使用Butterknife.bind來綁定該Class疫向,主要是用來實例化自動生成的類咳蔚。(該部分下篇文章將提及)
我們自己定義的綁定注解庫已經(jīng)完成了1/3,接下來我們將實現(xiàn)它的代碼自動生成部分搔驼。這時就到了上期提到的第二個Module:butterknife-compiler谈火。
NameUtils是一些常量的管理工具類。
final class NameUtils {
static String getAutoGeneratorTypeName(String typeName) {
return typeName + ConstantUtils.BINDING_BUTTERKNIFE_SUFFIX;
}
static class Package{
static final String ANDROID_VIEW = "android.view";
}
static class Class {
static final String CLASS_VIEW = "View";
static final String CLASS_ON_CLICK_LISTENER = "OnClickListener";
}
static class Method{
static final String BIND_VIEW = "bindView";
static final String SET_ON_CLICK_LISTENER = "setOnClickListener";
static final String ON_CLICK = "onClick";
}
static class Variable{
static final String ANDROID_ACTIVITY = "activity";
}
}
NameUitls包含了自動生成的類名稱匙奴,包名堆巧,方法名,變量名泼菌〉簦總之就是為了代碼更健全,方便管理哗伯。
第二個類Processor是今天的重中之重荒揣。也是注解庫代碼自動生成的核心部分。由于注解的自動生成代碼都是在注解進程中進行焊刹,所以這里它繼承于AbstractProcessor系任,其中主要有三個方法需要實現(xiàn)。
- init:初始化必要的數(shù)據(jù)
- getSupportedAnnotationTypes:所支持的注解
- process:解析注解虐块,編寫自動生成代碼
init
從簡單到容易俩滥,先是init方法,我們直接看代碼
@Override
public synchronized void init(ProcessingEnvironment processingEnv) {
super.init(processingEnv);
mFiler = processingEnv.getFiler();
mMessager = processingEnv.getMessager();
mElementUtils = processingEnv.getElementUtils();
}
方法參數(shù)processingEnv為我們提供注解處理所需的環(huán)境狀態(tài)贺奠。我們通過getFiler()霜旧、getMessager()與getElementUthis()方法,分別獲取創(chuàng)建源代碼的Filer儡率、消息發(fā)送器Messager(主要用于向外界發(fā)送錯誤信息)與解析注解元素所需的通用方法挂据。
例如:當我們已經(jīng)構(gòu)建好了需要自動生成的類,這時我們就可以使用Filter來將代碼寫入到java文件中儿普,如遇錯誤使用Messager將錯誤信息發(fā)送出去崎逃。
//寫入java文
try {
JavaFile.builder(packageName, typeBuilder.build()).build().writeTo(mFiler)
} catch (IOException e) {
mMessager.printMessage(Diagnostic.Kind.ERROR, e.getMessage(), typeElement);
}
代碼中的JavaFile與typeBuilder都是JavaPoet中的類。JavaPote主要提供Java API來幫助生成.java
資源文件眉孩。
getSupportedAnnotationTypes
@Override
public Set<String> getSupportedAnnotationTypes() {
return new TreeSet<>(Arrays.asList(
BindView.class.getCanonicalName(),
OnClick.class.getCanonicalName(),
Keep.class.getCanonicalName())
);
}
看方法名就知道了个绍,包含所支持的注解,將其通過set集合來返回浪汪。這里將我們上一期自定義的注解添加到set集合中即可障贸。
process
到了本篇文章的核心,process用來生成與注解相匹配的方法代碼吟宦。通過解析Class中定義的注解篮洁,生成與注解相關(guān)聯(lián)的類。
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
....
....
return true;
}
提供了兩個參數(shù):annotations與roundEnv殃姓,分別代表需要處理的注解袁波,這里就代表我們自定義的注解瓦阐;注解處理器所需的環(huán)境,幫助進行解析注解篷牌。
在開始解析注解之前睡蟋,我們應該先過濾我們所不需要的注解〖霞眨回頭看getSupportedAnnotationTypes方法戳杀,我們只支持BindView、OnClick與Keep這三個注解夭苗。為了解析出相匹配的注解信卡,我們將這個邏輯單獨抽離出來,交由getTypeElementsByAnnotationType來管理题造。
private Set<TypeElement> getTypeElementsByAnnotationType(Set<? extends TypeElement> annotations, Set<? extends Element> elements) {
Set<TypeElement> result = new HashSet<>();
//遍歷包含的 package class method
for (Element element : elements) {
//匹配 class or interface
if (element instanceof TypeElement) {
boolean found = false;
//遍歷class中包含的 filed method constructors
for (Element subElement : element.getEnclosedElements()) {
//遍歷element中包含的注釋
for (AnnotationMirror annotationMirror : subElement.getAnnotationMirrors()) {
for (TypeElement annotation : annotations) {
//匹配注釋
if (annotationMirror.getAnnotationType().asElement().equals(annotation)) {
result.add((TypeElement) element);
found = true;
break;
}
}
if (found) break;
}
if (found) break;
}
}
}
return result;
}
首先理解Element是什么傍菇?Element代表程序中的包名、類界赔、方法丢习,這也是注解所支持的作用類型。然后再回到代碼部分淮悼,已經(jīng)給出詳細代碼注釋咐低。
該方法的作用就是獲取到有我們自定義注解的class。這里介紹兩個主要的方法
- getEnclosedElements():獲元素中的閉包的注解元素袜腥,在我們的實例中元素為MainActivity(TypeElement见擦,Type代表Class),而閉包的注解元素則為sName瞧挤、sPhone、nameClick儡湾、phoneClick與onCreate特恬。在這里簡單的理解就是獲取有注解的字段名、方法名
- getAnnotationMirrors():獲取上述閉包元素的所有注解徐钠。這里分別為sName與sPhone上的@BindeView癌刽、nameClick與phoneClick上的@OnClick、onCreate上的@Override尝丐。
所以通過該方法最終返回的就是MainActivity显拜,它將被轉(zhuǎn)化為TypeElement類型返回钓辆,然后將由processing來處理凶朗。
我們再回到process方法中。通過getTypeElementsByAnnotationType()方法我們已經(jīng)獲取到了我們使用了自定義注解的TypeElement(MainActivity)刑桑。
//獲取與annotation相匹配的TypeElement,即有注釋聲明的class
Set<TypeElement> elements = getTypeElementsByAnnotationType(annotations, roundEnv.getRootElements());
下面我們再獲取構(gòu)建類所需的相關(guān)信息失息。
//包名
String packageName = mElementUtils.getPackageOf(typeElement).getQualifiedName().toString();
//類名
String typeName = typeElement.getSimpleName().toString();
//全稱類名
ClassName className = ClassName.get(packageName, typeName);
//自動生成類全稱名
ClassName autoGenerationClassName = ClassName.get(packageName,
NameUtils.getAutoGeneratorTypeName(typeName));
//構(gòu)建自動生成的類
TypeSpec.Builder typeBuilder = TypeSpec.classBuilder(autoGenerationClassName)
.addModifiers(Modifier.PUBLIC)
.addAnnotation(Keep.class);
注釋已經(jīng)劃分清楚了譬淳,可以分為四個步驟
- 獲取對應typeElement的包名(這里獲取的是com.idisfkj.androidapianalysis)
- 獲取typeElement的SimpleName(這里為MainActivity字符串)
- 根據(jù)上述獲取的包名與SimpleName來構(gòu)建一個ClassName档址,為了后續(xù)聲明方法的參數(shù)類型(這里為MainActivity類,注意是MainActivity類型)
- 構(gòu)建需要自動生成的ClassName邻梆,這里使用NameUtils.getAutoGeneratorTypeName進行了統(tǒng)一命名(這里自動生成的類名為MainActivityBinding守伸,都以原始類名后面加Binding)
所有信息準備完畢后,然后開始定義自動生成的類浦妄。這里通過使用TypeSpec.Builder來構(gòu)建尼摹。它是JavaPoet中的類。
JavaPoet
由于直接使用JavaFileObject生成.java資源文件是非常麻煩的剂娄,所以推薦使用JavaPoet蠢涝。JavaPoet是一個開源庫,主要用來幫助方便快捷的生成.java的資源文件宜咒。想要全面了解的可以查看Github鏈接惠赫。為了幫助快速讀懂該文章,這里對其中幾個主要方法進行介紹故黑。當然在使用前還需在butterknife-compiler中的builder.gradle添加依賴:
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation project(':butterknife-annotations')
implementation 'com.squareup:javapoet:1.11.1'
}
同時也將上一期我們自定義的注解Module引入儿咱。
- TypeSpec.Builder: 定義一個類
- addModifiers: 定義private、public與protected類型
- addAnnotation: 對Element元素添加注解场晶。例如:@Keep
- TypeSpec.Builder -> addMethod: 添加方法
- MethodSpec -> addParameter: 為方法添加參數(shù)類型與參數(shù)名
- MethodSpec -> addStatement: 在方法中添加代碼塊混埠。而其中的一些動態(tài)類型會使用占位符替代。例如:addStatement("N(N)", "bindView", "activity")诗轻,它將會生成bindView(activity)钳宪。占位符:N -> name,T -> type(ClassName), $L -> literals
有了上面的理解我們再來看下面的生成代碼:
//構(gòu)建自動生成的類
TypeSpec.Builder typeBuilder = TypeSpec.classBuilder(autoGenerationClassName)
.addModifiers(Modifier.PUBLIC)
.addAnnotation(Keep.class);
//添加構(gòu)造方法
typeBuilder.addMethod(MethodSpec.constructorBuilder()
.addModifiers(Modifier.PUBLIC)
.addParameter(className, NameUtils.Variable.ANDROID_ACTIVITY)
.addStatement("$N($N)",
NameUtils.Method.BIND_VIEW,
NameUtils.Variable.ANDROID_ACTIVITY)
.addStatement("$N($N)",
NameUtils.Method.SET_ON_CLICK_LISTENER,
NameUtils.Variable.ANDROID_ACTIVITY)
.build());
首先通過TypeSpec.Builder構(gòu)建一個類,類名為autoGenerationClassName(MainActivity$Binding)扳炬,類的訪問級別為public吏颖,由于為了防止混淆使用了我們自定義的@Keep注解。
然后再來添加類的構(gòu)造方法恨樟,使用addMethod半醉、addModifiers、addParameter與addStatement分別構(gòu)建構(gòu)造方法名劝术、方法訪問級別缩多、方法參數(shù)與方法中執(zhí)行的代碼塊。所以上面的代碼最終將會自動生成如下代碼:
@Keep
public class MainActivity$Binding {
public MainActivity$Binding(MainActivity activity) {
bindView(activity);
setOnClickListener(activity);
}
}
在自動生成類的構(gòu)造方法中調(diào)用了我們想要的bindView與setOnClickListener方法养晋。所以接下來我們要實現(xiàn)的就是這兩個方法的構(gòu)建衬吆。
bindView
//添加bindView成員方法
MethodSpec.Builder bindViewBuilder = MethodSpec.methodBuilder(NameUtils.Method.BIND_VIEW)
.addModifiers(Modifier.PRIVATE)
.returns(TypeName.VOID)
.addParameter(className, NameUtils.Variable.ANDROID_ACTIVITY);
//添加方法內(nèi)容
for (VariableElement variableElement : ElementFilter.fieldsIn(typeElement.getEnclosedElements())) {
BindView bindView = variableElement.getAnnotation(BindView.class);
if (bindView != null) {
bindViewBuilder.addStatement("$N.$N=($T)$N.findViewById($L)",
NameUtils.Variable.ANDROID_ACTIVITY,
variableElement.getSimpleName(),
variableElement,
NameUtils.Variable.ANDROID_ACTIVITY,
bindView.value()[0]
).addStatement("$N.$N.setText($N.getString($L))",
NameUtils.Variable.ANDROID_ACTIVITY,
variableElement.getSimpleName(),
NameUtils.Variable.ANDROID_ACTIVITY,
bindView.value()[1]);
}
}
typeBuilder.addMethod(bindViewBuilder.build());
使用MethodSpec.Builder來創(chuàng)建bindView方法,其它的都與構(gòu)造方法類似绳泉。使用returns為方法返回void類型逊抡。然后再遍歷MainActivity中的注解,找到與我們定義的BindView相匹配的字段零酪。最后分別向bindView方法中添加findViewById與setText代碼塊秦忿,同時將定義的方法添加到typeBuilder中麦射。所以執(zhí)行完上面代碼后在MainActivity$Binding中展示如下:
private void bindView(MainActivity activity) {
activity.sName=(TextView)activity.findViewById(2131165265);
activity.sName.setText(activity.getString(2131427362));
activity.sPhone=(TextView)activity.findViewById(2131165262);
activity.sPhone.setText(activity.getString(2131427360));
}
實現(xiàn)了我們最初的View的綁定與TextView的默認值設置。
setOnClickListener
//添加setOnClickListener成員方法
MethodSpec.Builder setOnClickListenerBuilder = MethodSpec.methodBuilder(NameUtils.Method.SET_ON_CLICK_LISTENER)
.addModifiers(Modifier.PRIVATE)
.returns(TypeName.VOID)
.addParameter(className, NameUtils.Variable.ANDROID_ACTIVITY, Modifier.FINAL);
//添加方法內(nèi)容
ClassName viewClassName = ClassName.get(NameUtils.Package.ANDROID_VIEW, NameUtils.Class.CLASS_VIEW);
ClassName onClickListenerClassName = ClassName.get(NameUtils.Package.ANDROID_VIEW, NameUtils.Class.CLASS_VIEW, NameUtils.Class.CLASS_ON_CLICK_LISTENER);
for (ExecutableElement executableElement : ElementFilter.methodsIn(typeElement.getEnclosedElements())) {
OnClick onClick = executableElement.getAnnotation(OnClick.class);
if (onClick != null) {
//構(gòu)建匿名class
TypeSpec typeSpec = TypeSpec.anonymousClassBuilder("")
.addSuperinterface(onClickListenerClassName)
.addMethod(MethodSpec.methodBuilder(NameUtils.Method.ON_CLICK)
.addModifiers(Modifier.PUBLIC)
.addParameter(viewClassName, NameUtils.Class.CLASS_VIEW)
.returns(TypeName.VOID)
.addStatement("$N.$N($N)",
NameUtils.Variable.ANDROID_ACTIVITY,
executableElement.getSimpleName(),
NameUtils.Class.CLASS_VIEW)
.build())
.build();
setOnClickListenerBuilder.addStatement("$N.findViewById($L).setOnClickListener($L)",
NameUtils.Variable.ANDROID_ACTIVITY,
onClick.value(),
typeSpec);
}
}
typeBuilder.addMethod(setOnClickListenerBuilder.build());
與bindView方法不同的是灯谣,由于使用到了匿名類OnClickListener與類View潜秋,所以我們這里也要定義他們的ClassName,然后使用TypeSpec來生成匿名類胎许。生成之后再添加到setOnClickListener方法中峻呛。最后再將setOnClickListener方法添加到MainActivity$Binding中。所以最終展示如下:
private void setOnClickListener(final MainActivity activity) {
activity.findViewById(2131165265).setOnClickListener(new View.OnClickListener() {
public void onClick(View View) {
activity.nameClick(View);
}
});
activity.findViewById(2131165262).setOnClickListener(new View.OnClickListener() {
public void onClick(View View) {
activity.phoneClick(View);
}
});
}
我們的MainActivity$Binding類就已經(jīng)定義完成辜窑,最后再寫入到java文件中
//寫入java文件
try {
JavaFile.builder(packageName, typeBuilder.build()).build().writeTo(mFiler);
} catch (IOException e) {
mMessager.printMessage(Diagnostic.Kind.ERROR, e.getMessage(), typeElement);
}
services
在butterknife-compiler中钩述,我們還需創(chuàng)建一個特定的目錄:
butterknife-compiler/src/main/resources/META-INF/services
在services目錄中,我們還需創(chuàng)建一個文件:javax.annotation.processing.Processor
穆碎,該文件是用來告訴編譯器牙勘,當它在編譯代碼的過程中正處于注解處理中時,會告訴注解處理器來自動生成哪些類所禀。
所以我們在文件中將添加我們自定義的Processor路徑
com.idisfkj.butterknife.compiler.Processor
這樣注解器就會調(diào)用該指定的Processor方面。到這里整個butterknife-compiler就完成了,現(xiàn)在我們可以Make Project
一下工程色徘,完成之后就可以全局搜索到MainActivity$Binding文件了恭金。或者在如下路徑中查看:
/app/build/generated/source/kapt/debug/com/idisfkj/androidapianalysis/MainActivity$Binding.java
文章中的代碼都可以在Github中獲取到褂策。使用時請將分支切換到feat_annotation_processing