Android注解&APT技術


image.png

序言

注解是Java程序和Android程序中常見的語法,之前雖然知道有這么個東西,但并沒有深入了解注解。寫EventBus源碼解析ButterKnife源碼解析的時候,發現注解在其中起到很大作用,就決定專門寫一篇文章介紹注解。

下面將會從這幾個方面展開介紹:

  1. 注解的概念和語法
  2. 運行時注解
  3. 編譯時注解(APT技術)
  4. 對比運行時和編譯時注解
  5. 總結

注解的概念和語法

1. 注解的概念

定義:注解用于為Java提供元數據,作為元數據,注解不影響代碼執行,但某些類型注解也可以用于這一目的,注解從Java5開始引入

2. 注解的語法

注解通過@interface關鍵字來定義。

@Retention(RetentionPolicy.CLASS)
@Target({ElementType.FIELD, ElementType.TYPE})
public @interface MyAnnotation {}

3. 元注解

在上面的定義中,RetentionTarget是什么東西?它們為什么能夠修飾注解。
實際上,它們是元注解:元注解是可以注解到注解上的注解,簡單來說就是一種基本注解,可以作用到其他注解上
Java中總共有5中元注解:@Retention,@Documented,@Target,@Inherited,@Repeatable。下面分別介紹它們:

@Retention

用來說明注解的存活時間,有三種取值:

  • RetentionPolicy.SOURCE:注解只在源碼階段保留,編譯器開始編譯時它將被丟棄忽視
  • RetentionPolicy.CLASS:注解會保留到編譯期,但運行時不會把它加載到JVM中
  • RetentionPolicy.RUNTIME:注解可以保留到程序運行時,它會被加載到JVM中,所以程序運行過程中可以獲取到它們

編譯期注解和運行時注解使用得比較多,下面會有兩個主題專門介紹。

@Target

指定注解可作用的目標,取值如下:

  • ElementType.PACKAGE:可作用在包上
  • ElementType.TYPE:可作用在類、接口、枚舉上
  • ElementType.ANNOTATION_TYPE:可以作用在注解上
  • ElementType.FIELD:可作用在屬性上
  • ElementType.CONSTRUCTOR:可作用在構造方法上
  • ElementType.METHOD:可作用在方法上
  • ElementType.PARAMETER:可作用在方法參數上
  • ElementType.LOCAL_VARIABLE:可作用在局部變量上,例如方法中定義的變量

它接收一個數組作為參數,即可以指定多個作用對象,就像上面的Demo:

@Target({ElementType.FIELD, ElementType.TYPE})
@Documented

從名字可知,這個注解跟文檔相關,它的作用是能夠將注解中的元素包含到Javadoc中去。

@Inherited

Inherited是繼承的意思,但并不是注解本身可被繼承,而是指一個父類SuperClass被該類注解修飾,那么它的子類SubClass如果沒有任何注解修飾,就會繼承父類的這個注解。
舉個栗子:

@Inherited
@Target(ElementType.Type)
@Retention(RetentionPolicy.RUNTIME)
public @interface Test {}

@Test
public class A {}

public class B extens A {}

解釋:注解Test被@Inherited修飾,A被Test修飾,B繼承A(B上又無其他注解),那么B就會擁有Test這個注解。

@Repeatable

這個詞是可重復的意思,它是java1.8引入的,算一個新特性
什么樣的注解可以多次應用來呢,通常是注解可以取多個值,舉個栗子:

public @Interface Persons {
    Person[] value();
}

@Repeatable(Persons.class)
public @Interface Person {
    String role() default ""
}

@Person("artist")
@Person("developer")
@Person("superman")
public class Me {}

解釋:@Person被@Repeatable修飾,所以Person可以多次作用在同一個對象Me上,而Repeatable接收一個參數,這個參數是個容器注解,用來存放多個@Person。

4. 注解的屬性

