參閱ButterKnife源碼,對編譯時注解(Annotation Processor)的理解總結

編譯時注解

運行時注解和編譯時注解,兩種注解方式對性能的影響是不一樣的。之前看到相關資料,都說對于注解的優(yōu)化,都用的是編譯時注解進行性能的提升。自己在使用的時候也查閱各種博客、第三方庫的代碼,對于編譯時注解的實現原理進行理解以及記錄。

編譯時注解框架基本構成

  • compiler
  • api
  • annotation

compiler

這部分主要是框架所使用的注解處理器(Annotation Processer),用于在編譯時掃描和處理注解。也可以自定義,并且處理自己的注解邏輯。

自定義注解器需要繼承 AbstractProcessor ,并且實現四個方法:

  • init
  • process
  • getSupportedAnnotationTypes
  • getSupportedSourceVersion

init 方法

處理器的初始化

    @Override
    public synchronized void init(ProcessingEnvironment processingEnvironment) {
        super.init(processingEnvironment);
        
        //錯誤處理工具
        mMessager = processingEnvironment.getMessager();

        //Filer可以創(chuàng)建文件
        mFileUtils = processingEnvironment.getFiler();

        //Element代表程序的元素,例如包、類、方法。
        mElementUtils = processingEnvironment.getElementUtils();
        
        //處理TypeMirror的工具類,用于取類信息
        mTypeUtils = env.getTypeUtils();

    }

process

/**
* 處理器實際處理邏輯入口
* @param set
* @param roundEnvironment 所有注解的集合
* @return 
*/
 @Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {

        mMessager.printMessage(Diagnostic.Kind.NOTE,"Start BindProcesser process method!!");

        //獲取注解的元素變量
        Set<? extends Element> elements = roundEnvironment.getElementsAnnotatedWith(BindView.class);

        //循環(huán)處理注解的每一個元素,并且放入一個 map 中
        for(Element element : elements){

            //校驗元素是否為 VariableElement
            if(!(element instanceof VariableElement)){
                return false;
            }

            //轉換變量類型
            VariableElement variableElement = (VariableElement)element;

            //修飾變量所在的類
            TypeElement typeElement =  (TypeElement)variableElement.getEnclosingElement();

            //使用類的全路徑作為key
            String qulifiedName = typeElement.getQualifiedName().toString();

            //獲取 map 中是都已經有相關的代理信息
            ProxyInfo proxyInfo = mProxyInfoMap.get(qulifiedName);

            if(proxyInfo == null){
                proxyInfo = new ProxyInfo(typeElement,mElementUtils);
                mProxyInfoMap.put(qulifiedName,proxyInfo);
            }

            //獲取注解
            BindView annotation = variableElement.getAnnotation(BindView.class);
            //注解上的控件ID
            int id = annotation.value();
            proxyInfo.injectVariables.put(id,variableElement);

            //第二步驟: 遍歷Map生成代理類
            for(String key: mProxyInfoMap.keySet()){
                ProxyInfo proxyInfo2 = mProxyInfoMap.get(key);

                try {
                    //創(chuàng)建文件對象
                    JavaFileObject soureFile = mFileUtils.createSourceFile(
                            proxyInfo2.getProxyClassFullName(),   //文件名,全路徑
                            proxyInfo2.getTypeElement());
                    //創(chuàng)建寫入對象
                    Writer writer = soureFile.openWriter();
                    //寫入內容
                    writer.write(proxyInfo2.generateJavaCode());
                    writer.flush();
                    writer.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }

            }
        }

        return true;
    }
     

以上代碼是參照文章</br>
https://blog.csdn.net/niubitianping/article/details/78492054

