Android 中優雅地使用註解

概述

註解(Annotation),是源碼中特殊的語法元數據,類、方法、變量、參數均可以被註解。利用註解能夠標記源碼以便編譯器爲源碼生成文檔和檢查代碼,也可讓編譯器和註解處理器在編譯時根據註解自動生成代碼,甚至能夠保留到運行時以便改變運行時的行爲。 Java 內置了一些註解,如 @Override 註解用來代表該方法是重寫父類方法,編譯器會負責檢查該方法與父類方法的聲明是否一致。@Deprecated 註解用來代表該元素已經被廢棄不建議使用了。@SuppressWarnings 註解用來表示編譯器能夠忽略特定警告。
註解類型的聲明和接口的聲明相似,不過須要使用 @interface 和元註解(用來定義註解的註解)描述,每一個方法聲明定義了註解類型的一個元素,且方法聲明不能包含任何參數或 throws,方法的返回類型必須是原語類型、StringClass、枚舉、註解和這些類型的數組,方法能夠有默認值,如:html

public @interface RequestForEnhancement {
    int id();
    String synopsis();
    String engineer() default "[unassigned]"; 
    String date();    default "[unimplemented]"; 
}
複製代碼

定義完註解類型後,就能夠用它去註解一些聲明瞭。註解是一種特殊的修飾符,能夠像 publicstaticfinal 修飾符同樣使用,不過一般註解要寫在這些修飾符以前。使用時爲 @ 符號加註解類型加元素值對列表並用括號括起來,如:java

@RequestForEnhancement(
    id       = 2868724,
    synopsis = "Enable time-travel",
    engineer = "Mr. Peabody",
    date     = "4/1/3007"
)
public static void travelThroughTime(Date destination) { ... }
複製代碼

註解類型也能夠沒有方法/元素,被稱爲標記註解類型,如:android

public @interface Preliminary { }

@Preliminary public class TimeTravel { ... }
複製代碼

若是註解類型只有一個元素,那麼元素應該命名爲 value,使用時也就能夠忽略元素名和等號了,如:git

public @interface Copyright {
    String value();
}

@Copyright("2002 Yoyodyne Propulsion Systems")
public class OscillationOverthruster { ... }
複製代碼

除了這些,不少註解還須要元註解去描述,如:github

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@Inherited
public @interface MethodInfo {
    String author() default "Peabody";
    String date();
    int version() default 1;
}
複製代碼

@Documented 代表該註解類型能夠被 javadoc 等工具文檔化 @Retention 代表該註解類型能夠保留多長時間,值爲枚舉值 RetentionPolicy:web

  • RetentionPolicy.SOURCE(只保留在源碼中,會被編譯器丟棄)
  • RetentionPolicy.CLASS(註解會被編譯器記錄在class文件中,但不須要被VM保留到運行時,這也是默認的行爲)
  • RetentionPolicy.RUNTIME(註解會被編譯器記錄在class文件中並被VM保留到運行時,因此能夠經過反射獲取)

@Target 代表該註解類型能夠註解哪些程序元素,若是註解類型不使用 @Target 描述那麼代表能夠註解全部程序元素,值是枚舉數組ElementType[]:api

  • ElementType.TYPE(類、接口(包括註解類型)、枚舉的聲明)
  • ElementType.FIELD(字段(包括枚舉常量)的聲明)
  • ElementType.METHOD(方法的聲明)
  • ElementType.PARAMETER(形參的聲明)
  • ElementType.CONSTRUCTOR(構造器的聲明)
  • ElementType.LOCAL_VARIABLE(本地變量的聲明)
  • ElementType.ANNOTATION_TYPE(註解類型的聲明)
  • ElementType.PACKAGE(包的聲明)
  • ElementType.TYPE_PARAMETER(泛型參數的聲明)
  • ElementType.TYPE_USE(泛型的使用)

@Inherited 代表該註解類型將被自動繼承。也就是說,若是註解類型被 @Inherited 註解,此時用戶查詢一個類聲明的註解,而類聲明沒被該註解類型註解,那麼將自動查詢該類父類的註解類型,以此類推直到找到該註解類型或達到頂層 Object 對象。數組

