1. 程式人生 > >自定義註解框架的那些事

自定義註解框架的那些事

構造 -i 靜態 cti tails cas 數組 講解 調用

一、前言

距離上次更新已過一個半月,工作太忙也不是停更的理由。我這方面做得很不好,希望大家給予監督。首先會講解【編譯期資源註入】,接著是【下拉刷新註入】(通過註解來實現下拉刷新功能),最後打造一款【特色的註解框架】。

大家準備好公交卡了嗎,開車了 …

技術分享

二、什麽是註解

每位童鞋對 註解 都有自己的理解,字面上的意思就是【額外的加入】,在項目當中使用的註解的框架已經越來越多,如 : retrofit ,butterknife,androidannotations … 2017年Android百大框架排行榜 基本都會看到註解的身影 。

註解可以減少大量重復的工作,提高開發效率,註解也非常的靈活,可以註解到 類、方法、屬性等

當中,用來自動完成一些規律性的代碼,以及降低類與類之間的耦合度。

前一篇文章對運行時RUNTIME事件的註解做了一個簡單的講解 初談Android-Annotations(二) , 本篇主要簡單講解編譯時CLASS**res**文件下的資源註入。前者一般是通過反射來實現的,影響效率,接下來一起來了解下編譯時CLASS的實現。

三、編譯時的註解

類似 butterknife,androidannotations,arouter 使用的都是編譯時的註解CLASS。這裏以 butterknife 為例,來看一看以下註解:

/**
 * Bind a field to the specified string resource ID.
 * <pre><code>
 * [email protected]
/* */ @}BindString(R.string.username_error) String usernameErrorText; * </code></pre> */ @Retention(CLASS) @Target(FIELD) //編譯時註解 針對屬性(成員變量)註解 public @interface BindString { /** String resource ID to which the field will be bound. */ @StringRes int value(); }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

如上定義了資源字符串的註解,那麽怎麽去實現字符串的註入的呢?

這裏就不得不提到註解處理器 AbstractProcessor,註解處理器是 javac 的一個工具,它用來在編譯時掃描和處理註解(Annotation)。以編譯過的字節碼作為輸入,生成文件(一般是使用 javapoet 生成 .Java 文件)作為輸出,生成的 Java 文件一樣被 javac 編譯。同樣運行時註解 (RetentionPolicy.RUNTIME) 和源碼註解 (RetentionPolicy.SOURCE) 也可以在註解處理器進行處理。

註意:通過註解處理器處理註解是不能修改已經存在的 Java 類。例如增刪一些方法。

四、字符串資源的註入

顏色(color),數組,尺寸的註入與字符串的註入類似,這裏以字符串的註入來講解。

先看看最終的實現效果:

技術分享

log的打印日誌:

技術分享

通過日誌可以看出:前面兩個屬性並沒有在註解中引用字符串的資源,最終的結果和引用 R.string.app_name 的結果一樣,這樣可以節省代碼量(為懶人服務)。目前支持駝峰式命名與前綴m命名,規則由你改寫,隨性打造屬於你自己的註入框架。

基礎知識科普

如下所示,實現一個自定義註解處理器,至少重寫四個方法,並且註冊你的自定義Processor

  • @AutoService(Processor.class),谷歌提供的自動註冊註解,為你生成註冊Processor所需要的格式文件。請在當前庫的 gradle 文件添加如下依賴:
