自定義Android注解解析器

前言

本文是IOC系列文章的第五篇,也是最后一篇,也是最重要的一篇。之所以說最重要,是因為掌握自定義注解解析器是所有Android架構(gòu)師必備的技能,沒有一個Android架構(gòu)師說自己不會自定義注解解析器的,另外掌握注解解析器更加有助于我們理解那些優(yōu)秀的開源框架,像Retrofit、EventBus和Dagger2等等。本文將詳細給大家?guī)黻P(guān)于自定義注解解析器的知識。

APT的工作流程

在上一篇文章Butterknife源碼全面解析的最后簡單介紹了一下APT技術(shù),這里我再給大家講講APT的工作流程。在代碼編譯階段(javac),會掃描所有AbstractProcessor的已注冊的子類,并且會調(diào)用其process()方法,在該方法中我們可以解析注解并生成java文件,然后在程序調(diào)用我們生成的代碼即可,其大致流程如下圖所示

apt大致流程.jpg

對我們開發(fā)者來說,最核心的目的就是實現(xiàn)AbstractProcessor并生成相關(guān)代碼。

AbstractProcessor

實現(xiàn)自定義注解必須要掌握的就是AbstractProcessor,它是虛處理器,運行在單獨的JVM中,使用AbstractProcessor對象必須要有javax環(huán)境。

AbstractProcessor有四個重要的方法:

  • init(ProcessingEnvironment processingEnvironment):會被注解工具所調(diào)用,并傳入ProcessingEnvironment參數(shù),通過ProcessingEnvironment參數(shù)我們可以拿到Filer和Messager等工具類,F(xiàn)iler看名字就知道是文件,生成java代碼的時候使用,Messager是用來輸出日志信息的。

  • process(Set<? extends TypeElement> annotations, RoundEnvironment env) :AbstractProcessor中最重要的方法,相當于main()函數(shù),通過RoundEnvironment參數(shù)我們可以獲得所有被注解標注的程序元素(包、類、成員變量、構(gòu)造方法、成員方法...),在這里我們可以解析這些程序元素并生成java文件

  • getSupportedAnnotationTypes():需要解析的注解集合,這里返回的Set<String>,這里可以用@SupportedAnnotationTypes()來代替

  • getSupportedSourceVersion:獲得支持的java版本,一般直接返回SourceVersion.latestSupported(),同樣可以用@SupportedSourceVersion注解來代替

注冊AbstractProcessor

寫好了AbstractProcessor我們必須要注冊才會生效,如何注冊呢?我們必須把注解解析器打包到jar包,需要在META-INF/services路徑下生成一個javax.annotation.processing.Processor文件,并在該文件中生成你所聲明的Processor對象,是不是聽起來很麻煩,其實做起來也很麻煩。谷歌爸爸為了方便廣大開發(fā)者,特意開發(fā)了auto-service庫,我們只需要在項目中引入auto-service庫,并且在我們聲明的
Processor類上面使用@AutoService(Processor.class)即可

@AutoService(Processor.class)
public class BindingProcessor extends AbstractProcessor {

    private Filer mFiler;//文件類
    private Messager mMessager;//打印錯誤信息
    private static final Map<TypeElement, List<ViewInfo>> bindViews = new HashMap<>();//綁定的view集合

注:這里需要注意的使用gradle版本大于5.0以上

#Sun Apr 26 15:32:47 PDT 2020
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.4-all.zip

在gradle5.0以上會自動忽略auto-service,所以在引入的時候我們需要

    implementation 'com.google.auto.service:auto-service:1.0-rc6'
    annotationProcessor 'com.google.auto.service:auto-service:1.0-rc6'(必須要加上這行代碼,否則無法注冊成功)

接著,我們可以編譯一下項目,然后打開build下面的classes


注冊Processor

如圖所示,即代表注冊成功了

JavaPoet

當我們解析好了注解的程序元素以后就需要生成java文件,一行行手打代碼就會很麻煩,java很貼心地推出了JavaPoet開源庫,我們只需要引入這個類庫即可。它可以很幫助開發(fā)者很輕松地生成需要的代碼,直接放上幾行代碼給大家看一下效果
引入類庫

