Android Annotation掃盲筆記

weixinjie發表於2018-02-05

寫在前面

今年大家都在搞元件化,元件化開發不可避免的需要用到路由(Router)來完成元件之間資料的互動,這就促進了各種路由發展如:阿里的ARouter以及ActivityRouter等優秀的Router框架。為了方便大家的開發這些Router庫以及像ButterKnife這類的庫都用到了註解技術。本篇目的是進行一波掃盲。

本文導讀

  • Android Annotation基礎
  • 解析Annotation
  • 實戰-自己做一個ButterKnife

1 Annotatin基礎

1.1 基本Annotation

Java提供了三個基本的Annotation註解,使用時需要在Annotation前面增加@符號,並把Annotation當成一個修飾符來使用。注:Java提供的基本Annotation註解都在java.lang包下面

  1. @Override:限定重寫父類方法:這個註解主要是命令編譯器幫助我們檢查程式碼(避免出現方法名寫錯這種低階錯誤),子類使用了Override註解之後,編譯的時候IDE會檢查父類中是否有這個函式,如果沒有這個函式會編譯失敗。
public class Animal {
    public void run() {
        //TODO
    }
}

複製程式碼
public class Monkey extends Animal {
    @Override
    public void run() {
        //使用了OVerride註解之後,必須重寫父類方法
    }
}
複製程式碼
  1. @Deprecated:標記該類或者方法已經過時。修改上面的Animal類,使用Deprecated修飾run方法後,子類在使用run方法是IDE會報警告。
public class Animal {
    @Deprecated
    public void run() {
        //TODO
    }
}
複製程式碼

Android Annotation掃盲筆記
3. @SuppressWarnings:抑制編譯器警告(用的比較少)。Java程式碼編譯時IDE往往會給開發者很多警告資訊,例如變數沒有使用等,這種警告多了之後很大程度上影響我們debug效率。此註解就是來抑制這些警告。舉個例子:

 @SuppressWarning("unused")
 public void foo() {
  String s;
 }
複製程式碼

如果不使用@SuppressWarning來抑制編譯器警告,上面的程式碼會被警告變數s從未使用。出了"unused",該註解支援的抑制型別還有下圖的內容(注該圖摘自IBM Knowledge Center)。

Android Annotation掃盲筆記

1.2 JDK元Annotation

JDK出了在java.lang包中提供了1.1介紹的幾種基本Annotation外還在java.lang.annotation包下面提供了四個Meta Annotation(元Annotation)。這四種元Annotation都是來修飾自定義註解的。(hold住節奏,看完這個小結我們們就可以自定義Annotation了)

  1. @Retention註解。該註解只能修飾一個Annotation定義,用於指定所修飾的Annotation可以保留多長"時間"(也可是說是保留的週期)。這裡說的“時間”有三種型別

Android Annotation掃盲筆記

  • RetentionPolicy.SOURCE:沒啥用,編譯器會直接忽略這種策略的註釋
  • RetentionPolicy.CLASS:自定義註解的預設值,編譯器會把這種策略的註釋儲存在class檔案中。像ButterKnife中的BindView註解就是用的這種方式。
  • RetentionPolicy.RUNTIME:編譯器會把該策略的註釋儲存到class檔案中,程式可以通過反射等方式來獲取。

舉個例子,自定義一個BindView註解(看不懂沒關係,現有一個感性的認識,下一節開始做自定義Annotation講解)。

//此註解的作用域是Class,也就是編譯時
@Retention(value = RetentionPolicy.CLASS)
public @interface BindView {
    int id() default 0;
}
複製程式碼

當成員變數為value時,可以省略。也就是說上述程式碼可以換成 @Retention(RetentionPolicy.CLASS)

  1. @Target註解:這貨也是用於修飾一個自定義的Annotation註解,用於指定自定義註解可以修飾哪些程式元素。該註解的成員變數有
  • ElementType.PACKAGE 註解作用於包
  • ElementType.TYPE 註解作用於型別(類,介面,註解,列舉)
  • ElementType.ANNOTATION_TYPE 註解作用於註解
  • ElementType.CONSTRUCTOR 註解作用於構造方法
  • ElementType.METHOD 註解作用於方法
  • ElementType.PARAMETER 註解作用於方法引數
  • ElementType.FIELD 註解作用於屬性
  • ElementType.LOCAL_VARIABLE 註解作用於區域性變數 同樣的,成員變數名為value時可以省略。我們豐富一下上面用到的自定義的BindView註解:
//此註解修飾的是屬性
@Target(ElementType.FIELD)
//此註解的作用域是Class,也就是編譯時
@Retention(value = RetentionPolicy.CLASS)
public @interface BindView {
    int id() default 0;
}
複製程式碼
  1. @Documented註解,該註解修飾的自定義註解可以使用javac命令提取成API文件。
  2. @Inherited註解,該註解修飾的自定義具有繼承性。舉個例子Animal類使用了@Inherited修飾的自定義註解,則子類Monkey也具有該自定義註解描述的特性。

1.3 自定義註解

  1. 定義Annotation,以上面使用的自定義BindView註解為例。可以直接新建Annotation型別的java檔案。

Android Annotation掃盲筆記

  1. 根據自己的需要,使用1.2的只是對自定義的註解進行修飾
/**
 * Created by will on 2018/2/4.
 */
@Documented
//此註解修飾的是屬性
@Target(ElementType.FIELD)
//此註解的作用域是Class,也就是編譯時
@Retention(value = RetentionPolicy.CLASS)
public @interface BindView {
 
}

複製程式碼
  1. 定義成員變數,自定義註解的成員變數以方法的形式來定義。豐富一下上面的BindView,由於這個自定義註解的功能是對Activity中的View進行繫結。所以我們定義一個id成員變數。
/**
 * Created by will on 2018/2/4.
 */
@Documented
//此註解修飾的是屬性
@Target(ElementType.FIELD)
//此註解的作用域是Class,也就是編譯時
@Retention(value = RetentionPolicy.CLASS)
public @interface BindView {
    int id();
}
複製程式碼
  1. 使用default關鍵字為成員變數指定預設值。繼續豐富BindView的程式碼。注default關鍵字放到int id() 後面。
@Documented
//此註解修飾的是屬性
@Target(ElementType.FIELD)
//此註解的作用域是Class,也就是編譯時
@Retention(value = RetentionPolicy.CLASS)
public @interface BindView {
    int id() default 0;
}
複製程式碼

根據有沒有成員變數,我們可以將Annotation劃分成兩種:

  • 沒有成員變數的註解稱為"標記Annotation",這種註解使用自身是否存在為我們提供資訊,例如Override等註解
  • 有成員變數的稱謂"後設資料Annotation"。我們可以使用apt等工具對這種Annotation的成員進行二次加工。

注意:只定義了自定義註解沒有任何效果,還需要對Annotation的資訊進行提取與加工!!!

上面我們自定義了BindView註解,你是不是想直接拿到Activity中使用呢?例如:

Android Annotation掃盲筆記
然後你發現Crash了。。。這就要引入下一節的內容了,使用apt對被註解的程式碼進行二次加工。

2. 解析Annotation

完成自定義Annotation後,我們還需要知道,針對這些註解,我們要做哪些相關的處理,這就涉及到了Annotation的解析操作。 解析Annotation,通常分為:對執行時Annotation的解析、對編譯時Annotation的解析; 解析Annotation,其實就是如何從程式碼中找到Annotation,通常我們的做法是:

  • 用反射的方式獲取Annotation,執行時Annotation的解析方式
  • 藉助apt工具獲取Annotation,編譯時Annotation的解析方式
  • 另外如果我們需要生成額外的程式碼、檔案,則還需要藉助JavaPoet API

2.1 利用反射解析Annotation

反射的解析方式,通常運用在執行時Annotation的解析。 反射是指:利用Class、Field、Method、Construct等reflect物件,獲取Annotation

  • field.getAnnotation(Annotation.class):獲取某個Annotation
  • field.getAnnotations():獲取所有的Annotation
  • field.isAnnotationPresent(Annotation.class):是否存在該Annotation

通常使用Runtime修飾的註解需要使用反射來配合解析

@Retention(value = RetentionPolicy.RUNTIME)

  1. 新建一個test自定義註解
/**
 * Created by will on 2018/2/4.
 */
@Documented
//此註解修飾的是屬性
@Target(ElementType.FIELD)
@Retention(value = RetentionPolicy.RUNTIME)
public @interface test {
    int id() default 0;
}
複製程式碼
  1. 新建一個java類Animal,並新增test註解
public class Animal {
    @BindView(id = 1000)
    String a;