dependencies {
    compile fileTree(dir: ‘libs‘, include: [‘*.jar‘])
    compile ‘com.google.auto.service:auto-service:1.0-rc3‘//添加
    compile ‘com.google.auto:auto-common:0.8‘//添加
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 1
  • 2
  • 3
  • 4
  • 5
  • init(ProcessingEnvironment processingEnv) 初始化處理器,一般在這裏獲取我們需要的工具類

  • getSupportedAnnotationTypes() 返回註解器所支持的註解類型集合

  • getSupportedSourceVersion 返回Java版本

  • process 當於每個處理器的主函數main(),處理註解的邏輯(掃描、檢驗,以及生成Java文件),如果返回 true,則這些註解已聲明並且不要求後續 Processor 處理它們,反之則要求後續 Processor 處理它們

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

    private Types typeUtils; //類型工具
    private Elements elementUtils;//元素工具
    private Filer filer; //文件管理器
    private Messager messager;//處理異常

    @Override
    public synchronized void init(ProcessingEnvironment processingEnv) {
        super.init(processingEnv);
        typeUtils = processingEnv.getTypeUtils();//獲取類的信息
        elementUtils = processingEnv.getElementUtils();//獲取程序的元素 如 包 類 方法
        filer = processingEnv.getFiler();//生成java文件
        messager = processingEnv.getMessager();//處理錯誤日誌
    }

     /** 
     * @param annotations   請求處理的註解類型 
     * @param roundEnv  有關當前和以前的信息環境 
     * @return 
     */  
    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        return false;
    }



    /**
     * @return 返回的是 註解類型的合法全稱集合,如果沒有這樣的類型,則返回一個空集合
     */
    @Override
    public Set<String> getSupportedAnnotationTypes() {
        Set<String> annotations = new LinkedHashSet<String>();
        annotations.add(BindString.class.getCanonicalName());
        return annotations;
    }

    /**
     * 
     * @return 通常返回SourceVersion.latestSupported()
     */
    @Override
    public SourceVersion getSupportedSourceVersion() {
        return SourceVersion.latestSupported();
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48

首先,我們梳理一下註解處理器的處理邏輯:

  • 遍歷備註解的元素

  • 檢驗元素是否符合要求

  • 獲取輸出類參數

  • 生成 java 文件

  • 錯誤處理

接著來看個簡單示例獲取被註解的元素名稱,與註解值:

    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        // roundEnv.getElementsAnnotatedWith()返回使用給定註解類型的元素
        for (Element element : roundEnv.getElementsAnnotatedWith(BindString.class)) {
            System.out.println("----------------------");
            // 判斷元素的類型為Class
            if (element.getKind() == ElementKind.FIELD) {
                // 顯示轉換元素類型
                VariableElement  variableElement= (VariableElement) element;
                // 輸出元素名稱
                System.out.println(""+variableElement.getSimpleName());
                // 輸出註解屬性值
                System.out.println(""+variableElement.getAnnotation(BindString.class).value());
            }
            System.out.println("----------------------");
        }
        return false;
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18

MainActivity 類:

    @BindString(R.string.app_name)
    String appName;
  • 1
  • 2
  • 1
  • 2

Gradle console 控制臺輸出如下:

技術分享

看到這裏,你一定會想,怎麽樣才能使 appName2131099681建立聯系呢?

如果不使用註解是這樣來建立聯系的:

    appName = getResources().getString(2131099681); // R.string.app_name =  2131099681
  • 1
  • 1

說一萬道一千,實現這行代碼的自動註入是我們的最終目的。

最後,我們來理解一下 Element 的概念,因為它是我們獲取註解的基礎。

Processor 處理過程中,會掃描全部的 Java 源碼,代碼的每一個部分都是一個特定類型的 Element ,它們像是XML一層的層級機構,比如類、變量、方法等,每個Element代表一個靜態的、語言級別的構件。

Element 有五個直接子接口,它們分別代表一種特定類型的元素,如下:

  • PackageElement 表示一個包程序元素

  • TypeElement 表示一個類,接口或枚舉程序元素 類型

  • VariableElement 表示一個字段、enum 常量、方法或構造方法參數、局部變量或異常參數 屬性

  • ExecutableElement 表示某個類或接口的方法、構造方法或初始化程序方法(靜態或實例),包括註解類型元素 方法

  • TypeParameterElement 表示一般類、接口、方法或構造方法元素的泛型參數 如 public class MainActivity<T> 泛型 T

在開發中Element可根據實際情況強轉為以上5種中的一種,它們都帶有各自獨有的方法,來看個簡單的例子:

package com.github.wsannotation; // PackageElement

import java.util.List;

/**
 * desc:
 * author: wens
 * date: 2017/8/11.
 */

public class UserBean      // TypeElement
        <T extends List> {  // TypeParameterElement

    private int age;           // VariableElement
    private String name;        // VariableElement

    public UserBean() {          // ExecutableElement
    }

    public void setName(String name) {  // ExecutableElement
       String weight;               // VariableElement
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23

其中 Element 代表的是源代碼,而 TypeElement代表的是源代碼中的類型元素,例如類。然而,TypeElement並不包含類本身的信息。你可以從TypeElement中獲取類的名字,但是你獲取不到類的信息,例如它的父類。這種信息需要通過TypeMirror獲取。你可以通過調用elements.asType()獲取元素的TypeMirror

相關文章鏈接:

自定義註解之編譯時註解(RetentionPolicy.CLASS)(三)—— 常用接口介紹

字符串註入流程

這裏參考了 ButterKnife 結合 androidannotations 的實現方式:

1、對元素效驗 verify 系列方法

2、獲取註解字段名和註解的資源ID以及處理資源ID為 -1 的情況

處理資源ID為 -1 的情況,我這裏借鑒了 androidannotations 的處理方式:

        //處理默認情況
        if (resId == -1) {
            TypeElement androidRType = elementUtils.getTypeElement("com.github.butterknifelib.R.string");
            List<? extends Element> idEnclosedElements = androidRType.getEnclosedElements();
            List<VariableElement> idFields = ElementFilter.fieldsIn(idEnclosedElements);
            for (VariableElement idField : idFields) {
                TypeKind fieldType = idField.asType().getKind();
                if (fieldType.isPrimitive() && fieldType.equals(TypeKind.INT)) {
                //制定規則
                    if (idField.getSimpleName().toString().toLowerCase().replaceAll("_", "")
                            .equals(name.startsWith("m") ? name.substring(1, name.length()).toLowerCase() : name.toLowerCase())) {
                        resId = (int) idField.getConstantValue();
                        break;
                    }
                }
            }
        }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

更好的方式是以 map 鍵值對來存儲資源類 。後期我會做詳細講解。

3、生成 xxxx$$ViewBinder.java (xxxx 代碼 Activity、View、Dialog)文件

4、給生成 xxxx$$ViewBinder.java 添加資源信息

5、通過 ButterKnife.bind(this); 反射得到 xxxx$$ViewBinder.java 類,並調用註入資源的方法

自定義註解框架的那些事