淺談Android下的註解

niknowzcd發表於2019-03-01

寫在開頭:最近在翻讀一些開源庫的時候,發現大多使用了註解,於是不得不來仔細瞭解一下Android下的註解知識

什麼是註解

java.lang.annotation,介面 Annotation,在JDK5.0及以後版本引入。

註解是程式碼裡的特殊標記,這些標記可以在編譯類載入執行時被讀取,並執行相應的處理。通過使用Annotation,開發人員可以在不改變原有邏輯的情況下,在原始檔中嵌入一些補充的資訊。程式碼分析工具、開發工具和部署工具可以通過這些補充資訊進行驗證、處理或者進行部署。

Annotation不能執行,它只有成員變數,沒有方法。Annotation跟public、final等修飾符的地位一樣,都是程式元素的一部分,Annotation不能作為一個程式元素使用。

註解的作用

註解將一些本來重複性的工作,變成程式自動完成,簡化和自動化該過程。比如用於生成Java doc,比如編譯時進行格式檢查,比如自動生成程式碼等,用於提升軟體的質量和提高軟體的生產效率。

常見的註解

Android已經定義好的註解大致分為4種,稱之為4大元註解

@Retention:定義該Annotation被保留的時間長度

  • RetentionPoicy.SOURCE:註解只保留在原始檔,當Java檔案編譯成class檔案的時候,註解被遺棄;用於做一些檢查性的操作,比如 @Override@SuppressWarnings
  • RetentionPoicy.CLASS:註解被保留到class檔案,但jvm載入class檔案時候被遺棄,這是預設的生命週期;用於在編譯時進行一些預處理操作,比如生成一些輔助程式碼(如 ButterKnife
  • RetentionPoicy.RUNTIME:註解不僅被儲存到class檔案中,jvm載入class檔案之後,仍然存在;用於在執行時去動態獲取註解資訊。這個註解大都會與反射一起使用

@Target:定義了Annotation所修飾的物件範圍

  • ElementType.CONSTRUCTOR:用於描述構造器
  • ElementType.FIELD:用於描述域
  • ElementType.LOCAL_VARIABLE:用於描述區域性變數
  • ElementType.METHOD:用於描述方法
  • ElementType.PACKAGE:用於描述包
  • ElementType.PARAMETER:用於描述引數
  • ElementType.TYPE:用於描述類、介面(包括註解型別) 或enum宣告

未標註則表示可修飾所有

@Inherited:是否允許子類繼承父類的註解,預設是false

@Documented 是否會儲存到 Javadoc 文件中

自定義註解

自定義註解中使用到較多的是執行時註解編譯時註解

執行時註解

下面通過一個簡單的動態繫結控制元件的例子來說明

首先定義一個簡單的自定義註解,

@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface BindView {
    int value() default  -1;
}
複製程式碼

然後在app執行時,通過反射將findViewbyId()得到的控制元件,注入到我們需要的變數中。

public class AnnotationActivity extends AppCompatActivity {

    @BindView(R.id.annotation_tv)
    private TextView mTv;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_annotation);

        getAllAnnotationView();

        mTv.setText("Annotation");
    }

    private void getAllAnnotationView() {
        //獲得成員變數
        Field[] fields = this.getClass().getDeclaredFields();

        for (Field field : fields) {
            try {
                //判斷註解
                if (field.getAnnotations() != null) {
                    //確定註解型別
                    if (field.isAnnotationPresent(BindView.class)) {
                        //允許修改反射屬性
                        field.setAccessible(true);
                        BindView bindView = field.getAnnotation(BindView.class);
                        //findViewById將註解的id,找到View注入成員變數中
                        field.set(this, findViewById(bindView.value()));
                    }
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
}
複製程式碼

最後mTv上顯示的就是我們想要的“Annotation”文字,這看起來是不是有點像ButterKnife,但是要注意反射是很消耗效能的,

所以我們常用的控制元件繫結庫ButterKnife並不是採用執行時註解,而是採用的編譯時註解.

編譯時註解

定義

在說編譯時註解之前,我們得先提一提註解處理器AbstractProcessor
它是javac的一個工具,用來在編譯時掃描和處理註解Annotation,你可以自定義註解,並註冊到相應的註解處理器,由註解處理器來處理你的註解。

一個註解的註解處理器,以Java程式碼(或者編譯過的位元組碼)作為輸入,生成檔案(通常是.java檔案)作為輸出。這些由註解器生成的.java程式碼和普通的.java一樣,可以被javac編譯。

匯入

因為AbstractProcessor是javac中的一個工具,所以在Android的工程下沒法直接呼叫。下面提供一個本人嘗試可行的匯入方式。

File–>New Module–>java library 新建一個java module,注意一定要是java library,不是Android library

接下來就可以在對應的library中使用AbstractProcessor

準備工作完成之後,下面通過一個簡單的註解繫結控制元件的例子來講述

工程目錄

--app                 (主工程)
--app_annotation      (java module 自定義註解)
--annotation-api      (Android module)
--app_compiler        (java module 註解處理器邏輯)
複製程式碼

在annotation module下建立註解

@Retention(RetentionPolicy.CLASS)
public @interface BindView {
	//繫結控制元件
	int value();
}
複製程式碼

在compiler module下建立註解處理器 CustomProcessor

public class CustomProcessor extends AbstractProcessor {
    //檔案相關的輔助類
    private Filer mFiler;
    //元素相關的輔助類
    private Elements mElements;
	
	//初始化引數
    @Override
    public synchronized void init(ProcessingEnvironment processingEnvironment) {
        super.init(processingEnvironment);
        mElements = processingEnvironment.getElementUtils();
        mFiler = processingEnvironment.getFiler();
    }
	
	//核心處理邏輯,相當於java中的主函式main(),你需要在這裡編寫你自己定義的註解的處理邏輯
	//返回值 true時表示當前處理,不允許後續的註解器處理
    @Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment env) {
        return true;
    }

    //自定義註解集合
    @Override
    public Set<String> getSupportedAnnotationTypes() {
        Set<String> types = new LinkedHashSet<>();
        types.add(BindView.class.getCanonicalName());
        return types;
    }

    @Override
    public SourceVersion getSupportedSourceVersion() {
        return SourceVersion.latestSupported();
    }
}
複製程式碼

其中核心程式碼process函式有兩個引數,我們重點關注第二個引數,因為env表示的是所有註解的集合

首先我們先簡單的說明一下porcess的處理流程

  1. 遍歷env,得到我們需要的元素列表
  2. 將元素列表封裝成物件,方便之後的處理(如同平時解析json資料一樣)
  3. 通過JavaPoet庫將物件以我們期望的形式生成java檔案
  1. 遍歷env,得到我們需要的元素列表
for(Element element : roundEnvironment.getElementsAnnotatedWith(BindView.class)){
	// todo ....

    // 判斷元素的型別為Class
    if (element.getKind() == ElementKind.CLASS) {
        // 顯示轉換元素型別
        TypeElement typeElement = (TypeElement) element;
        // 輸出元素名稱
        System.out.println(typeElement.getSimpleName());
        // 輸出註解屬性值
        System.out.println(typeElement.getAnnotation(BindView.class).value());
    }
}
複製程式碼

直接通過getElementsAnnotatedWith函式就能獲取到需要的註解的列表,函式體內加了些element簡單的使用

2.將元素列表封裝成物件,方便之後的處理

首先,我們需要明確,在繫結控制元件的這個事件下,我們需要的是控制元件的id。

新建類 BindViewField.class 用來儲存自定義註解BindView相關的屬性

BindViewField.class

public class BindViewField {

    private VariableElement mFieldElement;

    private int mResId;

    public BindViewField(Element element) throws IllegalArgumentException {
        if (element.getKind() != ElementKind.FIELD) {
            throw new IllegalArgumentException(String.format("Only field can be annotated with @%s",
                    BindView.class.getSimpleName()));
        }
        mFieldElement = (VariableElement) element;
        BindView bindView = mFieldElement.getAnnotation(BindView.class);
        mResId = bindView.value();
        if (mResId < 0) {
            throw new IllegalArgumentException(String.format("value() in %s for field % is not valid",
                    BindView.class.getSimpleName(), mFieldElement.getSimpleName()));
        }
    }

    public Name getFieldName() {
        return mFieldElement.getSimpleName();
    }

    public int getResId() {
        return mResId;
    }

    public TypeMirror getFieldType() {
        return mFieldElement.asType();
    }
}
複製程式碼

上述的BindViewField只能表示一個自定義註解bindView物件,而一個類中很可能會有多個自定義註解,所以還需要建立一個物件Annotation.class來管理自定義註解集合、

AnnotatedClass.class

public class AnnotatedClass {

    //類
    public TypeElement mClassElement;

    //類內的註解變數
    public List<BindViewField> mFiled;

    //元素幫助類
    public Elements mElementUtils;

    public AnnotatedClass(TypeElement classElement, Elements elementUtils) {
        this.mClassElement = classElement;
        this.mElementUtils = elementUtils;
        this.mFiled = new ArrayList<>();
    }
	
	//新增註解變數
    public void addField(BindViewField field) {
        mFiled.add(field);
    }
	
	//獲取包名
    public String getPackageName(TypeElement type) {
        return mElementUtils.getPackageOf(type).getQualifiedName().toString();
    }
	
	//獲取類名
    private static String getClassName(TypeElement type, String packageName) {
        int packageLen = packageName.length() + 1;
        return type.getQualifiedName().toString().substring(packageLen).replace(`.`, `$`);
    }
}
複製程式碼

給上完整的解析流程

//解析過後的目標註解集合
private Map<String, AnnotatedClass> mAnnotatedClassMap = new HashMap<>();

@Override
public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
    mAnnotatedClassMap.clear();
    try {
        processBindView(roundEnvironment);
    } catch (Exception e) {
        e.printStackTrace();
        return true;
    }
    return true;
}

private void processBindView(RoundEnvironment env) {
    for (Element element : env.getElementsAnnotatedWith(BindView.class)) {
        AnnotatedClass annotatedClass = getAnnotatedClass(element);
        BindViewField field = new BindViewField(element);
        annotatedClass.addField(field);
        System.out.print("p_element=" + element.getSimpleName() + ",p_set=" + element.getModifiers());
    }
}

private AnnotatedClass getAnnotatedClass(Element element) {
    TypeElement encloseElement = (TypeElement) element.getEnclosingElement();
    String fullClassName = encloseElement.getQualifiedName().toString();
    AnnotatedClass annotatedClass = mAnnotatedClassMap.get(fullClassName);
    if (annotatedClass == null) {
        annotatedClass = new AnnotatedClass(encloseElement, mElements);
        mAnnotatedClassMap.put(fullClassName, annotatedClass);
    }
    return annotatedClass;
}
複製程式碼

3.通過JavaPoet庫將物件以我們期望的形式生成java檔案

通過上述兩步成功獲取了自定義註解的元素物件,但是還是缺少一步關鍵的步驟,缺少一步findViewById(),實際上ButterKnife這個很出名的庫也並沒有省略findViewById()這一個步驟,只是在編譯的時候,在build/generated/source/apt/debug下生成了一個檔案,幫忙執行了findViewById()這一行為而已。

同樣的,我們這裡也需要生成一個java檔案,採用的是JavaPoet這個庫。具體的使用 參考連結

process函式中增加生成java檔案的邏輯

@Override
public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
    mAnnotatedClassMap.clear();
    try {
        processBindView(roundEnvironment);
    } catch (Exception e) {
        e.printStackTrace();
        return true;
    }

    try {
        for (AnnotatedClass annotatedClass : mAnnotatedClassMap.values()) {
            annotatedClass.generateFinder().writeTo(mFiler);
        }
    } catch (Exception e) {
        e.printStackTrace();
    }
    return true;
}
複製程式碼

