Android注解,這幾篇文章就夠了(三)自己寫個注解處理器

一 前言

前面兩篇文章,注解處理器理解注解,對注解有了一個初步認識,第二篇文章末尾也提到了,注解不是代碼的一部分,當開發者使用了Annotation注解以后,注解不會自己起作用,必須提供相應的代碼來處理這些信息。
這篇文章,我們就寫一個簡單的注解處理器,作用是類似于ButterKnife查找id。
源碼傳送門

二 項目結構

整個項目采用如下所示的結構:


    1. BindViewAnnotation,Java Library,存放我們定義的注解。
    1. bindviewapi,Android Library,聲明注解框架使用的api,本例子中,我們要實現的是查找view控件,并將控件和xml中的綁定。
    1. BindViewCompiler,Java Library,注解處理器,根據注解生成處理打代碼。

新建這幾個Library的過程不再陳述,特別注意的是,建BindViewCompiler Java Library時,在build.gradle下要加入以下代碼:

// 用于生成Java文件的庫
    implementation 'com.squareup:javapoet:1.11.1'
    implementation 'com.google.auto.service:auto-service:1.0-rc6'
    annotationProcessor 'com.google.auto.service:auto-service:1.0-rc6'

這些代碼后面會提及。

三 正式開始吧

(一)在BindViewAnnotation新建注解

前面說過了,我們要實現的功能是查找xml文件的id功能,注解歹意如下:

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.CLASS)

public @interface BindView {
    int value();
}

使用注解,在Activity中使用注解,在xml中定義一個button,

<Button
        android:id="@+id/btn_bind"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="BindButton"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

在Activity中使用我們定義的注解。

@BindView(R.id.btn_bind)
 public Button mButton;

前面說了,注解不會自動起作用,如果我們直接運行代碼,會直接報錯的,提示Button沒有定義,所以我們要寫代碼來處理這個注解的信息。

(二) 聲明注解框架用到的api

① 定義一個綁定注解的接口
public interface IViewBind<T> {
    void bind(T t);
}
② 向外提供的綁定方法,這里使用靜態方法來管理。
public class ViewBinder {

    public static void bind(Activity activity){

        try {

            // 1
            Class clazz=Class.forName(activity.getClass().getCanonicalName()+"$$ViewBinder");
           // 2
            IViewBind<Activity> iViewBinder= (IViewBind<Activity>) clazz.newInstance();
           // 3
            iViewBinder.bind(activity);
        } catch (ClassNotFoundException e) {
            Log.d("hbj--exp",e.getException()+"");
            Log.d("hbj--cause",e.getCause()+"");
            Log.d("hbj--mess",e.getMessage()+"");
            Log.d("hbj--class",e.getClass()+"");
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (InstantiationException e) {
            e.printStackTrace();
        }

    }
}

// 1 處獲取生成的類的名字,生成類的規則代碼在后面寫,生成的規則是 類名$$ViewBinder,例如在MainActivity中需要使用,生成的文件名字就是MainActivity$$ViewBinder.java。
// 2 獲取生成的類的實例。
// 3 完成綁定。
可能對第二條和第三條不是很好理解,現在貼出生成的java文件的源碼,結合生成的文件,應該就好理解了吧。

public class MainActivity$$ViewBinder< T extends MainActivity> implements IViewBind<T> {
@Override
public void bind(T activity) {
activity.mButton=activity.findViewById(2131165250);
}
}

(三) 根據注解生成代碼

現在只剩根據注解生成代碼。
創建一個自定義的Annotation Processor,繼承自AbstractProcessor。

  // 1
@AutoService(Processor.class)
public class MyProcessor extends AbstractProcessor {

   // 2
    @Override
    public synchronized void init(ProcessingEnvironment env){
    }
  
   // 3
    @Override
    public boolean process(Set<? extends TypeElement> annoations, RoundEnvironment roundEnv) { }

    // 4
    @Override
    public Set<String> getSupportedAnnotationTypes() { 
    }

    // 5
    @Override
    public SourceVersion getSupportedSourceVersion() {
    }
}

// 1處 @AutoService(Processor.class),向javac注冊我們自定義的注解處理器, 這樣,在javac編譯時,才會調用到我們這個自定義的注解處理器方法。
AutoService主要是生成 META-INF/services/javax.annotation.processing.Processor文件的。如果不加上這個注解的話,需要通過以下方法進行手動配置進行手動注冊:
1.在main下創建resources目錄,然后創建META-INF/services 目錄,最后在此目錄下創建文件:javax.annotation.processing.Processor,目錄如下,



文件里的內容是一些列的自定義注解處理器完整有效的類名集合,以換行符切割,這里就自定義了一個注解處理器,


注:但是有可能會出現使用@AutoService()無法動態生成入口文件的,這個問題可以如下解決:

這個要從google auto service 和META_INF,谷歌的 auto service 也是一種 Annotation Processor,它能自動生成 META-INF 目錄以及相關文件,避免手工創建該文件,手工創建的方法,上文有,手工創建有可能失誤。使用 auto service 中的 @AutoService(Processor.class) 注解修飾 Annotation Processor 類就可以在編譯過程中自動生成文件。

如果要進入的話,還要注意要引入兩個配置:

