自定義Android註解Part2:代碼自動生成

clipboard.png

上一期咱們已經把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

clipboard.png

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

  1. init:初始化必要的數據
  2. getSupportedAnnotationTypes:所支持的註解
  3. process:解析註解,編寫自動生成代碼

init

從簡單到容易,先是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資源文件。

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中定義的註解,生成與註解相關聯的類。

@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。這裏介紹兩個主要的方法

  1. getEnclosedElements():獲元素中的閉包的註解元素,在咱們的實例中元素爲MainActivity(TypeElement,Type表明Class),而閉包的註解元素則爲sName、sPhone、nameClick、phoneClick與onCreate。在這裏簡單的理解就是獲取有註解的字段名、方法名
  2. getAnnotationMirrors():獲取上述閉包元素的全部註解。這裏分別爲sName與sPhone上的@BindeView、nameClick與phoneClick上的@OnClick、onCreate上的@Override。

因此經過該方法最終返回的就是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);

註釋已經劃分清楚了,能夠分爲四個步驟

  1. 獲取對應typeElement的包名(這裏獲取的是com.idisfkj.androidapianalysis)
  2. 獲取typeElement的SimpleName(這裏爲MainActivity字符串)
  3. 根據上述獲取的包名與SimpleName來構建一個ClassName,爲了後續聲明方法的參數類型(這裏爲MainActivity類,注意是MainActivity類型)
  4. 構建須要自動生成的ClassName,這裏使用NameUtils.getAutoGeneratorTypeName進行了統一命名(這裏自動生成的類名爲MainActivity$Binding,都以原始類名後面加$Binding)

全部信息準備完畢後,而後開始定義自動生成的類。這裏經過使用TypeSpec.Builder來構建。它是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引入。

  1. TypeSpec.Builder: 定義一個類
  2. addModifiers: 定義private、public與protected類型
  3. addAnnotation: 對Element元素添加註解。例如:@Keep
  4. TypeSpec.Builder -> addMethod: 添加方法
  5. MethodSpec -> addParameter: 爲方法添加參數類型與參數名
  6. MethodSpec -> addStatement: 在方法中添加代碼塊。而其中的一些動態類型會使用佔位符替代。例如:addStatement("$N($N)", "bindView", "activity"),它將會生成bindView(activity)。佔位符:$N -> name, $T -> type(ClassName), $L -> literals

有了上面的理解咱們再來看下面的生成代碼:

//構建自動生成的類
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

//添加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

//添加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);
}

services

在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

相關文章

自定義Android註解Part1:註解變量

自定義Android註解Part3:綁定

關注

公衆號:怪談時間到了

怪談時間到了

相關文章
相關標籤/搜索