    @Deprecated
    public void run() {
        //TODO
    }
}
複製程式碼
  1. 可以使用反射來獲取a的註解成員屬性值
 private void testMethod() {
        Class clazz = Animal.class;
        Field[] fields = clazz.getDeclaredFields();
        for (Field field : fields) {
            BindView bindView = field.getAnnotation(BindView.class);
            if (bindView != null) {
                int id = bindView.id();
                Log.e("------", String.valueOf(id));
            }
        }
    }
複製程式碼

2.2 使用apt工具來解析Annotation

APT:是一個註解處理工具 Annotation Processing Tool 作用:利用apt,我們可以找到源代中的註解,並根據註解做相應的處理

  • 根據註解,生成額外的原始檔或其他檔案
  • 編譯生成的原始檔和原來的原始檔,一起生成class檔案

利用APT,在編譯時生成額外的程式碼,不會影響效能,只是影響專案構建的速度

這裡我們說一下Android中使用apt的步驟 Android中開發自定義的apt學會兩個庫及一個類基本就足夠了

  • JavaPoet API 這個庫的主要作用就是幫助我們通過類呼叫的形式來生成程式碼,簡單理解就是利用這個庫可以生成額外的Java程式碼。具體的API可以去github上看下,寫的很詳細。這裡不貼程式碼了。
  • AutoService 這個庫是Google開發的,主要的作用是註解 processor 類,並對其生成 META-INF 的配置資訊。可以理解使用這個庫之後編譯的時候IDE會編譯我們的Annotation處理器,只需要在自定義的Processor類上新增註釋 @AutoService(Processor.class)下面會用到。
  • Processor類,我們自定義的Annotation處理器都需要實現該介面,Java為我們提供了一個抽象類實現了該介面的部分功能,我們自定義Annotation處理器的時候大部分只需要繼承AbstractProcessor這個抽象類就行了。

JavaPoet的學習可以直接借鑑官方api,AutoService學習成本較低(只需要用裡面一句程式碼而已,學習成本可以忽略),下面我們重點學習一下AbstractProcessor的使用。

AbstractProcessor介紹

  1. AbstractProcessor方法介紹:下面新建一個AbstractProcessor來看下這貨的方法
/**
 * Created by will on 2018/2/5.
 */

public class CustomProcessor extends AbstractProcessor {
    @Override
    public synchronized void init(ProcessingEnvironment processingEnvironment) {
        super.init(processingEnvironment);
    }

    @Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
        return false;
    }

    @Override
    public Set<String> getSupportedAnnotationTypes() {
        return super.getSupportedAnnotationTypes();
    }

    @Override
    public SourceVersion getSupportedSourceVersion() {
        return super.getSupportedSourceVersion();
    }
}
複製程式碼
  • init(ProcessingEnvironment processingEnvironment): 每一個註解處理器類都必須有一個空的建構函式。然而,這裡有一個特殊的init()方法,它會被註解處理工具呼叫,並輸入ProcessingEnviroment引數。ProcessingEnviroment提供很多有用的工具類Elements,Types和Filer。
  • process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment): 這相當於每個處理器的主函式main()。你在這裡寫你的掃描、評估和處理註解的程式碼,以及生成Java檔案。輸入引數RoundEnviroment,可以讓你查詢出包含特定註解的被註解元素。以前面提到的自定義註解BindView為例,這裡可以查到所有註解了BindView的Activity。
  • getSupportedAnnotationTypes(): 這裡必須由開發者指定,該方法返回一個Set,作用是這個註解的處理器支援處理哪些註解。
  • getSupportedSourceVersion(): 用來指定你使用的Java版本。通常這裡返回SourceVersion.latestSupported()。然而,如果你有足夠的理由只支援Java 7的話,你也可以返回SourceVersion.RELEASE_7。
  1. AbstractProcessor基礎工具解析:從AbstractProcessor的init方法中可以獲取一系列的工具來輔助我們解析原始碼
  • Elements工具類 在AbstractProcessor的init方法中可以獲取到一個Elements工具類,具體程式碼為
 @Override
    public synchronized void init(ProcessingEnvironment processingEnv) {
        super.init(processingEnv);
       Elements elementUtils = processingEnv.getElementUtils();
    }
複製程式碼

這個工具類是用來處理原始碼的,在自定義註解處理器的領域裡面,Java原始碼每一個型別都屬於一個Element,具體使用方法可以直接參考Java官方文件

