Android 編譯時註解-提高

背景

在前面的文章中,講解了註解和編譯時註解等一些列相關的內容,爲了更加全面和真是的瞭解Android 編譯時註解在實戰項目中的使用,本文采起實現主流框架butterknife注入view去全面認識編譯時註解。javascript

註解專欄-博客html

效果

先來張效果圖壓壓驚

先來張圖壓壓驚,實現效果butterknifeview綁定java

使用

仿照butterknife實現了@BindView註解,經過WzgJector.bind方法綁定當前MainActivity,總體和butterknife使用徹底如出一轍,這裏爲了區分簡單的把butterknife更名了WzgJectorandroid

public class MainActivity extends AppCompatActivity {

    @BindView(R.id.tv_msg)
    TextView tvMsg;
    @BindView(R.id.tv_other)
    TextView tvOther;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        WzgJector.bind(this);
        if(tvMsg!=null){
            tvMsg.setText("我已經成功初始化了");
        }

        if(tvOther!=null){
            tvOther.setText("我就來看看而已");
        }
    }
}複製代碼

實現

實現的思路和Android編譯時註解-初認識實現原理大體同樣,因此這裏不重複闡述重複的步驟,重點講解提高的技術點,因此須要在瞭解基本編譯時註解的前提下繼續下面的學習git

Android編譯時註解-初認識github

定義註解

這裏使用了javaandroid自帶的註解,初始一個BindView註解,同時指定了@TargetFIELD,註解BindView帶有一個初始的int參數及時使用時的view-idapi

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

javaandroid自帶的註解不太清楚的同窗可參考下面兩篇文章app

Java-註解詳解框架

Android-註解詳解ide

Element詳解

Element

有了註解,必然須要有一個對應的註解處理器去處理註解,可是在處理註解的時候須要充分的瞭解註解處理器中的process方法及時核心的編譯代碼,而process方法的核心即是Element對象,因此在講解註解處理器前,須要對Element有全面的認識,方能事半功倍。

因爲Element的知識內容的複雜性,這裏重點講解核心內容,基本使用徹底是足夠了

源碼:

public interface Element extends AnnotatedConstruct {
    TypeMirror asType();

    ElementKind getKind();

    Set<Modifier> getModifiers();

    Name getSimpleName();

    Element getEnclosingElement();

    List<? extends Element> getEnclosedElements();

    boolean equals(Object var1);

    int hashCode();

    List<? extends AnnotationMirror> getAnnotationMirrors();

    <A extends Annotation> A getAnnotation(Class<A> var1);

    <R, P> R accept(ElementVisitor<R, P> var1, P var2);
}複製代碼

可看出其實Element是定義的一個接口,定義了外部調用暴露出的接口

方法 解釋
asType 返回此元素定義的類型
getKind 返回此元素的種類:包、類、接口、方法、字段...,以下枚舉值
getModifiers 返回此元素的修飾符,以下枚舉值
getSimpleName 返回此元素的簡單名稱,好比activity名
getEnclosingElement 返回封裝此元素的最裏層元素,若是此元素的聲明在詞法上直接封裝在另外一個元素的聲明中,則返回那個封裝元素; 若是此元素是頂層類型,則返回它的包若是此元素是一個包,則返回 null; 若是此元素是一個泛型參數,則返回 null.
getAnnotation 返回此元素針對指定類型的註解(若是存在這樣的註解),不然返回 null。註解能夠是繼承的,也能夠是直接存在於此元素上的

getKind方法

其中getKind方法比較特殊,getKind()方法來獲取具體的類型,方法返回一個枚舉值TypeKind

源碼:

public enum TypeKind {  
    /** The primitive type {@code boolean}. */  
    BOOLEAN,  
    /** The primitive type {@code byte}. */  
    BYTE,  
    /** The primitive type {@code short}. */  
    SHORT,  
    /** The primitive type {@code int}. */  
    INT,  
    /** The primitive type {@code long}. */  
    LONG,  
    /** The primitive type {@code char}. */  
    CHAR,  
    /** The primitive type {@code float}. */  
    FLOAT,  
    /** The primitive type {@code double}. */  
    DOUBLE,  
    /** The pseudo-type corresponding to the keyword {@code void}. */  
    VOID,  
    /** A pseudo-type used where no actual type is appropriate. */  
    NONE,  
    /** The null type. */  
    NULL,  
    /** An array type. */  
    ARRAY,  
    /** A class or interface type. */  
    DECLARED,  
    /** A class or interface type that could not be resolved. */  
    ERROR,  
    /** A type variable. */  
    TYPEVAR,  
    /** A wildcard type argument. */  
    WILDCARD,  
    /** A pseudo-type corresponding to a package element. */  
    PACKAGE,  
    /** A method, constructor, or initializer. */  
    EXECUTABLE,  
    /** An implementation-reserved type. This is not the type you are looking for. */  
    OTHER,  
    /** A union type. */  
    UNION,  
    /** An intersection type. */  
    INTERSECTION;  
}複製代碼