其中核心邏輯annotatedClass.generateFinder().writeTo(mFiler);
具體實現在AnnotatedClass

public JavaFile generateFinder() {

    //構建 inject 方法
    MethodSpec.Builder methodBuilder = MethodSpec.methodBuilder("inject")
            .addModifiers(Modifier.PUBLIC)
            .addAnnotation(Override.class)
            .addParameter(TypeName.get(mClassElement.asType()), "host", Modifier.FINAL)
            .addParameter(TypeName.OBJECT, "source")
            .addParameter(Utils.FINDER, "finder");

    //inject函式內的核心邏輯,
    // host.btn1=(Button)finder.findView(source,2131427450);  ----生成程式碼
    // host.$N=($T)finder.findView(source,$L)                 ----原始程式碼
    // 對比就會發現這裡執行了實際的findViewById繫結事件
    for (BindViewField field : mFiled) {
        methodBuilder.addStatement("host.$N=($T)finder.findView(source,$L)", field.getFieldName()
                , ClassName.get(field.getFieldType()), field.getResId());
    }

    String packageName = getPackageName(mClassElement);
    String className = getClassName(mClassElement, packageName);
    ClassName bindClassName = ClassName.get(packageName, className);

    //構建類物件
    TypeSpec finderClass = TypeSpec.classBuilder(bindClassName.simpleName() + "$$Injector")
            .addModifiers(Modifier.PUBLIC)
            .addSuperinterface(ParameterizedTypeName.get(Utils.INJECTOR, TypeName.get(mClassElement.asType())))   //繼承介面
            .addMethod(methodBuilder.build())
            .build();

    return JavaFile.builder(packageName, finderClass).build();
}
複製程式碼