Android Support Library 中的註解

Android Support Library 提供了不少實用註解,如能夠使用 @NonNull 註解進行空檢查,使用 @UiThread@WorkerThread 註解進行線程檢查,使用 @IdRes 代表這個整數表明資源引用,還能夠經過 @IntDef@StringDef 註解自定義註解來代替枚舉,如描述應用中使用的字體文件:oracle

public final class TypefaceManager {

    public static final int FONT_TYPE_ICONIC = 0;
    public static final int FONT_TYPE_IMPACT = 1;
    public static final int FONT_TYPE_HELVETICA = 2;
    public static final int FONT_TYPE_DIN = 3;

    @Retention(RetentionPolicy.SOURCE)
    @IntDef({FONT_TYPE_ICONIC, FONT_TYPE_IMPACT, FONT_TYPE_HELVETICA, FONT_TYPE_DIN})
    @interface FontType {
    }

    private Context mContext;
    private SparseArray<Typeface> mTypefaceSparseArray;

    public TypefaceManager(Context context) {
        this.mContext = context;
        this.mTypefaceSparseArray = new SparseArray<>();
    }

    public static void setTypeface(TextView textView, @FontType int fontType) {
        Typeface localTypeface = MyApplication.getInstance().getTypefaceManager().getTypeface(fontType);
        if (localTypeface != null && localTypeface != textView.getTypeface()) {
            textView.setTypeface(localTypeface);
        }
    }

    public static void setTypeface(Paint paint, @FontType int fontType) {
        Typeface localTypeface = MyApplication.getInstance().getTypefaceManager().getTypeface(fontType);
        if (localTypeface != null && localTypeface != paint.getTypeface()) {
            paint.setTypeface(localTypeface);
        }
    }

    public Typeface getTypeface(@FontType int fontType) {
        Typeface typeface = mTypefaceSparseArray.get(fontType);
        if (typeface == null) {
            try {
                String path = null;
                if (fontType == FONT_TYPE_ICONIC) {
                    path = "fonts/fontawesome-webfont.ttf";
                } else if (fontType == FONT_TYPE_IMPACT) {
                    path = "fonts/impact.ttf";
                } else if (fontType == FONT_TYPE_HELVETICA) {
                    path = "fonts/Helvetica.ttf";
                } else if (fontType == FONT_TYPE_DIN) {
                    path = "fonts/ptdin.ttf";
                }
                typeface = Typeface.createFromAsset(mContext.getAssets(), path);
                this.mTypefaceSparseArray.put(fontType, typeface);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        return typeface;
    }

}
複製代碼

註解的使用與解析

對於 @Retention(RetentionPolicy.RUNTIME) 的註解,註解會被編譯器記錄在 class 文件中並被 VM 保留到運行時,因此能夠經過反射獲取,如:app

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@Inherited
public @interface MethodInfo {
    String author() default "Peabody";
    String date();
    int version() default 1;
}
複製代碼

使用

public class App {
    @MethodInfo(
        author = 「frank」,
        date = "2018/02/27",
        version = 2)
    public String getDescription() {
        return "no description";
    }
}
複製代碼

能夠寫個工具在運行時利用反射獲取註解:

public static void main(String[] args) {
    try {
        Class cls = Class.forName("com.frank.App");
        for (Method method : cls.getMethods()) {
            MethodInfo methodInfo = method.getAnnotation(
MethodInfo.class);
            if (methodInfo != null) {
                System.out.println("method author:" + methodInfo.author());
                System.out.println("method version:" + methodInfo.version());
                System.out.println("method date:" + methodInfo.date());
            }
        }
    } catch (ClassNotFoundException e) {
        e.printStackTrace();
    }
}
複製代碼

對於 @Retention(RetentionPolicy.CLASS) 的註解,註解會被編譯器記錄在 class 文件中,但不須要被 VM 保留到運行時,如:

@Retention(RetentionPolicy.CLASS)
@Target(ElementType.FIELD)
public @interface BindView {
    @IdRes int value();
}
複製代碼

也就是說,這種編譯時註解適合用來在編譯時自動生成代碼,這就須要 apt(Annotation Processing Tool)工具查找並執行註解處理器(Annotation Processor)以生成源碼和文件,最終 javac 會編譯這些原始源文件和自動生成的文件。Android Gradle 插件的 2.2 版本開始支持註解處理器,你只須要使用 annotationProcessor 依賴註解處理器或者使用 javaCompileOptions.annotationProcessorOptions {} DSL指定註解處理器便可。定義註解處理器最簡單的方式就是繼承 AbstractProcessor,在其 process 實現方法中實現註解元素的分析和源碼文件的生成。

自定義註解和註解處理器

以簡化一系列 findViewById 爲例:

package com.frank.simplebutterknife;

import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.widget.TextView;

public class MainActivity extends AppCompatActivity {

    TextView mTitleTextView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        mTitleTextView = (TextView) findViewById(R.id.titleTextView);
        mTitleTextView.setText("Hello World!");
    }
}
複製代碼

咱們但願利用自定義註解和註解處理器後能夠這樣寫:

package com.frank.simplebutterknife;

import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.widget.TextView;

import simplebutterknife.BindView;

public class MainActivity extends AppCompatActivity {