Element子類

Element 有五個直接子接口,它們分別表明一種特定類型的元素

Tables Are
TypeElement 一個類或接口程序元素
VariableElement 一個字段、enum 常量、方法或構造方法參數、局部變量或異常參數
ExecutableElement 某個類或接口的方法、構造方法或初始化程序(靜態或實例),包括註解類型元素
PackageElement 一個包程序元素
TypeParameterElement 通常類、接口、方法或構造方法元素的泛型參數

五個子類各有各的用處而且有各類獨立的方法,在使用的時候能夠強制將Element對象轉換成其中的任一一種,可是前提是知足條件的轉換,否則會拋出異常。

其中最核心的兩個子分別是TypeElementVariableElement

TypeElement詳解

TypeElement定義的一個類或接口程序元素,至關於當前註解所在的class對象,及時本案例使用代碼中的MainActivity

源碼以下:

public interface TypeElement extends Element, Parameterizable, QualifiedNameable {
    List<? extends Element> getEnclosedElements();

    NestingKind getNestingKind();

    Name getQualifiedName();

    Name getSimpleName();

    TypeMirror getSuperclass();

    List<? extends TypeMirror> getInterfaces();

    List<? extends TypeParameterElement> getTypeParameters();

    Element getEnclosingElement();
}複製代碼

這裏講解主要的方法的含義

方法 解釋
getNestingKind 返回此類型元素的嵌套種類
getQualifiedName 返回此類型元素的徹底限定名稱。更準確地說,返回規範 名稱。對於沒有規範名稱的局部類和匿名類,返回一個空名稱.
getSuperclass 返回此類型元素的直接超類。若是此類型元素表示一個接口或者類 java.lang.Object,則返回一個種類爲 NONE 的 NoType
getInterfaces 返回直接由此類實現或直接由此接口擴展的接口類型
getTypeParameters 按照聲明順序返回此類型元素的形式類型參數

VariableElement詳解

源碼:

public interface VariableElement extends Element {
    Object getConstantValue();

    Name getSimpleName();

    Element getEnclosingElement();
}複製代碼

這裏VariableElement除了擁有Element的方法之外還有如下兩個方法

方法 解釋
getConstantValue 變量初始化的值
getEnclosingElement 獲取相關類信息

註解處理器

註解處理器須要兩個步驟的處理:

  • 1.收集先關的信息

  • 2.生成處理類

Element有了全面的瞭解事後,註解處理器即可很輕鬆的學習了,先來看看簡單版本的BindView處理