package com.example;    // PackageElement

public class Test {        // TypeElement

    private int a;      // VariableElement
    private Test other;  // VariableElement

    public Test () {}    // ExecuteableElement

    public void setA (  // ExecuteableElement
                     int newA   // TypeElement
                     ) {}
}
複製程式碼

例如,我有一個TypeElement,希望拿到這個class所在的包名就可以使用Elemnts這個工具

 private String getPackageName(TypeElement type) {
        return elementUtils.getPackageOf(type).getQualifiedName().toString();
    }
複製程式碼

再來一個栗子,有一個代表Test的TypeElement,希望獲取所有的子元素可以這麼寫(注意,這個很有用)

TypeElement testClass = ... ;  
for (Element e : testClass.getEnclosedElements()){ // iterate over children  
    Element parent = e.getEnclosingElement();  // parent == testClass
}
複製程式碼
  • Types:一個用來處理TypeMirror的工具類; TypeElement並不包含類本身的資訊。你可以從TypeElement中獲取類的名字,但是你獲取不到類的資訊,例如它的父類。這種資訊需要通過TypeMirror獲取。你可以通過呼叫elements.asType()獲取元素的TypeMirror。
  • Filer:正如這個名字所示,使用Filer你可以建立檔案。

好了枯燥的基礎知識看完了之後我們一起寫一個簡單的ButterKnife

3. 自己寫一個輕量級的ButterKnife

1. 新建一個Java專案,名字為annotations

  1. 這個專案用來定義所有自定義的註解,這部分用到了第一節的知識基礎。

Android Annotation掃盲筆記

  1. 在這個專案包裡面新建自定義的註解,我們模仿ButterKnife,這裡增加一個BindView的註解

Android Annotation掃盲筆記

@Documented
//此註解修飾的是屬性
@Target(ElementType.FIELD)
//此註解的作用域是Class,也就是編譯時
@Retention(value = RetentionPolicy.CLASS)
public @interface BindView {
    int id() default 0;
}

複製程式碼

2 新建Java專案,名稱為annotations_compiler

  1. 這個專案是用來處理自定義註解的,這裡姑且叫這個專案為BindView的處理器,這裡需要第二節的知識基礎
  2. 在build.gradle檔案中新增AutoService與JavaPoet的依賴
implementation 'com.google.auto.service:auto-service:1.0-rc2'
implementation 'com.squareup:javapoet:1.7.0'
複製程式碼
  1. 新建BindViewProcessor處理器類繼承自AbstractProcessor,對原始碼的註解進行處理(我儘可能的理解有歧義的地方都新增了註釋)
/**
 * Created by will on 2018/2/4.
 */

@AutoService(Processor.class)
public class BindViewProcessor extends AbstractProcessor {
    /**
     * 工具類,可以從init方法的ProcessingEnvironment中獲取
     */
    private Elements elementUtils;
    /**
     * 快取所有子Element
     * key:父Element類名
     * value:子Element
     */
    private HashMap<String, List<Element>> cacheElements = null;
    /**
     * 快取所有父Element
     * key:父Element類名
     * value:父Element
     */
    private HashMap<String, Element> cacheAllParentElements = null;

    @Override
    public Set<String> getSupportedAnnotationTypes() {
        // 規定需要處理的註解型別
        return Collections.singleton(BindView.class.getCanonicalName());
    }