    @BindView(R.id.titleTextView)
    TextView mTitleTextView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        SimpleButterKnife.bind(this);
        mTitleTextView.setText("Hello World!");
    }
}

複製代碼

也就是說 SimpleButterKnife.bind(this); 一行代碼就完成了全部被 @BindView 註解的 View 的 findViewById 操做。而實現方式就是利用註解和註解編譯器在編譯時自動生成一個這樣的文件:

package com.frank.simplebutterknife;

import android.view.View;
import android.widget.TextView;

public class MainActivity_ViewBinding {
  public MainActivity target;

  public MainActivity_ViewBinding(MainActivity target) {
    this(target, target.getWindow().getDecorView());
  }

  public MainActivity_ViewBinding(MainActivity target, View source) {
    this.target = target;

    target.mTitleTextView = (TextView) source.findViewById(2131165300);
  }
}
複製代碼

SimpleButterKnife.bind(this); 的實現中加載這個類並執行構造器就能夠了。 實現起來也很簡單,先新建一個 java-library 的module:simplebutterknife-annotations,用來聲明註解:

@Retention(RetentionPolicy.CLASS)
@Target(ElementType.FIELD)
public @interface BindView {
    @IdRes int value();
}
複製代碼

RetentionPolicy.CLASS 代表這個註解只會在編譯時使用,ElementType.FIELD 代表這個註解只用於註解字段,@IdRes 是 android support library 中的編譯時檢查註解,代表註解的值必須是資源 ID,因此該 module 的依賴爲:

dependencies {
    compileOnly 'com.google.android:android:4.1.1.4'
    api 'com.android.support:support-annotations:27.0.2'
}
複製代碼

聲明完註解後,再新建一個註解處理器 java-library 的module:simplebutterknife-compiler,用來對註解的元素進行分析和生成源碼文件:

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

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

    @Override
    public Set<String> getSupportedAnnotationTypes() {
        Set<String> types = new LinkedHashSet<>();
        types.add(BindView.class.getCanonicalName());
        return types;
    }

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

    class BindingSet {
        TypeName targetTypeName;
        ClassName bindingClassName;
        List<ViewBinding> viewBindings;
    }

    class ViewBinding {
        TypeName type;
        int id;
        String name;
    }
}
複製代碼

@AutoService(Processor.class) 註解是利用了 Google 的 AutoService 爲註解處理器自動生成 metadata 文件並將註解處理器jar文件加入構建路徑,這樣也就不須要再手動建立並更新 META-INF/services/javax.annotation.processing.Processor 文件了。 覆寫 getSupportedSourceVersion() 方法指定能夠支持最新的 Java 版本,覆寫 getSupportedAnnotationTypes() 方法指定該註解處理器用於處理哪些註解(咱們這裏只處理 @BindView 註解)。而檢索註解元素並生成代碼的是 process 方法的實現:

Map<TypeElement, BindingSet> bindingMap = new LinkedHashMap<>();
複製代碼