注解中可以定義屬性,也可以叫成員變量,不能定義方法。
就如上面的例子:

  • @Person中定義了一個屬性role,在使用的過程中就可以傳一個字符串
  • 又給role設置了默認值為空字符串,以就算不傳可以直接使用
  • 如果有多個屬性,就必須以key=value的形式指定屬性值

注解中的屬性支持8種基本類型外加字符串、類、接口、注解及以上類型的數組

5. Java預置注解

Java中提供了很多注解,如:

  • @Override:表示覆寫父類中的方法
  • @Depracated:標記過時的類、方法、成員變量
  • @FunctionalInterface:Java1.8引入的新特性,表示函數式接口(只有一個方法的普通接口),主要用于lambda表達式。
  • ......

運行時注解

上面介紹過,用Retention(RetentionPolicy.RUNTIME)修飾的就是運行時注解。使用這種注解,多數情況是為了在運行時做一些事情。至于具體做什么事?就看各位同學自己的意愿了。

這里,我通過一個例子來介紹怎么使用運行時注解。
現在,我打算通過運行時注解實現一個功能,跟ButterKnife類似,即自動注入功能,不需要我手動調用findViewById

下面是實現的步驟:

1. 定義注解

/**
 * author : user_zf
 * date : 2018/11/6
 * desc : 運行時通過反射自動注入View,不再需要寫findViewById
 */
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface InjectView {
    @IdRes int id() default -1;
}

這個注解中有一個屬性id,表示待注入控件的id

2. 定義注解解析工具

/**
 * author : user_zf
 * date : 2018/11/6
 * desc : 用來解析注解InjectView
 */
public class AnnotationUtil {

    /**
     * 解析注解InjectView
     *
     * @param activity 使用InjectView的目標對象
     */
    public static void inject(Activity activity) {
        Field[] fields = activity.getClass().getDeclaredFields();
        //通過該方法設置所有的字段都可訪問,否則即使是反射,也不能訪問private修飾的字段
        AccessibleObject.setAccessible(fields, true);
        for (Field field : fields) {
            boolean needInject = field.isAnnotationPresent(InjectView.class);
            if (needInject) {
                InjectView anno = field.getAnnotation(InjectView.class);
                int id = anno.id();
                if (id == -1) continue;
                View view = activity.findViewById(id);
                Class fieldType = field.getType();
                try {
                    //把View轉換成field聲明的類型
                    field.set(activity, fieldType.cast(view));
                } catch (Exception e) {
                    Log.e(InjectView.class.getSimpleName(), e.getMessage());
                }
            }
        }
    }
}

主要是通過反射,找到Activity中使用了@InjectView的字段,然后通過findViewById來初始化控件。

3. 使用注解

class MainActivity : AppCompatActivity() {

    @InjectView(id = R.id.tvHello)
    private var tvHello: TextView? = null
    @InjectView(id = R.id.btnHello)
    private var btnHello: Button? = null
    @InjectView(id = R.id.rlRoot)
    private var rlRoot: RelativeLayout? = null


    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        //通過注解初始化控件
        AnnotationUtil.inject(this@MainActivity)

        //設置控件
        tvHello?.text = "Hello World!"
        btnHello?.text = "Hello Button"
        btnHello?.setOnClickListener {
            Toast.makeText(this@MainActivity, "點擊按鈕了", Toast.LENGTH_SHORT).show()
        }
        rlRoot?.setBackgroundColor(resources.getColor(R.color.colorAccent, null))
    }
}

在控件上使用注解,就不需要我們手動初始化,注解解析工具會自動幫我們初始化。大大減少重復代碼。

原理:運行時注解主要通過反射進行解析,代碼運行過程中,通過反射我們可以知道哪些屬性、方法使用了該注解,并且可以獲取注解中的參數,做一些我們想做的事情

編譯時注解(APT技術)