Set<? extends Element> elements = roundEnv.getElementsAnnotatedWith(BindView.class);
        //1、收集信息
        for (Element element : elements) {
            /*檢查類型*/
            if (!(element instanceof VariableElement)) {
                return false;
            }
            VariableElement variableElement = (VariableElement) element;

            /*獲取類信息*/
            TypeElement typeElement = (TypeElement) variableElement.getEnclosingElement();
            /*類的絕對路徑*/
            String qualifiedName = typeElement.getQualifiedName().toString();
            /*類名*/
            String clsName = typeElement.getSimpleName().toString();
            /*獲取包名*/
            String packageName = processingEnv.getElementUtils().getPackageOf(typeElement).getQualifiedName().toString();

            BindView annotation = variableElement.getAnnotation(BindView.class);
            int id = annotation.value();

            /*參數名*/
            String name = variableElement.getSimpleName().toString();
            /*參數對象類*/
            String type = variableElement.asType().toString();

            ClassName InterfaceName = ClassName.bestGuess("com.example.annotation.api.ViewInjector");
            ClassName host = ClassName.bestGuess(qualifiedName);

            MethodSpec main = MethodSpec.methodBuilder("inject")
                    .addModifiers(Modifier.PUBLIC)
                    .returns(void.class)
                    .addAnnotation(Override.class)
                    .addParameter(host, "host")
                    .addParameter(Object.class, "object")
                    .addCode(""
                            + " if(object instanceof android.app.Activity){\n"
                            + " host." + name + " = (" + type + ")(((android.app.Activity)object).findViewById(" + id + "));\n"
                            + " }\n"
                            + "else{\n"
                            + " host." + name + " = (" + type + ")(((android.view.View)object).findViewById(" + id + "));\n"
                            + "}\n")
                    .build();

            TypeSpec helloWorld = TypeSpec.classBuilder(clsName + "ViewInjector")
                    .addModifiers(Modifier.PUBLIC)
                    .addMethod(main)
                    .addSuperinterface(ParameterizedTypeName.get(InterfaceName, host))
                    .build();

            try {
                // 生成 com.example.HelloWorld.java
                JavaFile javaFile = JavaFile.builder(packageName, helloWorld)
                        .addFileComment(" This codes are generated automatically. Do not modify!")
                        .build();
                // 生成文件
                javaFile.writeTo(filer);
            } catch (IOException e) {
                e.printStackTrace();
            }複製代碼

大致的思路,先判斷Element類型,若是是VariableElement則繼續獲取相關的包名(這裏必須在app包名一致,否則獲取不到android類)類對象信息,以及@BindView註解修飾的參數數據;最後將全部須要的數據經過javapoetFiler自動編譯建立一個java文件

最後獲得的生成類:

package com.wzgiceman.viewinjector;

import com.example.ViewInjector;
import java.lang.Object;
import java.lang.Override;

public class MainActivityViewInjector implements ViewInjector<MainActivity> {
  @Override
  public void inject(MainActivity host, Object object) {
     if(object instanceof android.app.Activity){
     host.tvMsg = (android.widget.TextView)(((android.app.Activity)object).findViewById(2131492945));
     }
    else{
     host.tvMsg = (android.widget.TextView)(((android.view.View)object).findViewById(2131492945));
    }
  }
}複製代碼

上面的簡單處理器中,只是單純的判斷一個註解狀況,在信息收集的處理上簡化了,致使當前處理器只能同時處理當前相同類中的莫一個註解這裏只初始化了tvMsg對象,tvOther並無初始化,固然這是不符合實際需求的,下面來優化收集和處理方案。

優化

優化方案其實就是多了一步信息的記錄的工做

建立信息類對象

首先建立一個類信息對象,其中包含了一下的屬性,其中varMap即是記錄當前類中全部註解相關的信息

public class VariMsg {
    /*包名*/
    private String pk;
    /*類名*/
    private String clsName;
    /*註解對象*/
    private HashMap<Integer,VariableElement> varMap;
    }複製代碼

BindViewProcessors

1.初始一個map記錄VariMsg對象,由於process方法可能會屢次調用,因此須要每次都clear一遍

Map<String, VariMsg> veMap = new HashMap<>();複製代碼

2.記錄信息
經過veMap記錄全部的相關信息,而且每次須要判斷是否重複,剔除重複的數據。

for (Element element : elements) {
            /*檢查類型*/
            if (!(element instanceof VariableElement)) {
                return false;
            }
            VariableElement variableElement = (VariableElement) element;

            /*獲取類信息*/
            TypeElement typeElement = (TypeElement) variableElement.getEnclosingElement();
            /*類的絕對路徑*/
            String qualifiedName = typeElement.getQualifiedName().toString();
            /*類名*/
            String clsName = typeElement.getSimpleName().toString();
            /*獲取包名*/
            String packageName = processingEnv.getElementUtils().getPackageOf(typeElement).getQualifiedName().toString();

            BindView annotation = variableElement.getAnnotation(BindView.class);
            int id = annotation.value();

            VariMsg variMsg = veMap.get(qualifiedName);
            if (variMsg == null) {
                variMsg = new VariMsg(packageName, clsName);
                variMsg.getVarMap().put(id, variableElement);
                veMap.put(qualifiedName, variMsg);
            } else {
                variMsg.getVarMap().put(id, variableElement);
    }
   }複製代碼

3.經過javapoet去生成java類文件
這裏主要是javapoet的運用,詳細用法可去javapoet查看

javapoet-GitHub

System.out.println("xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx");
        for (String key : veMap.keySet()) {
            ClassName InterfaceName = ClassName.bestGuess("com.example.ViewInjector");
            ClassName host = ClassName.bestGuess(key);
            VariMsg variMsg = veMap.get(key);

            StringBuilder builder = new StringBuilder();
            builder.append(" if(object instanceof android.app.Activity){\n");
            builder.append(code(variMsg.getVarMap(), "android.app.Activity"));
            builder.append("}\n");
            builder.append("else{\n");
            builder.append(code(variMsg.getVarMap(), "android.view.View"));
            builder.append("}\n");

            MethodSpec main = MethodSpec.methodBuilder("inject")
                    .addModifiers(Modifier.PUBLIC)
                    .returns(void.class)
                    .addAnnotation(Override.class)
                    .addParameter(host, "host")
                    .addParameter(Object.class, "object")
                    .addCode(builder.toString())
                    .build();

            TypeSpec helloWorld = TypeSpec.classBuilder(variMsg.getClsName() + "ViewInjector")
                    .addModifiers(Modifier.PUBLIC)
                    .addMethod(main)
                    .addSuperinterface(ParameterizedTypeName.get(InterfaceName, host))
                    .build();

            try {
                JavaFile javaFile = JavaFile.builder(variMsg.getPk(), helloWorld)
                        .addFileComment(" This codes are generated automatically. Do not modify!")
                        .build();
                javaFile.writeTo(filer);
            } catch (IOException e) {
                e.printStackTrace();
                System.out.println("e--->" + e.getMessage());
            }
        }
        System.out.println("xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx");
        return true;
    }


    /** * 根據註解對象生成code方法體 * * @param map * @param pk * @return */
    private String code(Map<Integer, VariableElement> map, String pk) {
        StringBuilder builder = new StringBuilder();
        for (Integer id : map.keySet()) {
            VariableElement variableElement = map.get(id);
            String name = variableElement.getSimpleName().toString();
            String type = variableElement.asType().toString();

            builder.append("host." + name + " = (" + type + ")(((" + pk + ")object).findViewById(" + id + "));\n");
        }
        return builder.toString();
    }複製代碼

