上一期咱們已經把butterknife-annotations中的註解變量都已經定義好了,分別爲BindView、OnClick與Keep。java
若是你是第一次進入本系列文章,強烈推薦跳到文章末尾查看上篇文章,要否則你可能會有點雲裏霧裏。android
若是在代碼中引用的話,它將與開源庫ButterKnife的操做相似。git
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,主要是用來實例化自動生成的類。(該部分下篇文章將說起)github
咱們本身定義的綁定註解庫已經完成了1/3,接下來咱們將實現它的代碼自動生成部分。這時就到了上期提到的第二個Module:butterknife-compiler。segmentfault
NameUtils是一些常量的管理工具類。api
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,其中主要有三個方法須要實現。app
從簡單到容易,先是init方法,咱們直接看代碼ide
@Override public synchronized void init(ProcessingEnvironment processingEnv) { super.init(processingEnv); mFiler = processingEnv.getFiler(); mMessager = processingEnv.getMessager(); mElementUtils = processingEnv.getElementUtils(); }
方法參數processingEnv爲咱們提供註解處理所需的環境狀態。咱們經過getFiler()、getMessager()與getElementUthis()方法,分別獲取建立源代碼的Filer、消息發送器Messager(主要用於向外界發送錯誤信息)與解析註解元素所需的通用方法。工具
例如:當咱們已經構建好了須要自動生成的類,這時咱們就可使用Filter來將代碼寫入到java文件中,如遇錯誤使用Messager將錯誤信息發送出去。
//寫入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
資源文件。
@Override public Set<String> getSupportedAnnotationTypes() { return new TreeSet<>(Arrays.asList( BindView.class.getCanonicalName(), OnClick.class.getCanonicalName(), Keep.class.getCanonicalName()) ); }
看方法名就知道了,包含所支持的註解,將其經過set集合來返回。這裏將咱們上一期自定義的註解添加到set集合中便可。
到了本篇文章的核心,process用來生成與註解相匹配的方法代碼。經過解析Class中定義的註解,生成與註解相關聯的類。
@Override public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) { .... .... return true; }
提供了兩個參數:annotations與roundEnv,分別表明須要處理的註解,這裏就表明咱們自定義的註解;註解處理器所需的環境,幫助進行解析註解。
在開始解析註解以前,咱們應該先過濾咱們所不須要的註解。回頭看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表明程序中的包名、類、方法,這也是註解所支持的做用類型。而後再回到代碼部分,已經給出詳細代碼註釋。
該方法的做用就是獲取到有咱們自定義註解的class。這裏介紹兩個主要的方法
因此經過該方法最終返回的就是MainActivity,它將被轉化爲TypeElement類型返回,而後將由processing來處理。
咱們再回到process方法中。經過getTypeElementsByAnnotationType()方法咱們已經獲取到了咱們使用了自定義註解的TypeElement(MainActivity)。
//獲取與annotation相匹配的TypeElement,即有註釋聲明的class Set<TypeElement> elements = getTypeElementsByAnnotationType(annotations, roundEnv.getRootElements());
下面咱們再獲取構建類所需的相關信息。
//包名 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)); //構建自動生成的類 TypeSpec.Builder typeBuilder = TypeSpec.classBuilder(autoGenerationClassName) .addModifiers(Modifier.PUBLIC) .addAnnotation(Keep.class);
註釋已經劃分清楚了,能夠分爲四個步驟
全部信息準備完畢後,而後開始定義自動生成的類。這裏經過使用TypeSpec.Builder來構建。它是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 typeBuilder = TypeSpec.classBuilder(autoGenerationClassName) .addModifiers(Modifier.PUBLIC) .addAnnotation(Keep.class); //添加構造方法 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構建一個類,類名爲autoGenerationClassName(MainActivity$Binding),類的訪問級別爲public,因爲爲了防止混淆使用了咱們自定義的@Keep註解。
而後再來添加類的構造方法,使用addMethod、addModifiers、addParameter與addStatement分別構建構造方法名、方法訪問級別、方法參數與方法中執行的代碼塊。因此上面的代碼最終將會自動生成以下代碼:
@Keep public class MainActivity$Binding { public MainActivity$Binding(MainActivity activity) { bindView(activity); setOnClickListener(activity); } }
在自動生成類的構造方法中調用了咱們想要的bindView與setOnClickListener方法。因此接下來咱們要實現的就是這兩個方法的構建。
//添加bindView成員方法 MethodSpec.Builder bindViewBuilder = MethodSpec.methodBuilder(NameUtils.Method.BIND_VIEW) .addModifiers(Modifier.PRIVATE) .returns(TypeName.VOID) .addParameter(className, NameUtils.Variable.ANDROID_ACTIVITY); //添加方法內容 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來建立bindView方法,其它的都與構造方法相似。使用returns爲方法返回void類型。而後再遍歷MainActivity中的註解,找到與咱們定義的BindView相匹配的字段。最後分別向bindView方法中添加findViewById與setText代碼塊,同時將定義的方法添加到typeBuilder中。因此執行完上面代碼後在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)); }
實現了咱們最初的View的綁定與TextView的默認值設置。
//添加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); //添加方法內容 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) { //構建匿名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類就已經定義完成,最後再寫入到java文件中
//寫入java文件 try { JavaFile.builder(packageName, typeBuilder.build()).build().writeTo(mFiler); } catch (IOException e) { mMessager.printMessage(Diagnostic.Kind.ERROR, e.getMessage(), typeElement); }
在butterknife-compiler中,咱們還需建立一個特定的目錄:butterknife-compiler/src/main/resources/META-INF/services
在services目錄中,咱們還需建立一個文件:javax.annotation.processing.Processor
,該文件是用來告訴編譯器,當它在編譯代碼的過程當中正處於註解處理中時,會告訴註解處理器來自動生成哪些類。
因此咱們在文件中將添加咱們自定義的Processor路徑
com.idisfkj.butterknife.compiler.Processor
這樣註解器就會調用該指定的Processor。到這裏整個butterknife-compiler就完成了,如今咱們能夠Make Project
一下工程,完成以後就能夠全局搜索到MainActivity$Binding文件了。或者在以下路徑中查看:
/app/build/generated/source/kapt/debug/com/idisfkj/androidapianalysis/MainActivity$Binding.java
文章中的代碼均可以在Github中獲取到。使用時請將分支切換到feat_annotation_processing
公衆號:怪談時間到了