    implementation 'com.squareup:javapoet:1.12.1'
            FieldSpec fieldSpec = FieldSpec.builder(String.class,"name",Modifier.PRIVATE).build();//成員變量

            MethodSpec methodSpec = MethodSpec.constructorBuilder()//生成的方法對象
                    .addModifiers(Modifier.PUBLIC)//方法的修飾符
                    .addParameter(className, paramName)//方法中的參數(shù),第一個是參數(shù)類型,第二個是參數(shù)名
                    .addCode(builder.build())//方法體重的代碼
                    .build();

            TypeSpec typeSpec = TypeSpec.classBuilder(typeElement.getSimpleName().toString() + TestBindView.SUFFIX)//類對象,參數(shù):類名
                    .addMethod(methodSpec)//添加方法
                    .addField(fieldSpec)//添加成員變量
                    .build();

            JavaFile javaFile = JavaFile.builder(packageName, typeSpec).build();//javaFile對象,最終用來寫入的對象,參數(shù)1:包名;參數(shù)2:TypeSpec

            try {
                javaFile.writeTo(mFiler);
            } catch (IOException e) {
                e.printStackTrace();
            }

具體用法這里不多說了,JavaPoet使用起來很簡單,最好就是先去GitHub上看一下官方文檔,接著寫一個Hello JavaPoet就能掌握了,送上github地址 JavaPoet

使用Android Studio搭建IOC項目

了解了AbstractProcessor和JavaPoet我們開始搭建IOC項目中,剛才介紹AbstractProcessor時提到了,其必須要有javax環(huán)境,而我們默認的Android Module中沒有javax,所以必須要建立一個java library要來處理annotation-processor(注解解析器),另外還需要再創(chuàng)建一個java library來處理annotation(注解),這里之所以要把annotation-processor和annotation分開,是因為annotation-processor我們只有在編譯期才用到,所以不必要把annotation-processor的相關(guān)代碼打入到APK中,這里我們通過annotationProcessor方式依賴即可。關(guān)于Android module和java library的依賴關(guān)系是Android module依賴annotation-processor和annotation,其中annotation-processor又依賴annotation,因為要通過annotation-processor解析annotation中的注解,一圖勝千文

ioc項目依賴關(guān)系.jpg

注:Demo的github地址在文章的最后

通過自定義注解實現(xiàn)Butterknife的BindView功能

在上一篇文章Butterknife源碼全面解析中給大家介紹了Butterknife的源碼和實現(xiàn)思路,如圖

butterknife實現(xiàn)原理.png

這里我們仿照Butterknife的思路,首先在annotation中聲明一個@TestBindView注解

@Retention(RetentionPolicy.CLASS)
@Target(ElementType.FIELD)
public @interface TestBindView {
    int value();
    String SUFFIX = "_TestBinding";
}

然后我們在自定義的BindingProcessor中的init()方法中初始化,F(xiàn)iler和Messager對象

@AutoService(Processor.class)
public class BindingProcessor extends AbstractProcessor {

    private Filer mFiler;//文件類
    private Messager mMessager;//打印錯誤信息
    private static final Map<TypeElement, List<ViewInfo>> bindViews = new HashMap<>();//綁定的view集合


    @Override
    public synchronized void init(ProcessingEnvironment processingEnvironment) {
        super.init(processingEnvironment);
        mFiler = processingEnvironment.getFiler();//初始化文件對象
        mMessager = processingEnvironment.getMessager();//初始化信息對象

    }

注意:這里Messager對象需要說一下,之所以出現(xiàn)錯誤要使用Messager對象打印出來,而不采取我們傳統(tǒng)的try catch方法是因為往往注解解析器出錯會引發(fā)一大堆的異常,這個時候如果用try catch會導致我們的異常信息特別多,定位問題很麻煩,而如果使用Messager就會讓錯誤信息簡單明了。

下面就來了解析注解并且生成java文件的核心代碼了,這里我想講一下我的思路:

