Android Annotation掃盲筆記

寫在前面

今年你們都在搞組件化,組件化開發不可避免的須要用到路由(Router)來完成組件之間數據的交互,這就促進了各類路由發展如:阿里的ARouter以及ActivityRouter等優秀的Router框架。爲了方便你們的開發這些Router庫以及像ButterKnife這類的庫都用到了註解技術。本篇目的是進行一波掃盲。html

本文導讀

  • Android Annotation基礎
  • 解析Annotation
  • 實戰-本身作一個ButterKnife

1 Annotatin基礎

1.1 基本Annotation

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

  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
    }
}
複製代碼

3. @SuppressWarnings:抑制編譯器警告(用的比較少)。Java代碼編譯時IDE每每會給開發者不少警告信息,例如變量沒有使用等,這種警告多了以後很大程度上影響咱們debug效率。此註解就是來抑制這些警告。舉個栗子:

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

若是不使用@SuppressWarning來抑制編譯器警告,上面的代碼會被警告變量s從未使用。出了"unused",該註解支持的抑制類型還有下圖的內容(注該圖摘自IBM Knowledge Center)。android

1.2 JDK元Annotation

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

  1. @Retention註解。該註解只能修飾一個Annotation定義,用於指定所修飾的Annotation能夠保留多長"時間"(也但是說是保留的週期)。這裏說的「時間」有三種類型

  • RetentionPolicy.SOURCE:沒啥用,編譯器會直接忽略這種策略的註釋
  • RetentionPolicy.CLASS:自定義註解的默認值,編譯器會把這種策略的註釋保存在class文件中。像ButterKnife中的BindView註解就是用的這種方式。
  • RetentionPolicy.RUNTIME:編譯器會把該策略的註釋保存到class文件中,程序能夠經過反射等方式來獲取。

舉個例子,自定義一個BindView註解(看不懂不要緊,現有一個感性的認識,下一節開始作自定義Annotation講解)。github

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

當成員變量爲value時,能夠省略。也就是說上述代碼能夠換成 @Retention(RetentionPolicy.CLASS)segmentfault

  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文件。

  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劃分紅兩種:api

  • 沒有成員變量的註解稱爲"標記Annotation",這種註解使用自身是否存在爲咱們提供信息,例如Override等註解
  • 有成員變量的稱謂"元數據Annotation"。咱們可使用apt等工具對這種Annotation的成員進行二次加工。

注意:只定義了自定義註解沒有任何效果,還須要對Annotation的信息進行提取與加工!!!緩存

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

而後你發現Crash了。。。這就要引入下一節的內容了,使用apt對被註解的代碼進行二次加工。

2. 解析Annotation

完成自定義Annotation後,咱們還須要知道,針對這些註解,咱們要作哪些相關的處理,這就涉及到了Annotation的解析操做。 解析Annotation,一般分爲:對運行時Annotation的解析、對編譯時Annotation的解析; 解析Annotation,其實就是如何從代碼中找到Annotation,一般咱們的作法是:oracle

  • 用反射的方式獲取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. 這個項目用來定義全部自定義的註解,這部分用到了第一節的知識基礎。

  1. 在這個項目包裏面新建自定義的註解,咱們模仿ButterKnife,這裏增長一個BindView的註解

@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
相關文章
相關標籤/搜索