   implementation 'com.google.auto.service:auto-service:1.0-rc6'
   annotationProcessor 'com.google.auto.service:auto-service:1.0-rc6'

這兩個重復寫的原因是:annotationProcessor這個是新版 gradle 提供的 java plugin 內置的配置項,在gradle 5.+ 中將 Annotation Processor 從編譯期 classpath 中去除了,javac 也就無法發現 Annotation Processor。此處如果按照 gradle 4.+ 的寫法,只寫一個 implementation 是無法使用 auto service 的 Annotation Processor 的。必須要使用 annotationProcessor 來配置 Annotation Processor 使其生效。

// 2處 init(ProcessingEnvironment env): 每一個注解處理器類都必須有一個空的構造函數。然而,這里有一個特殊的init()方法,它會被注解處理工具調用,并輸入ProcessingEnviroment參數。ProcessingEnviroment提供很多有用的工具類Elements,Types和Filer。

// 3處 public boolean process(Set<? extends TypeElement> annoations, RoundEnvironment env)這相當于每個處理器的主函數main()。 在這里寫掃描、評估和處理注解的代碼,以及生成Java文件。輸入參數RoundEnviroment,可以讓查詢出包含特定注解的被注解元素。
// 4處 getSupportedAnnotationTypes(),這里必須指定,這個注解處理器是注冊給哪個注解的。注意,它的返回值是一個字符串的集合,包含本處理器想要處理的注解類型的合法全稱。換句話說,在這里定義你的注解處理器注冊到哪些注解上。

// 5處 getSupportedSourceVersion();用來指定你使用的Java版本。
下面給出BindViewProcessor的完整代碼:

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

  // 文件相關輔助類
  private Filer mFiler;
  // 日志相關輔助類
  private Messager mMessager;


  @Override
  public synchronized void init(ProcessingEnvironment processingEnv) {
      super.init(processingEnv);

      mFiler = processingEnv.getFiler();
      mMessager = processingEnv.getMessager();
  }


  @Override
  public SourceVersion getSupportedSourceVersion() {
      return SourceVersion.latestSupported();
  }

  @Override
  public Set<String> getSupportedAnnotationTypes() {
      Set<String> annotations=new LinkedHashSet<>();
      annotations.add(BindView.class.getCanonicalName());
      return annotations;
  }

  @Override
  public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
      Set<? extends Element> elements = roundEnv.getElementsAnnotatedWith(BindView.class);
      Map<TypeElement,ArrayList<BindViewInfo>> bindViewMap=new HashMap<>();
      for (Element element : elements) {
          // 因為BindView只作用于Filed,判斷注解是否是屬性,不是的話直接結束
          if (element.getKind() != ElementKind.FIELD) {
              mMessager.printMessage(Diagnostic.Kind.ERROR, element.getSimpleName().toString() + " is not filed,can not use @BindView");
              return false;
          }
          // 獲取注解元數據
          int id = element.getAnnotation(BindView.class).value();
          // 獲取屬性的類
          TypeElement typeElement= (TypeElement) element.getEnclosingElement();

          if (!bindViewMap.containsKey(typeElement)){
              bindViewMap.put(typeElement,new ArrayList<BindViewInfo>());
          }
          ArrayList<BindViewInfo> bindViewInfos=bindViewMap.get(typeElement);
          // 添加list
          bindViewInfos.add(new BindViewInfo(id,element.getSimpleName().toString()));
      }
      produceClass(bindViewMap);
      return true;
  }


  private void produceClass(Map<TypeElement,ArrayList<BindViewInfo>> hasMap){

      if (hasMap == null || hasMap.isEmpty()){
          return;
      }

      Set<TypeElement> typeElements=hasMap.keySet();
      for (TypeElement typeElement:typeElements){
          produceJavaClass(typeElement,hasMap.get(typeElement));
      }

  }

/**
   * 產生Java文件
   * @param typeElement
   * @param bindViewInfos
   */
  private void produceJavaClass(TypeElement typeElement, List<BindViewInfo> bindViewInfos){

      try {
          StringBuffer stringBuffer=new StringBuffer();
          stringBuffer.append("package ");
          stringBuffer.append(getPackageName(typeElement.getQualifiedName().toString())+";\n");
          stringBuffer.append("import com.jackson.bindviewapi.IViewBind;\n");
          stringBuffer.append("public class "+typeElement.getSimpleName()+"$$ViewBinder< T extends "+typeElement.getSimpleName()+"> implements IViewBind<T> {\n");
          stringBuffer.append("@Override\n");
          stringBuffer.append("public void bind(T activity) {\n");

          for (BindViewInfo bindViewInfo:bindViewInfos){
              stringBuffer.append("activity."+bindViewInfo.name+"=activity.findViewById("+bindViewInfo.id+");\n");
          }
          stringBuffer.append("}\n}");
          JavaFileObject javaFileObject=mFiler.createSourceFile(typeElement.getQualifiedName().toString()+"$$ViewBinder");
          Writer writer=javaFileObject.openWriter();
          writer.write(stringBuffer.toString());
          writer.close();
      } catch (IOException e) {
          e.printStackTrace();
      }
  }

  private String getPackageName(String className){
      if (className==null || className.equals("")){
          return "";
      }

      return className.substring(0,className.lastIndexOf("."));
  }
}

重新編譯項目,在app/build/source/apt/debug/com.jackson.annotationdemo下就會找到生成的文件,如下圖



生成文件的代碼,前面已經給出,可以對照著生成java文件的代碼,來看BindViewProcessor生成java文件的代碼規則。

(四) 使用

在MainActivity中使用,代碼如下:

 @BindView(R.id.btn_bind)
    public Button mButton;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        ViewBinder.bind(this);
        mButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                Toast.makeText(MainActivity.this,"注解測試",Toast.LENGTH_SHORT).show();
            }
        });
    }

可以看到,在MainActivity中,并沒有mButton的findViewById()來初始化,而是通過注解完成,代碼運行正常。
源碼傳送門

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

推薦閱讀更多精彩內容