1. 程式人生 > >仿照 ButterKnife 的 Android 註解實例

仿照 ButterKnife 的 Android 註解實例

canonical super nds nload 通過反射 inf 文件相關 down you

什麽是註解

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

註解處理器是 javac 的一個工具,它用來在編譯時掃描和處理註解(Annotation)。你可以自定義註解,並註冊到相應的註解處理器,由註解處理器來處理你的註解。一個註解的註解處理器,以 Java 代碼(或者編譯過的字節碼)作為輸入,生成文件(通常是 .java 文件)作為輸出。這些生成的 Java 代碼是在生成的 .java 文件中,所以你不能修改已經存在的 Java 類,例如向已有的類中添加方法。這些生成的 Java 文件,會同其他普通的手動編寫的 Java 源代碼一樣被 javac 編譯。

基本的註解

  • @ Override--限定重寫父類方法
  • @ Deprecated--標示已過時
  • @ SuppressWarning--抑制編譯器警告
  • @ SafeVarargs--這貨與Java7裏面的堆汙染有關,具體想了解的,到傳送這裏

JDK的元註解

JDK除了提供上述的幾種基本的註釋外,還提供了幾種註釋,用於修飾其他的註解定義

  1. @retention 這個是決定你註釋存活的時間的,它包含一個RetationPolicy的值成員變量,用於指定它所修飾的註釋保留時間,一般有:

      • Retationpolicy.CLASS:  編譯器將把註釋記錄在類文件中,不過當Java的程序執行的時候,JVM將拋棄它。
      • Retationpolicy.SOURCE:  Annotation只保留在原代碼中,當編譯器編譯的時候就會拋棄它。
      • Retationpolicy.RUNTIME:  在Retationpolicy.CLASS的基礎上,JVM執行的時候也不會拋棄它,所以我們一般在程序中可以通過反射來獲得這個註解,然後進行處理。
  2. @target 這個註解一般用來指定被修飾的註釋修飾哪些元素,這個註解也包含一個值變量:

      • ElementType.ANNOTATION_TYPE:  指定該註釋只能修飾註釋。
      • ElementType.CONSTRUCTOR:  指定只能修飾構造器。
      • ElementType.FIELD:  指定只能成員變量。
      • ElementType.LOCAL_VARIABLE:  指定只能修飾局部變量。
      • ElementType.METHOD:  指定只能修飾方法。
      • ElementType.PACKAGE:  指定只能修飾包定義。
      • ElementType.PARAMETER:  指定只能修飾參數。
      • ElementType.TYPE:  指定可以修飾類,接口,枚舉定義。
  3. @Document 這個註解修飾的註釋類可以被 javadoc 的工具提取成文檔

  4. @Inherited 被他修飾的註解具有繼承性


自定義註釋

上面講了一些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,你可以自定義註解,並註冊到相應的註解處理器,由註解處理器來處理你的註解。

一個註解的註解處理器,以 Java 代碼(或者編譯過的字節碼)作為輸入,生成文件(通常是.java文件)作為輸出。這些由註解器生成的.java代碼和普通的.java一樣,可以被javac編譯。

因為 AbstractProcessor 是 javac 中的一個工具,所以在 Android 的工程下沒法直接調用。

新建一個 java library

在 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。推薦你使用前者。

在類前面,使用了 AutoService 註解,這個在 build.gradle 中已經引入。AutoService 註解處理器是 Google 開發的,用來生成 META-INF/services/ javax.annotation.processing.Processor 文件的,你只需要在你定義的註解處理器上添加 @AutoService(Processor.class) 就可以了,簡直不能再方便了。

註解器處理流程

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

  1. 遍歷 env,得到我們需要的元素列表

  2. 將元素列表封裝成對象,方便之後的處理,比如獲取元素的各種屬性等

  3. 通過 JavaPoet 庫將對象以我們期望的形式生成 java 文件,來處理註解

第一步:遍歷env,得到我們需要的元素列

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);
}

activity實現類 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); } }

到此,該實例講解結束。

總結:

下面會對上面的實例的思想進行總結,方便大家進一步理解其實現原理:

1、先看代碼層面的。可以看到在主工程先對成員變量 tv1 添加了註解,獲得了 view 的 id;其次先調用 bind 方法,獲取到 activity 實例。接著就開始調用 tv1.setText。可是有沒有發現,tv1 沒有初始化。其初始化過程就是在 bind 這個過程當中。

2、註解生成代碼過程。通過獲取成員變量所屬的類,根據類名和命名規則獲取最後註解會生成的類名,通過反射的形式,調用其中的 inject 方法。inject 方法中會有 activity 的 this 引用,通過 findViewById 方法,即可為 tv1 初始化。這樣後面調用 tv1.setText 就不會出現空指針了。

項目源碼:https://download.csdn.net/download/szengjiaqi/10629127

不能下載的,留下郵箱,發你

參考文獻:

1、Android的編譯時註解APT實戰(AbstractProcessor)

2、淺談Android下的註解

仿照 ButterKnife 的 Android 註解實例