須要爲每一個包含註解的 Activity 都生成一個對應的 _ViewBinding 文件,因此使用 Map 來存儲。BindingSet 存儲 Activity 信息和它的 View 綁定信息,View 綁定信息(ViewBinding)包括綁定 View 的類型、View 的 ID 以及 View 的變量名。

for (Element element : roundEnvironment.getElementsAnnotatedWith(BindView.class))
複製代碼

查找全部被 @BindView 註解的程序元素(Element),爲了簡化,這裏只認爲被註解的元素是 View 字段且它的外層元素(EnclosingElement)爲 Activity 類:

// 註解元素的外側元素,即 View 的所在 Activity 類
TypeElement enclosingElement = (TypeElement) element.getEnclosingElement();
// 註解的 value 值,即 View 的 id
int id = element.getAnnotation(BindView.class).value();
// 註解元素的名字,即 View 變量名
Name simpleName = element.getSimpleName();
String name = simpleName.toString();
// 註解元素的類型,即 View 的類型
TypeMirror elementType = element.asType();
TypeName type = TypeName.get(elementType);
複製代碼

而後把這些信息存到 Activity 對應的 View 綁定中:

BindingSet bindingSet = bindingMap.get(enclosingElement);
if (bindingSet == null) {
    bindingSet = new BindingSet();
    TypeMirror typeMirror = enclosingElement.asType();
    TypeName targetType = TypeName.get(typeMirror);
    String packageName = MoreElements.getPackage(enclosingElement).getQualifiedName().toString();
    String className = enclosingElement.getQualifiedName().toString().substring(
            packageName.length() + 1).replace('.', '$');
    ClassName bindingClassName = ClassName.get(packageName, className + "_ViewBinding");
    bindingSet.targetTypeName = targetType;
    bindingSet.bindingClassName = bindingClassName;
    bindingMap.put(enclosingElement, bindingSet);
}
if (bindingSet.viewBindings == null) {
    bindingSet.viewBindings = new ArrayList<>();
}
ViewBinding viewBinding = new ViewBinding();
viewBinding.type = type;
viewBinding.id = id;
viewBinding.name = name;
bindingSet.viewBindings.add(viewBinding);
複製代碼

肯定完 Activity 信息和它對應的 View 綁定信息後,爲每一個 Activity 生成對應的 XXX_ViewBinding.java 文件,文件內容就是前面所說相似這樣的綁定類:

package com.frank.simplebutterknife;

import android.view.View;
import android.widget.TextView;

public class MainActivity_ViewBinding {
  public MainActivity target;

  public MainActivity_ViewBinding(MainActivity target) {
    this(target, target.getWindow().getDecorView());
  }

  public MainActivity_ViewBinding(MainActivity target, View source) {
    this.target = target;

    target.mTitleTextView = (TextView) source.findViewById(2131165300);
  }
}
複製代碼

雖然經過字符串拼接能夠拼出這樣的文件內容,但咱們還得考慮 import,還得考慮大括號和換行,甚至還得考慮註釋和代碼美觀,因此利用 JavaPoet 來生成 .java 文件是個不錯的選擇:

for (Map.Entry<TypeElement, BindingSet> entry : bindingMap.entrySet()) {
            TypeElement typeElement = entry.getKey();
            BindingSet binding = entry.getValue();

            TypeName targetTypeName = binding.targetTypeName;
            ClassName bindingClassName = binding.bindingClassName;
            List<ViewBinding> viewBindings = binding.viewBindings;
            // binding 類
            TypeSpec.Builder viewBindingBuilder = TypeSpec.classBuilder(bindingClassName.simpleName())
                    .addModifiers(Modifier.PUBLIC);
            // public的target字段用來保存 Activity 引用
            viewBindingBuilder.addField(targetTypeName, "target", Modifier.PUBLIC);
            // 構造器
            MethodSpec.Builder activityViewBuilder = MethodSpec.constructorBuilder()
                    .addModifiers(Modifier.PUBLIC)
                    .addParameter(targetTypeName, "target");
            activityViewBuilder.addStatement("this(target, target.getWindow().getDecorView())");
            viewBindingBuilder.addMethod(activityViewBuilder.build());
            // 第二個構造器
            MethodSpec.Builder viewBuilder = MethodSpec.constructorBuilder()
                    .addModifiers(Modifier.PUBLIC)
                    .addParameter(targetTypeName, "target")
                    .addParameter(ClassName.get("android.view", "View"), "source");
            viewBuilder.addStatement("this.target = target");
            viewBuilder.addCode("\n");
            for (ViewBinding viewBinding : viewBindings) {
                CodeBlock.Builder builder = CodeBlock.builder()
                        .add("target.$L = ", viewBinding.name);
                builder.add("($T) ", viewBinding.type);
                builder.add("source.findViewById($L)", CodeBlock.of("$L", viewBinding.id));
                viewBuilder.addStatement("$L", builder.build());
            }
            viewBindingBuilder.addMethod(viewBuilder.build());
            // 輸出 Java 文件
            JavaFile javaFile = JavaFile.builder(bindingClassName.packageName(), viewBindingBuilder.build())
                    .build();
            try {
                javaFile.writeTo(processingEnv.getFiler());
            } catch (IOException e) {
                processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, e.getMessage());
            }
        }
