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

上一期我們已經(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)。

  1. init:初始化必要的數(shù)據(jù)
  2. getSupportedAnnotationTypes:所支持的注解
  3. 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。這里介紹兩個主要的方法

  1. getEnclosedElements():獲元素中的閉包的注解元素袜腥,在我們的實例中元素為MainActivity(TypeElement见擦,Type代表Class),而閉包的注解元素則為sName瞧挤、sPhone、nameClick儡湾、phoneClick與onCreate特恬。在這里簡單的理解就是獲取有注解的字段名、方法名
  2. 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)劃分清楚了譬淳,可以分為四個步驟

  1. 獲取對應typeElement的包名(這里獲取的是com.idisfkj.androidapianalysis)
  2. 獲取typeElement的SimpleName(這里為MainActivity字符串)
  3. 根據(jù)上述獲取的包名與SimpleName來構(gòu)建一個ClassName档址,為了后續(xù)聲明方法的參數(shù)類型(這里為MainActivity類,注意是MainActivity類型)
  4. 構(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引入儿咱。

  1. TypeSpec.Builder: 定義一個類
  2. addModifiers: 定義private、public與protected類型
  3. addAnnotation: 對Element元素添加注解场晶。例如:@Keep
  4. TypeSpec.Builder -> addMethod: 添加方法
  5. MethodSpec -> addParameter: 為方法添加參數(shù)類型與參數(shù)名
  6. 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

相關(guān)文章

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

自定義Android注解Part3:綁定

關(guān)注

怪談時間到了
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末横腿,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子斤寂,更是在濱河造成了極大的恐慌耿焊,老刑警劉巖,帶你破解...
    沈念sama閱讀 222,865評論 6 518
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件遍搞,死亡現(xiàn)場離奇詭異罗侯,居然都是意外死亡,警方通過查閱死者的電腦和手機尾抑,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 95,296評論 3 399
  • 文/潘曉璐 我一進店門歇父,熙熙樓的掌柜王于貴愁眉苦臉地迎上來蒂培,“玉大人再愈,你說我怎么就攤上這事』ご粒” “怎么了翎冲?”我有些...
    開封第一講書人閱讀 169,631評論 0 364
  • 文/不壞的土叔 我叫張陵,是天一觀的道長媳荒。 經(jīng)常有香客問我抗悍,道長驹饺,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 60,199評論 1 300
  • 正文 為了忘掉前任缴渊,我火速辦了婚禮赏壹,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘衔沼。我一直安慰自己蝌借,他們只是感情好,可當我...
    茶點故事閱讀 69,196評論 6 398
  • 文/花漫 我一把揭開白布指蚁。 她就那樣靜靜地躺著菩佑,像睡著了一般。 火紅的嫁衣襯著肌膚如雪凝化。 梳的紋絲不亂的頭發(fā)上稍坯,一...
    開封第一講書人閱讀 52,793評論 1 314
  • 那天,我揣著相機與錄音搓劫,去河邊找鬼瞧哟。 笑死,一個胖子當著我的面吹牛糟把,可吹牛的內(nèi)容都是我干的绢涡。 我是一名探鬼主播,決...
    沈念sama閱讀 41,221評論 3 423
  • 文/蒼蘭香墨 我猛地睜開眼遣疯,長吁一口氣:“原來是場噩夢啊……” “哼雄可!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起缠犀,我...
    開封第一講書人閱讀 40,174評論 0 277
  • 序言:老撾萬榮一對情侶失蹤数苫,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后辨液,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體虐急,經(jīng)...
    沈念sama閱讀 46,699評論 1 320
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,770評論 3 343
  • 正文 我和宋清朗相戀三年滔迈,在試婚紗的時候發(fā)現(xiàn)自己被綠了止吁。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,918評論 1 353
  • 序言:一個原本活蹦亂跳的男人離奇死亡燎悍,死狀恐怖敬惦,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情谈山,我是刑警寧澤俄删,帶...
    沈念sama閱讀 36,573評論 5 351
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響畴椰,放射性物質(zhì)發(fā)生泄漏臊诊。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 42,255評論 3 336
  • 文/蒙蒙 一斜脂、第九天 我趴在偏房一處隱蔽的房頂上張望抓艳。 院中可真熱鬧,春花似錦帚戳、人聲如沸壶硅。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,749評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽庐椒。三九已至,卻和暖如春蚂踊,著一層夾襖步出監(jiān)牢的瞬間约谈,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,862評論 1 274
  • 我被黑心中介騙來泰國打工犁钟, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留棱诱,地道東北人。 一個月前我還...
    沈念sama閱讀 49,364評論 3 379
  • 正文 我出身青樓涝动,卻偏偏與公主長得像迈勋,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子醋粟,可洞房花燭夜當晚...
    茶點故事閱讀 45,926評論 2 361

推薦閱讀更多精彩內(nèi)容