使用Retention(RetentionPolicy.CLASS)修飾的注解就是編譯時注解。
說到編譯時注解,就需要引出我們今天的主角:APT(編譯時解析技術)。
APT技術主要是通過編譯期解析注解,并且生成java代碼的一種技術,一般會結合Javapoet技術來生成代碼。
下面,我們還是通過一個栗子來介紹APT技術。
在寫Bean的時候經常需要寫Getter和Setter方法,我們想通過一個注解,在編譯的過程中自動幫我們生成Getter和Setter方法,這里會生成一個新的類,而不是修改原來的類

1. 編寫注解

/**
 * author : user_zf
 * date : 2018/11/7
 * desc : 編譯期給bean生成getter和setter方法的注解(限java類使用)
 */
@Retention(RetentionPolicy.CLASS)
@Target(ElementType.FIELD)
public @interface GenerateGS {}

2. 編寫注解解析器Processor

這里的注解解析器和運行時注解解析器不一樣,這里需要繼承AbstractProcessor類。
在Android Module和Android Library Module中是不能使用AbstractProcessor類的,需要新建一個Java Library Module,把注解解析器放在這個Java Module中,然后用Android Module依賴這個Java Module
接下來,看一下我們的GenerateGSProcessor的實現:

/**
 * author : user_zf
 * date : 2018/11/7
 * desc : generateGS編譯時注解解析器
 */
//@AutoService(Processor.class)
//@SupportedAnnotationTypes("study.com.aptlib.GenerateGS")
//@SupportedSourceVersion(SourceVersion.RELEASE_7)
public class GenerateGSProcessor extends AbstractProcessor {

    private Filer mFiler;

    /**
     * 初始化Processor和一些工具類
     */
    @Override
    public synchronized void init(ProcessingEnvironment processingEnv) {
        super.init(processingEnv);
        mFiler = processingEnv.getFiler();
    }

    /**
     * 返回該Processor能夠處理的注解
     */
    @Override
    public Set<String> getSupportedAnnotationTypes() {
        LinkedHashSet<String> types = new LinkedHashSet<>();
        types.add(GenerateGS.class.getCanonicalName());
        return types;
    }

    /**
     * 返回Java的版本號
     */
    @Override
    public SourceVersion getSupportedSourceVersion() {
        return SourceVersion.latestSupported();
    }

    /**
     * 真正處理注解的方法
     */
    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        HashMap<String, HashSet<Element>> nameMap = new HashMap<>();
        Set<? extends Element> annotatedElements = roundEnv.getElementsAnnotatedWith(GenerateGS.class);
        //遍歷處理帶注解的Element,把他們分類保存在Map中,key=包裹類 value=類中所有使用注解的Element
        for (Element element : annotatedElements) {
            Element parent = element.getEnclosingElement();
            String parentName = parent.getSimpleName().toString();
            HashSet<Element> set = nameMap.get(parentName);
            if (set == null) {
                set = new HashSet<>();
            }
            set.add(element);
            nameMap.put(parentName, set);
        }