複製代碼

好了,註解處理器已經寫完了,再調整一下註解處理器 module 的依賴:

dependencies {
    implementation project(':simplebutterknife-annotations')
    implementation 'com.google.auto:auto-common:0.10'
    api 'com.squareup:javapoet:1.9.0'
    compileOnly 'com.google.auto.service:auto-service:1.0-rc4'
}
複製代碼

在 app module 中須要依賴註解 module 並註冊註解處理器 module:

dependencies {
    ...
    api project(':simplebutterknife-annotations')
    annotationProcessor project(':simplebutterknife-compiler')
}
複製代碼

app module 中的工具類 SimpleButterKnifebind 方法只須要加載這個自動生成的類並執行它的構造器就好了:

public final class SimpleButterKnife {

    public static void bind(Activity target) {
        View sourceView = target.getWindow().getDecorView();
        Class<?> targetClass = target.getClass();
        String targetClassName = targetClass.getName();
        Constructor constructor;
        try {
            Class<?> bindingClass = targetClass.getClassLoader().loadClass(targetClassName + "_ViewBinding");
            constructor = bindingClass.getConstructor(targetClass, View.class);
        } catch (ClassNotFoundException e) {
            // TODO Not found. should try search its superclass
            throw new RuntimeException("Not found. should try search its superclass of " + targetClassName, e);
        } catch (NoSuchMethodException e) {
            throw new RuntimeException("Unable to find binding constructor for " + targetClassName, e);
        }
        try {
            constructor.newInstance(target, sourceView);
        } catch (IllegalAccessException e) {
            throw new RuntimeException("Unable to invoke " + constructor, e);
        } catch (InstantiationException e) {
            throw new RuntimeException("Unable to invoke " + constructor, e);
        } catch (InvocationTargetException e) {
            Throwable cause = e.getCause();
            if (cause instanceof RuntimeException) {
                throw (RuntimeException) cause;
            }
            if (cause instanceof Error) {
                throw (Error) cause;
            }
            throw new RuntimeException("Unable to create binding instance.", cause);
        }
    }
}
複製代碼

從新構建下工程,就能夠在 build\generated\source\apt\debug 目錄中查看自動生成的文件了:

package com.frank.simplebutterknife;

import android.view.View;
import android.widget.TextView;

public class MainActivity_ViewBinding {
  public MainActivity target;

  public MainActivity_ViewBinding(MainActivity target) {
    this(target, target.getWindow().getDecorView());
  }

  public MainActivity_ViewBinding(MainActivity target, View source) {
    this.target = target;

    target.mTitleTextView = (TextView) source.findViewById(2131165300);
  }
}

複製代碼

此時,看一下 Butter Knife 的源碼,其實就是在基礎上的補充完善。

總結

編譯時的註解和註解處理器能夠生成一些模板代碼,因爲不涉及到反射因此也不會影響性能,註解的使用也會讓代碼獲得簡化,更加直觀優雅,因此不少項目都在使用,包括 Butter Knife、Dagger二、EventBus、Glide 等開源庫,因此有必要了解並使用註解,尤爲是編譯時註解。

參考

相關文章
相關標籤/搜索