到這裡,大部分邏輯都已實現,用來繫結控制元件的輔助類也已通關JavaPoet生成了,只差最後一步,宿主註冊,如同ButterKnife一般,ButterKnife.bind(this)

編寫呼叫介面

在annotation-api下新建

注入介面Injector

public interface Injector<T> {

    void inject(T host, Object source, Finder finder);
}
複製程式碼

宿主通用介面Finder(方便之後擴充套件到view和fragment)

public interface Finder {

    Context getContext(Object source);

    View findView(Object source, int id);
}
複製程式碼

activity實現類 ActivityFinder

public class ActivityFinder implements Finder{

    @Override
    public Context getContext(Object source) {
        return (Activity) source;
    }

    @Override
    public View findView(Object source, int id) {
        return ((Activity) (source)).findViewById(id);
    }
}
複製程式碼

核心實現類 ButterKnife

public class ButterKnife {

    private static final ActivityFinder finder = new ActivityFinder();
    private static Map<String, Injector> FINDER_MAP = new HashMap<>();

    public static void bind(Activity activity) {
        bind(activity, activity);
    }

    private static void bind(Object host, Object source) {
        bind(host, source, finder);
    }

    private static void bind(Object host, Object source, Finder finder) {
        String className = host.getClass().getName();
        try {
            Injector injector = FINDER_MAP.get(className);
            if (injector == null) {
                Class<?> finderClass = Class.forName(className + "$$Injector");
                injector = (Injector) finderClass.newInstance();
                FINDER_MAP.put(className, injector);
            }
            injector.inject(host, source, finder);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
複製程式碼

主工程下呼叫

對應的按鈕可以直接使用,不需要findViewById()

public class MainActivity extends AppCompatActivity {

	@BindView(R.id.annotation_tv)
	public TextView tv1;
	
	@Override
	protected void onCreate(Bundle savedInstanceState) {
	    super.onCreate(savedInstanceState);
	    setContentView(R.layout.activity_main);
	    ButterKnife.bind(this);
	    tv1.setText("annotation_demo");
	}
}
複製程式碼

JavaPoet的簡單介紹

常用的幾個類

  • MethodSpec 代表一個建構函式或方法宣告。
  • TypeSpec 代表一個類,介面,或者列舉宣告。
  • FieldSpec 代表一個成員變數,一個欄位宣告。
  • JavaFile包含一個頂級類的Java檔案。

常用的佔位符

$L for variable (變數)

$S for Strings

$T for Types

$N for Names(我們自己生成的方法名或者變數名等等)

補充內容

自定義Processor註解處理器中最主要的處理方法是process()函式,而process()函式中重要的是 RoundEnvironment引數,

通常的使用方式

for (Element element : env.getElementsAnnotatedWith(BindView.class)) {
	//todo 
}
複製程式碼

通過BindView註解獲取所有的Element物件,而這個Element是什麼呢?

Element表示一個程式元素,可以是包,類或者是方法,所有通過註解取到的元素都將以Element型別處理.準確的來說是Element物件的子類處理。

Element的子類

  • ExecutableElement 表示某個類或介面的方法、構造方法或初始化程式(靜態或例項),包括註釋型別元素。
    對應註解時的@Target(ElementType.METHOD)
    @Target(ElementType.CONSTRUCTOR)
  • PackageElement 表示一個包程式元素,提供對有關包及其成員的資訊訪問。對應註解時@Target(ElementType.PACKAGE)
  • TypeElement 表示一個類或介面程式元素,提供對有關型別及其成員的資訊訪問。對應@Target(ElementType.TYPE)
    注意:列舉型別是一種類,而註解型別是一種介面。
  • TypeParameterElement 表示一般類、介面、方法或構造方法元素的型別引數。
    對應@Target(ElementType.PARAMETER)
  • VariableElement 表示一個欄位、enum常量、方法或構造方法引數、區域性變數或異常引數。
    對應@Target(ElementType.LOCAL_VARIABLE)

不同型別的Element的資訊獲取方式不同

修飾方法的註解和ExecutableElement

當你有一個註解是以@Target(ElementType.METHOD)定義時,表示該註解只能修飾方法。

獲取我們需要的一些基本資訊

//BindClick.class 以 @Target(ElementType.METHOD)修飾
for (Element element : roundEnv.getElementsAnnotatedWith(BindClick.class)) {
    //對於Element直接強轉
    ExecutableElement executableElement = (ExecutableElement) element;

    //非對應的Element,通過getEnclosingElement轉換獲取
    TypeElement classElement = (TypeElement) element.getEnclosingElement();

    //當(ExecutableElement) element成立時,使用(PackageElement) element.getEnclosingElement();將報錯。
    //需要使用elementUtils來獲取
    Elements elementUtils = processingEnv.getElementUtils();
    PackageElement packageElement = elementUtils.getPackageOf(classElement);

    //全類名
    String fullClassName = classElement.getQualifiedName().toString();
    //類名
    String className = classElement.getSimpleName().toString();
    //包名
    String packageName = packageElement.getQualifiedName().toString();
    //方法名
    String methodName = executableElement.getSimpleName().toString();

    //取得方法引數列表
    List<? extends VariableElement> methodParameters = executableElement.getParameters();
    //引數型別列表
    List<String> types = new ArrayList<>();
    for (VariableElement variableElement : methodParameters) {
        TypeMirror methodParameterType = variableElement.asType();
        if (methodParameterType instanceof TypeVariable) {
            TypeVariable typeVariable = (TypeVariable) methodParameterType;
            methodParameterType = typeVariable.getUpperBound();

        }
        //引數名
        String parameterName = variableElement.getSimpleName().toString();
        //引數型別
        String parameteKind = methodParameterType.toString();
        types.add(methodParameterType.toString());
    }
}
複製程式碼

修飾屬性、類成員的註解和VariableElement

for (Element element : roundEnv.getElementsAnnotatedWith(IdProperty.class)) {
    //ElementType.FIELD註解可以直接強轉VariableElement
    VariableElement variableElement = (VariableElement) element;

    TypeElement classElement = (TypeElement) element
            .getEnclosingElement();
    PackageElement packageElement = elementUtils.getPackageOf(classElement);
    //類名
    String className = classElement.getSimpleName().toString();
    //包名
    String packageName = packageElement.getQualifiedName().toString();
    //類成員名
    String variableName = variableElement.getSimpleName().toString();

    //類成員型別
    TypeMirror typeMirror = variableElement.asType();
    String type = typeMirror.toString();

}
複製程式碼

修飾類的註解和TypeElement

for (Element element : roundEnv.getElementsAnnotatedWith(BindView.class)) {
    //ElementType.TYPE註解可以直接強轉TypeElement
    TypeElement classElement = (TypeElement) element;

    PackageElement packageElement = (PackageElement) element
                .getEnclosingElement();

    //全類名
    String fullClassName = classElement.getQualifiedName().toString();
    //類名
    String className = classElement.getSimpleName().toString();
    //包名
    String packageName = packageElement.getQualifiedName().toString();
     //父類名
     String superClassName = classElement.getSuperclass().toString();

}
複製程式碼

原始碼地址

demo地址

參考

探究Android中的註解
註解快速入門

自定義註解之編譯時註解

一小時搞明白註解處理器

javapoet——讓你從重複無聊的程式碼中解放出來

JavaPoet原始碼無法正常匯入Modifier類的討論

Android編譯時註解框架5-語法講解

相關文章