        generateJavaFile(nameMap);
        return true;
    }

    /**
     * 根據Map生成Java文件
     */
    private void generateJavaFile(Map<String, HashSet<Element>> map) {
        System.out.println("開始生成代碼");
        Set<Map.Entry<String, HashSet<Element>>> nameSet = map.entrySet();
        for(Map.Entry<String, HashSet<Element>> entry : nameSet) {
            String className = entry.getKey();
            Set<Element> fields = entry.getValue();
            TypeSpec.Builder typeSpecBuilder = TypeSpec.classBuilder(className + "$Bean")
                    .addModifiers(Modifier.PUBLIC);
            //遍歷添加屬性和對應的getter/setter方法
            for (Element element : fields) {
                //只處理field
                if (element.getKind().isField()) {
                    //獲取字段名稱
                    String fieldName = element.getSimpleName().toString();
                    //字段名稱首字母變成大寫
                    char[] cs = fieldName.toCharArray();
                    cs[0] -= 32;
                    String firstUpperName = String.valueOf(cs);
                    //獲取字段類型
                    TypeName type = TypeName.get(element.asType());
                    //生成字段
                    FieldSpec fieldSpec = FieldSpec.builder(type, fieldName, Modifier.PRIVATE).build();
                    //生成getter/setter方法
                    MethodSpec getterMethod = MethodSpec.methodBuilder("get" + firstUpperName)
                            .addModifiers(Modifier.PUBLIC)
                            .returns(type)
                            .addStatement("return " + fieldName)
                            .build();
                    MethodSpec setterMethod = MethodSpec.methodBuilder("set" + firstUpperName)
                            .addModifiers(Modifier.PUBLIC)
                            .returns(TypeName.VOID)
                            .addParameter(type, fieldName)
                            .addStatement("this." + fieldName + " = " + fieldName)
                            .build();
                    //給$Bean添加字段及對應的getter和setter方法
                    typeSpecBuilder.addField(fieldSpec)
                            .addMethod(getterMethod)
                            .addMethod(setterMethod);
                }
            }
            TypeSpec typeSpec = typeSpecBuilder.build();
            JavaFile javaFile = JavaFile.builder("study.com.aptlib", typeSpec).build();
            try {
                javaFile.writeTo(mFiler);
                System.out.println("生成" + className + "$Bean" + "類");
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        System.out.println("代碼生成完畢");
    }
}

GenerateGSProcessor中主要有四個方法:init方法,getSupportedAnnotationTypes方法,getSupportedSourceVersion方法和process方法。代碼注釋中給出了這四種方法的作用,其中最主要的方法就是process方法, 這個方法就是用來解析注解的。而getSupportedAnnotationTypes和getSupportedSourceVersion兩個方法可以用注解來替代,分別是@SupportedAnnotationTypes@SupportedAnnotationTypes,在GenerateGsProcessor的注釋中可以看到他們。
有人會問,除了那兩個注解之外,還有一個@AutoService注解,這個是干嘛的呢?
別著急,下面我們會介紹的。

3. 添加SPI配置文件

我們先來簡單介紹一下SPI(服務提供接口Service Provider Interface)機制,主要做作用是為接口尋找服務實現
舉個栗子,我們現在有三個模塊:common、A、B,并且A和B都依賴與common。現在,common模塊中有個接口Fly(有一個fly方法)而A中定義Fly的實現類Bird,B中定義Fly的實現類Butterfly。
在A和B中都添加配置文件,A的配置文件中寫上Bird的帶包全名,B的配置文件中寫上Butterfly帶包全名,接著在需要使用的地方,A和B都可以使用下面一段代碼:

ServiceLoader<Fly> serviceLoader = ServiceLoader.load(Fly.class, Fly.class.getClassLoader());
Iterator<Fly> it = serviceLoader.iterator();
if (it.hasNext()) {
    it.next().fly();
}

這樣,在A中的效果就是Bird在飛,B中的效果是Butterfly在飛。有點類似于策略模式,可以通過配置文件動態加載。

接下來,總結一下配置方法:

1、定義接口和接口實現類
2、創建resources/META-INF/services目錄
3、在該目錄下創建一個文件,文件名為接口名(帶包全名),內容為接口實現類的帶包全名
4、在代碼中通過ServiceLoader動態加載并且調用實現類的內部方法。

好,現在讓我們回到APT技術來,APT技術中的Processor
就使用了SPI機制,接口是Process,實現類是GenerateGSProcessor,所以我們需要做下面幾件事:

  • 在main目錄下創建resources/META-INF/services目錄


    image.png
  • 在該目錄下新建javax.annotation.processing.Processor文件


    image.png
  • 在文件中添加內容study.com.aptlib.GenerateGSProcessor
study.com.aptlib.GenerateGSProcessor

到這里SPI配置完畢。

可能大家會覺得這種配置方式比較麻煩,對,確實比較麻煩。我們可以使用Google提供的auto-service庫來簡化這些操作

compile 'com.google.auto.service:auto-service:1.0-rc4'
compile 'com.google.auto:auto-common:0.10'

然后在GenerateGSProcessor類上添加注解:

@AutoService(Processor.class)

這就是上面提到的AutoService,用這個注解可以替代SPI的配置文件

4. 在Android Module使用注解

首先,添加項目依賴

annotationProcessor project(':aptlib')
api project(':aptlib')

這里為什么要添加兩次呢?

  • annotationProcessor:指定專門的注解解析庫
  • api:表示添加注解依賴,因為我們的GenerateGs寫在aptlib庫,所以需要單獨添加這個依賴,如果注解和解析器放在不同的module,就不需要這么寫

接下來,在代碼中使用注解:

public class Person {
    @GenerateGS
    private String name;
    @GenerateGS
    private int gender;
    @GenerateGS
    private String hobby;
}

通過rebuild來編譯我們的項目,就會生成Person$Bean類:

package study.com.aptlib;

import java.lang.String;

public class Person$Bean {
    private String hobby;
    
    private String name;
    
    private int gender;
    
    public String getHobby() {
      return hobby;
    }
    
    public void setHobby(String hobby) {
      this.hobby = hobby;
    }
    
    public String getName() {
      return name;
    }
    
    public void setName(String name) {
      this.name = name;
    }
    
    public int getGender() {
      return gender;
    }
    
    public void setGender(int gender) {
      this.gender = gender;
    }
}

有沒有很神奇。

對比運行時和編譯時注解

在很多情況下,運行時注解和編譯時注解可以實現相同的功能,比如依賴注入框架,我們既可以在運行時通過反射來初始化控件,也可以再編譯時就生成控件初始化代碼。那么,這兩者有什么區別呢?
答:編譯時注解性能比運行時注解好,運行時注解需要使用到反射技術,對程序的性能有一定影響,而編譯時注解直接生成了源代碼,運行過程中直接執行代碼,沒有反射這個過程。

很多框架的實現都是用到了編譯時注解,如ButterKnife、EventBus、Dagger2等等。

項目中使用這些庫的時候,會有一個比較讓人疑惑的地方。就拿ButterKnife舉例。

我們使用ButterKnife時,會添加依賴:

annotationProcessor 'com.jakewharton:butterknife-compiler:8.6.0'

但我們在項目結構中怎么也找不到ButterKnifeProcessor和編譯庫的代碼。這個是為什么呢?
經過一番研究,總算知道原因了,一些插件庫、注解解析庫并不會放在項目結構中,而是會放在gradle的緩存目錄中:
/Users/user_zf/.gradle/caches/modules-2/files-2.1/com.jakewharton/butterknife-compiler/8.6.0/d3defb48a63aa0591117d0cec09f47a13fffda19,在這個路徑中,總算找到了butterknife-compiler-8.6.0.jar

總結

經過上面的介紹,相信大家對注解有了比較全面的認識。各位同學可以嘗試在項目開發過程中去使用注解,它可以大大提升我們的開發效率,減少不必要的重復代碼。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 172,813評論 25 708
  • 用兩張圖告訴你,為什么你的 App 會卡頓? - Android - 掘金 Cover 有什么料? 從這篇文章中你...
    hw1212閱讀 12,824評論 2 59
  • 夯實 Java 基礎 - 注解 不知道大家有沒有一種感覺,當你想要了解某個知識點的時候,就會發現好多技術類 APP...
    醒著的碼者閱讀 1,081評論 4 7
  • 我在江南那年, 種下, 一顆花籽。 等待北方的你, 去欣賞花開。
    折玫人閱讀 250評論 0 3
  • 很多系統方法都有版本支持的說明,所以對于支持多個系統版本,需要判斷系統版本來執行方法。比如: 第一個方法只支持7-...
    羅淞閱讀 1,568評論 0 0