  1. 通過process()方法里面的RoundEnvironment對象拿到所有被我們目標注解(TestBindView)注解的程序元素,然后遍歷

2.對程序元素進行分類,用一個Map容器,其中key是TypeElement(類元素,這里就代表我們的Activity),value是一個ViewInfo集合,ViewInfo包含viewId和viewName

3.拿到第二步的Map容器,開始利用JavaPoet生成代碼,我們這里的邏輯很簡單就是聲明一個后綴,通過Activity的名字拼接后綴作生成的文件名,然后在構(gòu)造方法里面調(diào)用findViewById()方法

下面直接上代碼

 @Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
        Set<? extends Element> elements = roundEnvironment.getElementsAnnotatedWith(TestBindView.class);//獲取TestBindView注解的所有元素
        for (Element element : elements) {//遍歷元素
            VariableElement variableElement = (VariableElement) element;//因為注解的作用域是成員變量,所以這里可以直接強轉(zhuǎn)成 VariableElement
            Set<Modifier> modifiers = variableElement.getModifiers();//權(quán)限修飾符
            if (modifiers.contains(Modifier.PRIVATE) || modifiers.contains(Modifier.PROTECTED)) {//類型檢查
                mMessager.printMessage(Diagnostic.Kind.ERROR, "成員變量的類型不能是PRIVATE或者PROTECTED");
                return false;
            }
            TypeElement typeElement = (TypeElement) variableElement.getEnclosingElement();//獲得外部元素對象
            sortToAct(typeElement, variableElement);//以類元素進行分類
        }

        writeToFile();

        return false;
    }

第二步驟分類邏輯

 /**
     * 把view信息跟activity關(guān)聯(lián)在一起
     */
    private void sortToAct(TypeElement typeElement, VariableElement variableElement) {

        List<ViewInfo> viewInfos;
        if (bindViews.get(typeElement) != null) {//判斷之前是否存儲過這個typeElement的ViewInfo集合
            viewInfos = bindViews.get(typeElement);
        } else {
            viewInfos = new ArrayList<>();
        }
        TestBindView annotation = variableElement.getAnnotation(TestBindView.class);//拿到注解
        int viewId = annotation.value();//獲取viewId
        String viewName = variableElement.getSimpleName().toString();//獲取viewName
        ViewInfo viewInfo = new ViewInfo(viewId, viewName);//生成viewinfo對象
        viewInfos.add(viewInfo);//放入集合
        bindViews.put(typeElement, viewInfos);//存入map中
    }

第三步驟生成代碼