到這裏註解處理器最終版本就生成成功了,看下最後生成的代碼類

package com.wzgiceman.viewinjector;

import com.example.ViewInjector;
import java.lang.Object;
import java.lang.Override;

public class MainActivityViewInjector implements ViewInjector<MainActivity> {
  @Override
  public void inject(MainActivity host, Object object) {
     if(object instanceof android.app.Activity){
    host.tvMsg = (android.widget.TextView)(((android.app.Activity)object).findViewById(2131492945));
    host.tvOther = (android.widget.TextView)(((android.app.Activity)object).findViewById(2131492946));
    }
    else{
    host.tvMsg = (android.widget.TextView)(((android.view.View)object).findViewById(2131492945));
    host.tvOther = (android.widget.TextView)(((android.view.View)object).findViewById(2131492946));
    }
  }
}複製代碼

api

api模塊主要定義的是給外部提供的使用方法,這裏使用方法即是WzgJector.bind(this)方法,相同於Butter Knife中的ButterKnife.bind(this);

public class MainActivity extends AppCompatActivity {

   xxxxxx
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        WzgJector.bind(this);
        xxxx
    }
}複製代碼

實現

建立一個app module

定義接口類ViewInjector

暴露的外部方法,主要在編譯自動生成的註解處理類中使用

/** * 接口 * Created by WZG on 2017/1/11. */
public interface ViewInjector<M> {
    void inject(M m, Object object);
}複製代碼

實際處理類WzgJector

提供了兩個方法,一種是activity綁定,一種是view或者fragment綁定,綁定完之後,經過反射獲得相關注解編譯處理類及時ViewInjector子類對象,調用inject(M m, Object object)方法完成初始過程。

public class WzgJector {
    public static void bind(Object activity) {
        bind(activity, activity);
    }

    public static void bind(Object host, Object root) {
        Class<?> clazz = host.getClass();
        String proxyClassFullName = clazz.getName() + "ViewInjector";
        try {
            Class<?> proxyClazz = Class.forName(proxyClassFullName);
            ViewInjector viewInjector = (ViewInjector) proxyClazz.newInstance();
            viewInjector.inject(host, root);
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (InstantiationException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }
    }
}複製代碼

到這裏其實會發現,編譯時註解並非徹底不使用反射,可是它避免了關鍵性重複性代碼的屢次反射使用,繼而提高了編譯的性能。

結果

到這裏butterknife@BindView實現原理基本就是如此,因爲時間緣由@OnClick就再也不講解了。其實原理同樣,小夥伴們能夠本身安裝本文思路添加@OnClick的處理。

專欄

註解-編譯運行時註解

源碼

下載源碼

建議

若是你有任何的問題和建議歡迎加入QQ羣告訴我!

相關文章
相關標籤/搜索