    @Override
    public boolean process(Set<? extends TypeElement> annotations
            , RoundEnvironment roundEnv) {
        //掃描所有註解了BindView的Field,因為我們所有註解BindView的地方都是一個Activity的成員
        Set<? extends Element> elements = roundEnv.getElementsAnnotatedWith(BindView.class);
        for (Element element : elements) {
            //將所有子elements進行過濾
            addElementToCache(element);
        }

        if (cacheElements == null || cacheElements.size() == 0) {
            return true;
        }
        for (String parentElementName : cacheElements.keySet()) {
            //判斷一下獲取到的parent element是否是類
            try {
                //使用JavaPoet構造一個方法
                MethodSpec.Builder bindViewMethodSpec = MethodSpec.methodBuilder("bindView")
                        .addModifiers(Modifier.PUBLIC, Modifier.STATIC)
                        .returns(void.class)
                        .addParameter(ClassName.get(cacheAllParentElements.get(parentElementName).asType())
                                , "targetActivity");

                List<Element> childElements = cacheElements.get(parentElementName);
                if (childElements != null && childElements.size() != 0) {
                    for (Element childElement : childElements) {
                        BindView bindView = childElement.getAnnotation(BindView.class);
                        //使用JavaPoet對方法內容進行新增
                        bindViewMethodSpec.addStatement(
                                String.format("targetActivity.%s = (%s) targetActivity.findViewById(%s)"
                                        , childElement.getSimpleName()
                                        , ClassName.get(childElement.asType()).toString()
                                        , bindView.id()));
                    }
                }

                //構造一個類,以Bind_開頭
                TypeSpec typeElement = TypeSpec.classBuilder("Bind_"
                        + cacheAllParentElements.get(parentElementName).getSimpleName())
                        .addModifiers(Modifier.PUBLIC, Modifier.FINAL)
                        .addMethod(bindViewMethodSpec.build())
                        .build();

                //進行檔案寫入
                JavaFile javaFile = JavaFile.builder(
                        getPackageName((TypeElement) cacheAllParentElements.get(parentElementName))
                        , typeElement).build();
                javaFile.writeTo(processingEnv.getFiler());

            } catch (IOException e) {
                e.printStackTrace();
                return true;
            }


        }


        return true;
    }

    private String getPackageName(TypeElement type) {
        return elementUtils.getPackageOf(type).getQualifiedName().toString();
    }

    @Override
    public synchronized void init(ProcessingEnvironment processingEnv) {
        super.init(processingEnv);
        elementUtils = processingEnv.getElementUtils();
    }

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

    /**
     * 快取父Element對應的所有子Element
     * 快取父Element
     *
     * @param childElement
     */
    private void addElementToCache(Element childElement) {
        if (cacheElements == null) {
            cacheElements = new HashMap<>();
        }

        if (cacheAllParentElements == null) {
            cacheAllParentElements = new HashMap<>();
        }
        //父Element類名
        String parentElementName = null;
        parentElementName = ClassName.get(childElement.getEnclosingElement().asType()).toString();

        if (cacheElements.containsKey(parentElementName)) {
            List<Element> childElements = cacheElements.get(parentElementName);
            childElements.add(childElement);
        } else {
            ArrayList<Element> childElements = new ArrayList<>();
            childElements.add(childElement);
            cacheElements.put(parentElementName, childElements);
            cacheAllParentElements.put(parentElementName, childElement.getEnclosingElement());
        }
    }
}
複製程式碼

3.新建Android專案,使用自定義的註解

  1. 新增對上述兩個專案的引用

注意:Android Gradle外掛2.2版本釋出後,Android 官方提供了annotationProcessor來代替android-apt,annotationProcessor同時支援 javac 和 jack 編譯方式,而android-apt只支援 javac 方式。同時android-apt作者宣佈不在維護,這裡我直接用了annotationProcessor

implementation project(':annotations')
annotationProcessor project(':annotations_compiler')
複製程式碼
  1. 在Activity的View中新增@BindView註解,並設定id
public class MainActivity extends AppCompatActivity {

    @BindView(id = R.id.tv_test)
    TextView tv_test;
    @BindView(id = R.id.tv_test1)
    TextView tv_test1;
    @BindView(id = R.id.iv_image)
    ImageView iv_image;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        Bind_MainActivity.bindView(this);
        tv_test.setText("test_1");
        tv_test1.setText("test_2");
        iv_image.setImageDrawable(getDrawable(R.mipmap.ic_launcher));
    }

}
複製程式碼
  1. 此時你的IDE可能會報Bind_MainActivity找不到,沒關係,重新Build一下就好了。Build一下後在app/build/generated/source/apt/debug/[你的包名]/annotation/路徑下就回生成apt輸出的檔案了。

其他的問題

  1. 如果你發現build後沒有apt檔案輸出,呵呵,因為你寫的processor有Bug~~~。這時候你需要debug你的processor。關於如何debug,請移步這篇部落格
  2. 關於android-apt切換為官方annotationProcessor的問題,請移步android-apt切換為官方annotationProcessor
  3. 待補充ing...

最後附上demo原始碼

參考文章

About Me

contact way value
mail weixinjie1993@gmail.com
wechat W2006292
github https://github.com/weixinjie
blog https://juejin.im/user/57673c83207703006bb92bf6

相關文章