 /**
     * 生成文件
     */
    private void writeToFile() {
        Set<TypeElement> typeElements = bindViews.keySet();
        String paramName = "target";
        for (TypeElement typeElement : typeElements) {
            ClassName className = ClassName.get(typeElement);//獲取參數(shù)類型
            PackageElement packageElement = (PackageElement) typeElement.getEnclosingElement();//獲得外部對象
            String packageName = packageElement.getQualifiedName().toString();//獲得包名
            List<ViewInfo> viewInfos = bindViews.get(typeElement);
            CodeBlock.Builder builder = CodeBlock.builder();//代碼塊對象
            for (ViewInfo viewInfo : viewInfos) {
                //生成代碼
                builder.add(paramName + "." + viewInfo.getViewName() + " = " + paramName + ".findViewById(" + viewInfo.getViewId() + ");\n");

            }

            FieldSpec fieldSpec = FieldSpec.builder(String.class,"name",Modifier.PRIVATE).build();//成員變量

            MethodSpec methodSpec = MethodSpec.constructorBuilder()//生成的方法對象
                    .addModifiers(Modifier.PUBLIC)//方法的修飾符
                    .addParameter(className, paramName)//方法中的參數(shù),第一個是參數(shù)類型,第二個是參數(shù)名
                    .addCode(builder.build())//方法體重的代碼
                    .build();

            TypeSpec typeSpec = TypeSpec.classBuilder(typeElement.getSimpleName().toString() + TestBindView.SUFFIX)//類對象,參數(shù):類名
                    .addMethod(methodSpec)//添加方法
                    .addField(fieldSpec)//添加成員變量
                    .build();

            JavaFile javaFile = JavaFile.builder(packageName, typeSpec).build();//javaFile對象,最終用來寫入的對象,參數(shù)1:包名;參數(shù)2:TypeSpec

            try {
                javaFile.writeTo(mFiler);//寫入文件
            } catch (IOException e) {
                e.printStackTrace();
            }

        }

注意:以上代碼都寫入了詳細的注釋,這里有有兩點要說一下
第一:獲得被標注的元素注解以后我們要判斷一下它的修飾符類型,不能是private或者protected,因為我們要通過Activity對象訪問它的控件對象,這一點跟Butterknife是一致的

第二:就是Element.getEnclosingElement()這個方法很重要,是獲取元素的封閉元素。啥叫封閉元素呢?舉個例子,就是如果你是一個VariableElement(成員變量元素),把你封閉起來的就是TypeElement(類元素);如果你是一個TypeElement的,把你封閉起來的就是PackageElement(包元素);如果你是一個PackageElement,那么返回的就是null了,因為沒有東西把包封閉起來。我們注解的作用域是成員變量,所以我們直接拿到VariableElement,然后再通過VariableElement就可以拿到TypeElement和PackageElement,這對我們后面來生成代碼非常重要。

BindingProcessor里面的代碼寫完以后,我們在App module下寫一個工具類用來加載生成的java文件同時調(diào)用其構(gòu)造方法

  public class ViewBindUtil {

    /**
     * 綁定Activity
     * */
    public static void bind(Activity activity) {
        if (activity == null) {
            return;
        }
        String activityName = activity.getClass().getName();//獲取類的全限定名
        ClassLoader classLoader = activity.getClass().getClassLoader();//獲得類加載器
        try {
            Class<?> loadClass = classLoader.loadClass(activityName + TestBindView.SUFFIX);//加載類
            Constructor<?> constructor = loadClass.getConstructor(activity.getClass());
            constructor.newInstance(activity);//調(diào)用其構(gòu)造方法
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

下面我們用一下咱們寫的注解解析器

public class MainActivity extends AppCompatActivity {

    @TestBindView(R.id.button)
    Button button;

    @TestBindView(R.id.textView)
    TextView textView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        ViewBindUtil.bind(this);
        button.setText("按鈕");
        textView.setText("文本");
    }
}

編譯一下項目,先看看生成的代碼,有木有問題


生成的代碼.jpg

最后運行一下,發(fā)現(xiàn)一切OK,這里就不貼圖,這樣一個簡單的TestBindView注解解析器就完成了,本文的重點是不是這個例子,而且教大家如何完成一個注解解析器,以后再有模板類的代碼咱們都可以考慮使用這種方法簡化,同時對一些優(yōu)秀的開源如何用注解實現(xiàn)的代碼也會更加清楚。

如何調(diào)試Processor中的代碼

考慮到大家剛開始使用注解解析器難免會存在一些問題,所以如何調(diào)試Processor中的代碼還是很有必要講一講。我們運行時的代碼都會調(diào)用,點一下綠色的??即可,而調(diào)試Processor的代碼略微麻煩一點點。

調(diào)試步驟1.jpg

調(diào)試步驟2.jpg

調(diào)試步驟三.jpg

調(diào)試步驟四.jpg

完成前面四步,接下來
點擊一下小蜘蛛.jpg

在Processor代碼里打好斷點
打好斷點.jpg

最后一步,切換成app,點擊運行
點擊運行.jpg

這里還有一個很重要的點需要強調(diào)一下,除了第一次運行外,其余每次運行都必須要在app module下改點代碼(哪怕是加個空格),否則,調(diào)試不會生效,切記!!!

總結(jié)

本文是系列文章IOC(依賴控制翻轉(zhuǎn))的最后一篇,終于打完收工了,如開篇所說掌握自定義注解解析器非常重要,未來大家要給公司搭建一些項目架構(gòu),難免會用到這項技術(shù)。本文首先介紹了AbstractProcessor和JavaPoet知識,然后交了如果使用AS搭建一個ioc項目,最后用了一個仿Butterknife的例子手擼一個自定義注解解析器的demo。

最后的最后,放上Demo的github地址:apt_demo,如果您覺得本文還不錯,記得給個贊,謝謝~

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,316評論 6 531
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,481評論 3 415
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,241評論 0 374
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,939評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,697評論 6 409
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,182評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,247評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,406評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,933評論 1 334
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 40,772評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,973評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,516評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 44,209評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,638評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,866評論 1 285
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,644評論 3 391
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,953評論 2 373

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