處理器的邏輯:

  1. 遍歷得到源碼中,需要解析的元素列表。</br>
    Set<? extends Element> elements = roundEnvironment.getElementsAnnotatedWith(BindView.class);</br>
    Processor的過程中,回遍歷所有的java源碼,查找到相關的元素 Element。代碼的每一個部分就相當于一個 Element ,每個Element代表一個靜態(tài)的、語言級別的構件。</br>
    PackageElement -- 包 </br>
    TypeElement -- 類 </br>
    VariableElement -- 變量 </br>
    ExecuteableElement -- 方法 </br>

  2. 判斷元素是否可見和符合要求。</br>
    獲取所有的Element之后,則開始校驗判斷是否可用或者是符合自己的業(yè)務邏輯。</br>

     //校驗元素是否為 VariableElement
     if(!(element instanceof VariableElement)){
         return false;
     }
     
     //或者通過如下api校驗元素是否可用
     SuperficialValidation.validateElement(element);
     
     // 或者檢查元素是否是一個類
      if (element.getKind() != ElementKind.CLASS) {
            ...
      }
    
  • 組織數據結構得到輸出類參數。
    例如:</br>

    //修飾變量所在的類
    TypeElement typeElement =  (TypeElement)variableElement.getEnclosingElement();
    
    //使用類的全路徑作為key
    String qulifiedName = typeElement.getQualifiedName().toString();
    
    //獲取 map 中是都已經有相關的代理信息
    ProxyInfo proxyInfo = mProxyInfoMap.get(qulifiedName);
    
    if(proxyInfo == null){
        proxyInfo = new ProxyInfo(typeElement,mElementUtils);
        mProxyInfoMap.put(qulifiedName,proxyInfo);
    }
    
    //獲取注解
    BindView annotation = variableElement.getAnnotation(BindView.class);
    //注解上的控件ID
    int id = annotation.value();
    proxyInfo.injectVariables.put(id,variableElement);
    
    //第二步驟: 遍歷Map生成代理類
    for(String key: mProxyInfoMap.keySet()){
        ProxyInfo proxyInfo2 = mProxyInfoMap.get(key);
    
  • 輸入生成java文件。

    //第二步驟: 遍歷Map生成代理類
    for(String key: mProxyInfoMap.keySet()){
        ProxyInfo proxyInfo2 = mProxyInfoMap.get(key);
    
        try {
            //創(chuàng)建文件對象
            JavaFileObject soureFile = mFileUtils.createSourceFile(
                    proxyInfo2.getProxyClassFullName(),   //文件名,全路徑
                    proxyInfo2.getTypeElement());
            //創(chuàng)建寫入對象
            Writer writer = soureFile.openWriter();
            //寫入內容
            writer.write(proxyInfo2.generateJavaCode());
            writer.flush();
            writer.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
            }
    

    Java代碼生成有兩種方式:</br>
    一種是 使用 Filer 進行生成,如下代碼:</br>

    /**
     * 生成java文件代碼
     * @return
     */
    public String generateJavaCode() {
        StringBuilder builder = new StringBuilder();
        builder.append("http:// Generated code. Do not modify!\n");
        builder.append("package ").append(packageName).append(";\n\n");
    
        //注意,這個ImPort的包路徑,是api的包路徑
        builder.append("import com.example.qhh_api.*;\n");
        builder.append('\n');
    
        builder.append("public class ").append(proxyClassName).append(" implements " + ProxyInfo.PROXY + "<" + mTypeElement.getQualifiedName() + ">");
        builder.append(" {\n");
    
        generateMethods(builder);
        builder.append('\n');
    
        builder.append("}\n");
        return builder.toString();
    
    }
    
    
    /**
     * 生成方法
     * @param builder
     */
    private void generateMethods(StringBuilder builder) {
    
        builder.append("@Override\n ");
        builder.append("public void inject(" + mTypeElement.getQualifiedName() + " host, Object source ) {\n");
    
        for (int id : injectVariables.keySet()) {
            VariableElement element = injectVariables.get(id);
            String name = element.getSimpleName().toString();
            String type = element.asType().toString();
            builder.append(" if(source instanceof android.app.Activity){\n");
            builder.append("host." + name).append(" = ");
            builder.append("(" + type + ")(((android.app.Activity)source).findViewById( " + id + "));\n");
            builder.append("\n}else{\n");
            builder.append("host." + name).append(" = ");
            builder.append("(" + type + ")(((android.view.View)source).findViewById( " + id + "));\n");
            builder.append("\n}");
        }
        builder.append("  }\n");
    }
    

    第二種方式,使用 JavaPoet 庫,導包方式:</br>
    compile 'com.squareup:javapoet:1.9.0' </br>
    最新 'com.squareup:javapoet:1.11.1'

    注意:JavaPoet 也是必須在 Java Library 中使用,因為Javax的核心包在 Android module 和 Library中都不存在。Android Library 調用 Java Library中的方法包含 javax 庫文件的會報錯。

    MethodSpec methodSpec = MethodSpec.methodBuilder("inject")
                    .addModifiers(Modifier.PRIVATE)
                    .returns(void.class)
                    .addParameter(String.class, "id")
                    .addStatement("String view = id")
                    .build();
    
    TypeSpec typeSpec = TypeSpec.classBuilder("ViewInject")
            .addModifiers(Modifier.PUBLIC)
            .addMethod(methodSpec)
            .build();
    
    JavaFile javaFile = JavaFile.builder("com.sensetime.test", typeSpec)
            .build();
    
    try {
        javaFile.writeTo(mFileUtils);
    } catch (IOException e) {
        e.printStackTrace();
    }
    

    JavaPoet 的使用可以參考:</br>
    https://github.com/square/javapoet </br>
    https://juejin.im/post/584d4b5b0ce463005c5dc444

  • 錯誤處理。

在工程中生成 compiler(Annotation Processer),需要生成 Java Library,而不是 Android Library 。

總結

主要是通過一些網上的Demo,以及ButterKnife的源碼,來理解編譯時注解框架的工作原理。簡單的使用編譯時注解,完成對項目的一些解耦探索。</br>
在使用依賴的時候,出現在Activity中輸入自定義的注解,無法自動提示,只能輸入全稱的問題。找了好多好多參考,最終自己輸入全稱實現,很尷尬。

小小的demo : https://github.com/qinhaihang/AnnotationProcesserDemo

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內容

  • 上一篇我們講解了ButterKnife的設計思想,理解了ButterKnife綁定相關源碼的實現邏輯。但是它是怎么...
    Ihesong閱讀 1,099評論 0 2
  • 前言 對注解,一開始是在學習java的時候接觸到的,就是在《Thinking in Java》里草草看過。后來開發(fā)...
    許方鎮(zhèn)閱讀 4,904評論 0 6
  • 一名合格的SEO人優(yōu)化的網站,應該有清晰合理的網絡結構和權重分配,除此之外,更應該有完善的關鍵詞庫。今天,老鐵se...
    大山小帥閱讀 1,022評論 0 1
  • 格西老師說:無論是你想要成為一個優(yōu)秀的老師,或者一個優(yōu)秀的管理者,或者一個優(yōu)秀的父母,你都可以用這個方法。這個是來...
    閃光的種子閱讀 372評論 0 2
  • 膝關節(jié)一針不但當場解決了臏下脂肪墊內側副韌帶損傷引起的疼痛,改善了膝關節(jié)的曲伸困難,而且關節(jié)積液三天后基本消除。我...
    鄭州陳明濤閱讀 237評論 0 0

友情鏈接更多精彩內容