java.lang.annotation
,接口 Annotation,在JDK5.0及之後版本引入。html
註解處理器是 javac 的一個工具,它用來在編譯時掃描和處理註解(Annotation)。你能夠自定義註解,並註冊到相應的註解處理器,由註解處理器來處理你的註解。一個註解的註解處理器,以 Java 代碼(或者編譯過的字節碼)做爲輸入,生成文件(一般是 .java 文件)做爲輸出。這些生成的 Java 代碼是在生成的 .java 文件中,因此你不能修改已經存在的 Java 類,例如向已有的類中添加方法。這些生成的 Java 文件,會同其餘普通的手動編寫的 Java 源代碼同樣被 javac 編譯。java
JDK除了提供上述的幾種基本的註釋外,還提供了幾種註釋,用於修飾其餘的註解定義android
@retention 這個是決定你註釋存活的時間的,它包含一個RetationPolicy的值成員變量,用於指定它所修飾的註釋保留時間,通常有:api
@target 這個註解通常用來指定被修飾的註釋修飾哪些元素,這個註解也包含一個值變量:oracle
@Document 這個註解修飾的註釋類能夠被 javadoc 的工具提取成文檔app
@Inherited 被他修飾的註解具備繼承性ide
上面講了一些JDK自帶的註釋,那麼咱們如今就能夠用這些JDK自帶的註釋來實現一些咱們想要的功能。先一步一步地模仿 butterknife 的實現吧。函數
定義一個註解:工具
@Target(ElementType.FIELD) // 用於成員變量 @Retention(RetentionPolicy.CLASS) //註解保留在 class 文件, 當Java的程序執行的時候,JVM將拋棄它 public @interface BindView { int value(); }
@interface
和接口的定義方式就少一個 @ 哦,不要搞混了。裏面有一個變量值時,就是咱們使用的時候 @BindView
(R.id.textView)
指定的 R.id.textView
id,旨在自動注入 view 的 id。
先來講下註解處理器 AbstractProcessor
。它是 javac 的一個工具,用來在編譯時掃描和處理註解 Annotation,
你能夠自定義註解,並註冊到相應的註解處理器,由註解處理器來處理你的註解。post
一個註解的註解處理器,以 Java 代碼(或者編譯過的字節碼)做爲輸入,生成文件(一般是.java文件)做爲輸出。這些由註解器生成的.java代碼和普通的.java同樣,能夠被javac編譯。
由於 AbstractProcessor
是 javac 中的一個工具,因此在 Android 的工程下無法直接調用。
在 build.gradle 引入相關 jar :
apply plugin: 'java-library' dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) api 'com.squareup:javapoet:1.7.0' api 'com.google.auto.service:auto-service:1.0-rc2' api project(':lib-annotation') } sourceCompatibility = "1.7" targetCompatibility = "1.7" //指定編譯的編碼 tasks.withType(JavaCompile){ options.encoding = "UTF-8" }
javapoet 和 auto-service 後面會講到,這兩個在註解處理器中有着極大的做用。
引入以後,就開始編寫註解處理器了。
@AutoService(Processor.class) public class CustomProcessor extends AbstractProcessor { private static final String TAG = "CustomProcessor"; // 文件相關的輔助類 private Filer mFiler; // 元素相關的輔助類 private Elements mElements; // 元素相關的輔助類 private Elements mElementUtils; /** * 解析的目標註解集合 */ private Map<String, AnnotatedClass> mAnnotatedClassMap = new HashMap<>(); @Override public synchronized void init(ProcessingEnvironment processingEnvironment) { super.init(processingEnvironment); mElementUtils = processingEnvironment.getElementUtils(); mFiler = processingEnvironment.getFiler(); } //核心處理邏輯,至關於java中的主函數main(),你須要在這裏編寫你本身定義的註解的處理邏輯 //返回值 true時表示當前處理,不容許後續的註解器處理 @Override public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) { mAnnotatedClassMap.clear(); try { processBindView(roundEnvironment); } catch (IllegalArgumentException e) { return true; } try { for (AnnotatedClass annotatedClass : mAnnotatedClassMap.values()) { annotatedClass.generateFinder().writeTo(mFiler); } } catch (Exception e) { e.printStackTrace(); } return true; } @Override public Set<String> getSupportedAnnotationTypes() { Set<String> types = new LinkedHashSet<>();
// 標明該註解處理器是爲了處理 BindView 註解的 types.add(BindView.class.getCanonicalName()); return types; } private void processBindView(RoundEnvironment roundEnv) { for (Element element : roundEnv.getElementsAnnotatedWith(BindView.class)) { // element = tv1; AnnotatedClass annotatedClass = getAnnotatedClass(element); BindViewField field = new BindViewField(element); annotatedClass.addField(field);
// 經過上面方法調用,能夠獲取到註解元素,以及和註解元素相關的類名,經過註解元素得到被註解的成員變量名,後續會對其進行初始化 } } private AnnotatedClass getAnnotatedClass(Element element) {
// 經過註解元素獲取其封裝類,得到類的引用 TypeElement encloseElement = (TypeElement) element.getEnclosingElement(); // encloseElement.getSimpleName() = MainActivity; String fullClassName = encloseElement.getQualifiedName().toString(); // com.sjq.recycletest.MainActivity AnnotatedClass annotatedClass = mAnnotatedClassMap.get(fullClassName); if (annotatedClass == null) {
// 存到map當中,不用每次都生成一次,這樣一個類裏面有多個註解的時候,能夠加快處理速度 annotatedClass = new AnnotatedClass(encloseElement, mElementUtils); mAnnotatedClassMap.put(fullClassName, annotatedClass); } return annotatedClass; } @Override public SourceVersion getSupportedSourceVersion() { return SourceVersion.latestSupported(); } }
init(ProcessingEnvironment processingEnvironment)
:每個註解處理器類都必須有一個空的構造函數。然而,這裏有一個特殊的init()方法,它會被註解處理工具調用,並輸入 ProcessingEnviroment 參數.ProcessingEnviroment 提供不少有用的工具類 Types 和 Filer。後面咱們將看到詳細的內容。
process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment)
:這至關於每一個處理器的主函數main()。你在這裏寫你的掃描,評估和處理註解的代碼,以及生成Java文件。輸入參數 RoundEnviroment,可讓你查詢出包含特定註解的被註解元素。後面咱們將看到詳細的內容。
getSupportedAnnotationTypes()
:這裏你必須指定,這個註解處理器是註冊給哪一個註解的。注意,它的返回值是一個字符串的集合,包含本處理器想要處理的註解類型的合法全稱。換句話說,你在這裏定義你的註解處理器註冊到哪些註解上。
getSupportedSourceVersion()
:用來指定你使用的Java版本。一般這裏返回SourceVersion.latestSupported()。然而,若是你有足夠的理由只支持Java 7 的話,你也能夠返回 SourceVersion.RELEASE_7。推薦你使用前者。
首先咱們先簡單的說明一下 porcess
的處理流程:
遍歷 env,獲得咱們須要的元素列表
將元素列表封裝成對象,方便以後的處理,好比獲取元素的各類屬性等
經過 JavaPoet 庫將對象以咱們指望的形式生成 java 文件,來處理註解
if (element.getKind() == ElementKind.FEILD) { // 顯示轉換元素類型 TypeElement typeElement = (TypeElement) element; // 輸出元素名稱 System.out.println(typeElement.getSimpleName()); // 輸出註解屬性值 System.out.println(typeElement.getAnnotation(BindView.class).value()); } }
上面的代碼和 processBindView 的代碼是同樣的。判斷元素類型,在進一步處理。有些註解可能對類和方法是同時生效的,這時候,判斷類型分別處理就顯得很是有必要了。
getElementsAnnotatedWith
可以獲取到添加該註解的全部元素列表
其實不進行封裝也是能夠的,可是這樣當咱們在使用的時候,可能就須要在不一樣的地方寫不少重複的代碼,爲此,能夠進一步封裝,當咱們須要獲取元素屬性的時候,直接調用相關方法便可。
新建類 BindViewField.class
用來保存自定義註解 BindView
相關的屬性,後續須要元素上的信息均可以從該類獲取。
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。
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; // type.getQualifiedName().toString() = com.sjq.recycletest.MainActivity return type.getQualifiedName().toString().substring(packageLen).replace('.', '$'); } }
第三步: 經過 JavaPoet 庫將對象以咱們指望的形式生成 java 文件
經過上述兩步成功獲取了自定義註解的元素對象,可是仍是缺乏一步關鍵的步驟,缺乏一步 findViewById(),
實際上 ButterKnife 這個很出名的庫也並無省略 findViewById()
這一個步驟,只是在編譯的時候,在 build/generated/source/apt/debug 下生成了一個文件,幫忙執行了findViewById()
這一行爲而已。
一樣的,咱們這裏也須要生成一個 java 文件,採用的是 JavaPoet 這個庫。具體的使用 參考連接
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); // com.sjq.recycletest String className = getClassName(mClassElement, packageName); ClassName bindClassName = ClassName.get(packageName, className); // bindClassName.toString() com.sjq.recycletest.MainActivity //構建類對象,注意此處的 $$Injector,生成的類名是由咱們本身來控制的 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 下新建 android library。
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); }
ActivityFinder
顧名思義,就是經過 activity 的 findViewById 來找到某個 view。
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();
// 用於存儲已經綁定的class,避免重複綁定 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) {
// 此處拿到的類名就是經過註解生成的中間處理類,即 MainActivity$$Injector Class<?> finderClass = Class.forName(className + "$$Injector");
// 經過反射拿到class實例 injector = (Injector) finderClass.newInstance(); FINDER_MAP.put(className, injector); } injector.inject(host, source, finder); } catch (Exception e) { e.printStackTrace(); } } }
在 bind 方法內部,經過必定的規則拼接最後生成的 .java 文件類名,而後經過反射的方法拿到實例,最後,調用 injector.inject 的方法來完成初始化,該方法就是得經過註解來生成的。
對應的按鈕能夠直接使用,不須要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);
// tv1 的初始化過程就是在bind過程當中完成的 ButterKnife.bind(this); tv1.setText("annotation_demo"); } }
選擇 build 下的 make project,會進行編譯:
最終生成的類名以下:
public class MainActivity$$Injector implements Injector<MainActivity> { @Override public void inject(final MainActivity host, Object source, Finder finder) {
// 最終仍是經過 findViewById 來對 tv1 進行初始化 host.tv1=(TextView)finder.findView(source,2131165300); } }
到此,該實例講解結束。
下面會對上面的實例的思想進行總結,方便你們進一步理解其實現原理:
二、註解生成代碼過程。經過獲取成員變量所屬的類,根據類名和命名規則獲取最後註解會生成的類名,經過反射的形式,調用其中的 inject 方法。inject 方法中會有 activity 的 this 引用,經過 findViewById 方法,便可爲 tv1 初始化。這樣後面調用 tv1.setText 就不會出現空指針了。
項目源碼:https://download.csdn.net/download/szengjiaqi/10629127
不能下載的,留下郵箱,發你
參考文獻:
一、Android的編譯時註解APT實戰(AbstractProcessor)
總結:其實你會發現最終被註解的 id 也仍是經過 findViewById 方法來查找資源的。假設咱們不使用註解,將生成的類放到外層來,直接使用的話,這樣子就得在activity中引入更多的類,代碼上也會更加混亂,很差維護。可是理解上可能比註解更好理解吧。
在假設,若是咱們不是在 activty 使用註解呢,對於通常的 view 或者 其餘類來使用的話能夠嗎?能夠的,你只須要傳入持有 findViewById 對象便可。好比 rootview.
使用註解的方式,讓代碼邏輯變得更加清楚,依賴也會下降。可是對於不瞭解的註解的同窗,可能代碼看起來會比較困難,可是使用很容易。
最後,咱們想一想,何時推薦使用註解呢?我的以爲是重複工做比較多的時候,這時候是須要註解的,由於工做重複,註解能夠很方便就完成。