ButterKnife原理分析(二)注解的處理

上一篇我們講解了ButterKnife的設計思想,理解了ButterKnife綁定相關源碼的實現(xiàn)邏輯。但是它是怎么通過注解的方式生成的那些邏輯代碼,這才是最讓我們迫切想知道,因此在這篇,我將說說ButterKnife中注解處理的原理。本篇主要有以下內容:

  1. 注解Annotation
  2. 注解處理器AbstractProcessor
  3. AutoService注冊注解處理器
  4. JavaPoet生成Java代碼
  5. Element元素相關
  6. 編譯時注解解析
  7. 例子項目

注解(Annotation)

注解(Annotation)在Java中已經是很普遍的使用了,它其實就是一種標記信息,然后程序在編譯或者運行的時候可以讀取這個標記信息,去執(zhí)行特定的邏輯,比如@BindView(R.id.tv_text) TextView tvText,程序在編譯時會讀取到這個@BindView注解,解析出它的值R.id.tv_text,再根據它注解的這個tvText,就可以生成類似tvText = (TextView)findViewById(R.id.tv_text);的功能代碼。
注解按生命周期可以分為

  • RetentionPolicy.SOURCE(源碼注解),只在源碼中存在,在編譯時會被丟棄,通常用于檢查性的操作,如@Override。
  • RetentionPolicy.CLASS(編譯時注解),在編譯后的class文件中依然存在,通常用于編譯時處理,如ButterKnife的@BindView。
  • RetentionPolicy.RUNTIME(運行時注解),不僅在編譯后的class文件中存在,在被jvm虛擬機加載之后,仍然存在,通常用于運行時處理,如Retrofit的@Get。

同時注解按使用的對象可以分為

  • ElementType.TYPE(類型注解),標記在接口、類、枚舉上。
  • ElementType.FIELD(屬性注解),標記在屬性字段上。
  • ElementType.METHOD(方法注解),標記在方法上。
  • ElementType.PARAMETER(方法參數(shù)注解),標記在方法參數(shù)上。
  • ElementType.CONSTRUCTOR(構造方法注解),標記在構造方法上。
  • ElementType.LOCAL_VARIABLE(本地變量注解),標記在本地變量上。
  • ElementType.ANNOTATION_TYPE(注解的注解),標記在注解上。
  • ElementType.PACKAGE(包注解),標記在包上。
  • ElementType.TYPE_PARAMETER(類型參數(shù)注解,Java1.8加入),標記類型參數(shù)上。
  • ElementType.TYPE_USE(類型使用注解,Java1.8加入),標記在類的使用上。

我們首先來認識ButterKnife的一個自定義屬性注解@BindView

/**
 * 作用于View的注解,如@BindView(R.id.text) TextView tvText
 *
 * @Retention(RetentionPolicy.CLASS) 表示生命周期到類的編譯時期
 * @Target(ElementType.FIELD) 表示注解作用在字段上
 *
 * Created by Administrator on 2017/12/31 0031.
 */
@Retention(RetentionPolicy.CLASS)
@Target(ElementType.FIELD)
public @interface BindView {

    @android.support.annotation.IdRes
    int value();
}

它可以保留在類編譯之后,使用場景是作用在屬性上

關于注解的詳細介紹查看Java注解基礎概念總結

注解處理器AbstractProcessor

注解只是一種標記信息,所以需要我們自己去處理注解,注解的處理有編譯時注解處理和運行時注解處理。運行時注解,我們可以通過反射獲取注解信息,進而進行相應處理。而編譯時注解就需要使用注解處理器(Annotation Processor)進行處理。那什么是注解處理器?

注解處理器是javac的一個工具,它用來在編譯時掃描和處理注解(Annotation)。你可以自定義注解,并注冊到相應的注解處理器,由注解處理器來處理你的注解。一個注解的注解處理器,以Java代碼(或者編譯過的字節(jié)碼)作為輸入,生成文件(通常是.java文件)作為輸出。這些生成的Java代碼是在新生成的.java文件中,所以你不能修改已經存在的Java類,例如向已有的類中添加方法。這些生成的Java文件,會同其他普通的手動編寫的Java源代碼一樣被javac編譯。
要實現(xiàn)一個注解處理器需要繼承AbstractProcessor,并重寫它的4個方法,同時必須要有一個無參的構造方法,以便注解工具能夠對它進行初始化。

public class ViewBindProcessor extends AbstractProcessor {  
    private Types typeUtils;
    private Elements elementUtils;
    private Filer filer;
    private Messager messager;

    @Override  
    public synchronized void init(ProcessingEnvironment processingEnv) {  
        super.init(processingEnv);  
        
        //提供給注解處理器使用的工具類
        typeUtils = processingEnv.getTypeUtils();
        elementUtils = processingEnv.getElementUtils();
        filer = processingEnv.getFiler();
        messager = processingEnv.getMessager();
    }  
  
    @Override  
    public Set<String> getSupportedAnnotationTypes() {  
        //添加需要處理的注解
        Set<String> annotataions = new LinkedHashSet<String>();  
        annotataions.add(MyAnnotation.class.getCanonicalName());  
        return annotataions;  
    }  
  
    @Override  
    public SourceVersion getSupportedSourceVersion() {
        //指定使用的Java版本
        return SourceVersion.latestSupported();  
    } 
    
     @Override  
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {  
        //這里實現(xiàn)注解的處理,重點方法
        return false;  
    }  
}  
  • init,會被注解處理工具調用,參數(shù)ProcessingEnvironment提供了Elements,Types,F(xiàn)iler,Messager 等。
  • getSupportedAnnotationTypes(),指定注解處理器要處理哪些注解,返回一個字符串集合,包含要處理注解的全名。
  • getSupportedSourceVersion, 指定使用的Java版本,通常這里返回SourceVersion.latestSupported()
  • process,相當于每個處理器的main函數(shù),在這里可以做掃描、評估和處理注解代碼的操作,以及生成Java文件。

那么init方法中ProcessingEnvironment提供的Elements,Types,F(xiàn)iler,Messager 等是做什么用的呢?

  • Elements:用來處理程序元素的工具類
  • Types:用來處理類型數(shù)據的工具類
  • Filer:用來給注解處理器創(chuàng)建文件
  • Messager:用來給注解處理器報告錯誤,警告,提示等消息。

AutoService注冊注解處理器

以前要注冊注解處理器是要在module的META-INF目錄下新建services目錄,并創(chuàng)建一個名為javax.annotation.processing.Processor的文件,然后在文件中寫入要注冊的注解處理器的全名,例如在javax.annotation.processing.Processor的文件中加上

com.pinery.compile.ViewBindProcessor

來注冊ViewBindProcessor注解處理器。

后來Google推出了通過添加AutoService注解庫來實現(xiàn)注解處理器的注冊,通過在你的注解處理器上加上@AutoService(Processor.class)注解,即可在編譯時生成 META-INF/services/javax.annotation.processing.Processor 文件。
配置AutoService需要在工程的build.gradle中添加

JavaPoet生成Java代碼

JavaPoet是Square公司出品的生成Java源文件庫,正如其名,會寫Java代碼的詩人,使用它的一系列API就可以很方便的生成java源代碼了。

JavaPoet中有幾個常用的類:

  • MethodSpec,代表一個構造函數(shù)或方法聲明。
  • TypeSpec,代表一個類,接口,或者枚舉聲明。
  • FieldSpec,代表一個成員變量,一個字段聲明。
  • JavaFile,包含一個頂級類的Java文件。

這是一個計算從1到100相加的方法

public static int caculateNum() {
    int result = 0;
    for(int i = 1; i < 100; i++) {
      result = result + i;
    }
    return result;
}

我們用MethodSpec實現(xiàn)這個方法聲明

MethodSpec caculateMethod = MethodSpec.methodBuilder("caculateNum")
      .addModifiers(Modifier.PUBLIC, Modifier.STATIC)
      .returns(int.class)
      .addStatement("int result = 0")
      .beginControlFlow("for(int i = $L; i < $L; i++)", 1, 100)
      .addStatement("result = result $L i", "+")
      .endControlFlow()
      .addStatement("return result")
      .build();

可以發(fā)現(xiàn),通過addModifiers添加方法修飾符,returns來定義方法的返回值類型,addStatement來添加方法中的一行語句,它會處理分號和換行,beginControlFlow和endControlFlow構成一個封閉的控制語段,適用于if,for,while等。$L相當于一個占位符,代表的是一個字面量,其他還有:

  • $S for Strings,代表一個字符串
  • $T for Types,代表一個類型,使用它會自動import導入包
  • $N for Names,代表我們自己生成的方法名或者變量名等等

    例如
addStatement("$T.out.println($S)", System.class, "Hello World"))

這是定義的一個屬性

private final String name = "Pinery";

我們用MethodSpec實現(xiàn)這個方法聲明

FieldSpec nameField = FieldSpec.builder(String.class, "name")
    .addModifiers(Modifier.PRIVATE, Modifier.FINAL)
    .initializer("$S", "Pinery")
    .build();

下面是一個類的定義

public class MyClass{

    private final String name = "Pinery";

    public static int caculateNum() {
        int result = 0;
        for(int i = 1; i < 100; i++) {
          result = result + i;
        }
        return result;
    }
}

我們用TypeSpec實現(xiàn)這個方法聲明

TypeSpec helloWorld = TypeSpec.classBuilder("MyClass")
    .addModifiers(Modifier.PUBLIC)
    .addField(nameField)
    .addMethod(caculateMethod)
    .build();

通過TypeSpec添加了屬性實現(xiàn)和方法實現(xiàn),其他常用的還有

  • addTypeVariable,添加泛型聲明
  • addSuperinterface,添加接口實現(xiàn)
  • addJavadoc,添加注釋
  • interfaceBuilder,生成一個接口

等,詳細使用可以參考JavaPoet官方文檔

Element元素相關

注解處理工具掃描java源文件,源代碼中的每一部分都是程序中的Element元素,如包,類,方法,字段等。每一個元素代表一個靜態(tài)的,語言級別的結構。源代碼其實是一種結構化的文本(例如json文本就是一種結構化的文本),因此需要對它進行解析,解析的話,解析器會解析某些信息代表某些結構,例如源代碼中的類聲明信息代表TypeElement類型元素,方法聲明信息代表代表ExecutableElement類型元素。有了這些結構,就能完整的表示整個源代碼信息了。
Element元素分為以下類型:

  • ExecutableElement: 可執(zhí)行元素,包括類或接口的方法、構造方法或初始化程序
  • PackageElement: 包元素,提供對有關包及其成員的信息的訪問
  • TypeElement: 類或接口元素,提供對有關類型及其成員的信息的訪問
  • TypeParameterElement: 表示一般類、接口、方法或構造方法元素的形式類型參數(shù),類型參數(shù)聲明一個 TypeVariable
  • VariableElement: 表示一個字段、enum常量、方法或構造方法參數(shù)、局部變量或異常參數(shù)。

Element元素還有個asType()可以獲取元素類型TypeMirror,TypeMirror有以下具體類型:

  • ArrayType: 表示一個數(shù)組類型。多維數(shù)組類型被表示為組件類型也是數(shù)組類型的數(shù)組類型。
  • DeclaredType: 表示某一聲明類型,是一個類 (class) 類型或接口 (interface) 類型。這包括參數(shù)化的類型(比如 java.util.Set<String>)和原始類型。
  • ErrorType: 表示無法正常建模的類或接口類型。這可能是處理錯誤的結果。大多數(shù)對于派生于這種類型(比如其成員或其超類型)的信息的查詢通常不會返回有意義的結果。
  • ExecutableType: 表示 executable 的類型。executable 是一個方法、構造方法或初始化程序。
  • NoType: 在實際類型不適合的地方使用的偽類型。NoType 的種類有:
    • VOID:對應于關鍵字 void。
    • PACKAGE:包元素的偽類型。
    • NONE:用于實際類型不適合的其他情況中;例如,java.lang.Object 的超類。
  • NullType: 表示 null 類型。此類表達式 null 的類型。
  • PrimitiveType: 表示一個基本類型。這些類型包括 boolean、byte、short、int、long、char、float 和 double。
  • ReferenceType: 表示一個引用類型。這些類型包括類和接口類型、數(shù)組類型、類型變量和 null 類型。
  • TypeVariable: 表示一個類型變量。類型變量可由某一類型、方法或構造方法的類型參數(shù)顯式聲明。
  • WildcardType: 表示通配符類型參數(shù)。

編譯時注解解析

有了上面知識點的了解之后,下面就可以進行編譯時的注解解析了,需要以下幾個步驟:

  1. 定義注解
  2. 定義一個繼承自AbstractProcessor的注解處理器,重寫它4個方法中
  3. 使用AutoService注冊自定義的注解處理器
  4. 實現(xiàn)process方法,在這里處理注解
  5. 處理所有注解,得到TypeElement和注解信息等信息
  6. 使用JavaPoet生成新的Java類

在annotations的moudle中定義一個注解

/**
 * 作用于View的注解,如@BindView(R.id.text) TextView tvText
 *
 * @Retention(RetentionPolicy.CLASS) 表示生命周期到類的編譯時期
 * @Target(ElementType.FIELD) 表示注解作用在字段上
 *
 * Created by Administrator on 2017/12/31 0031.
 */
@Retention(RetentionPolicy.CLASS)
@Target(ElementType.FIELD)
public @interface BindView {

    @android.support.annotation.IdRes
    int value();
}

在compile的moudle中定義一個ViewBindProcessor注解處理器

/**
 * 自定義的AbstractProcessor,用于編譯時處理注解
 */
@AutoService(Processor.class) //添加AutoService注解,自動注冊ViewBindProcessor注解處理器
public class ViewBindProcessor extends AbstractProcessor{

    private Map<TypeElement, List<ViewBindInfo>> bindMap = new HashMap<>();

    //用來處理類型數(shù)據的工具類
    private Types typeUtils;
    //用來處理程序元素的工具類
    private Elements elementUtils;
    //用來給注解處理器創(chuàng)建文件
    private Filer filer;
    //用來給注解處理器報告錯誤,警告,提示等消息。
    private Messager messager;

    /**
     * 會被注解處理工具調用,參數(shù)ProcessingEnvironment提供了Elements,Types,F(xiàn)iler,Messager 等。
     * @param processingEnvironment
     */
    @Override
    public synchronized void init(ProcessingEnvironment processingEnvironment) {
        super.init(processingEnvironment);

        typeUtils = processingEnv.getTypeUtils();
        elementUtils = processingEnv.getElementUtils();
        filer = processingEnv.getFiler();
        messager = processingEnv.getMessager();
    }

    /**
     * 指定注解處理器是注冊給那一個注解的,它是一個字符串的集合,意味著可以支持多個類型的注解,并且字符串是合法全名。
     * @return
     */
    @Override
    public Set<String> getSupportedAnnotationTypes() {
        Set<String> annotataions = new LinkedHashSet<String>();

        //添加自定義的BindView注解
        annotataions.add(BindView.class.getCanonicalName());

        return annotataions;
    }

    /**
     * 指定使用的Java版本,通常這里返回SourceVersion.latestSupported()
     * @return
     */
    @Override
    public SourceVersion getSupportedSourceVersion() {
        return SourceVersion.latestSupported();
    }

    /**
     * 相當于每個處理器的main函數(shù),在這里可以做掃描、評估和處理注解代碼的操作,以及生成Java文件。
     * @param set
     * @param roundEnvironment
     * @return
     */
    @Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {

        collectBindViewAnnotations(roundEnvironment);

        generateJavaFilesWithJavaPoet();

        return false;
    }

    /**
     * 收集BindView注解
     * @param roundEnvironment
     * @return
     */
    private boolean collectBindViewAnnotations(RoundEnvironment roundEnvironment){
        //查找所有添加了注解BindView的元素
        Set<? extends Element> elements = roundEnvironment.getElementsAnnotatedWith(BindView.class);
        if(elements == null || elements.isEmpty()){
            return false;
        }

        for(Element element : elements){
            //注解BindView必須添加在屬性上
            if(element.getKind() != ElementKind.FIELD){
                error(element, "只有類的屬性可以添加@%s注解", BindView.class.getCanonicalName());
                return false;
            }

            //獲取注解的值
            int viewId = element.getAnnotation(BindView.class).value();
            //這個元素是屬性類型的元素
            VariableElement viewElement = (VariableElement) element;
            //獲取直接包含屬性元素的元素,即類元素
            TypeElement typeElement = (TypeElement) viewElement.getEnclosingElement();

            //將類型元素作為key,保存到bindMap暫存
            List<ViewBindInfo> viewBindInfoList = bindMap.get(typeElement);
            if(viewBindInfoList == null){
                viewBindInfoList = new ArrayList<>();
                bindMap.put(typeElement, viewBindInfoList);
            }

            info("注解信息:viewId=%d, name=%s, type=%s", viewId, viewElement.getSimpleName().toString(), viewElement.asType().toString());

            viewBindInfoList.add(new ViewBindInfo(viewId, viewElement.getSimpleName().toString(), viewElement.asType()));
        }

        return true;
    }

    /**
     * 生成注解處理之后的Java文件
     */
    private void generateJavaFilesWithJavaPoet(){
        if(bindMap.isEmpty()){
            return;
        }

        //針對每個類型元素,生成一個新文件,例如,針對MainActivity,生成MainActivity_ViewBind文件
        for(Map.Entry<TypeElement, List<ViewBindInfo>> entry : bindMap.entrySet()){
            TypeElement typeElement = entry.getKey();
            List<ViewBindInfo> list = entry.getValue();

            //獲取當前類型元素所在的包名
            String pkgName = elementUtils.getPackageOf(typeElement).getQualifiedName().toString();

            //獲取類的全名稱
            ClassName t = ClassName.bestGuess("T");
            ClassName viewBinder = ClassName.bestGuess("com.pinery.bind_lib.ViewBinder");

            //定義方法結構
            MethodSpec.Builder methodSpecBuilder = MethodSpec.methodBuilder("bind")
                    .addAnnotation(Override.class)//Override注解
                    .addModifiers(Modifier.PUBLIC)//public修飾符
                    .returns(void.class)//返回類型void
                    .addParameter(t, "activity")//參數(shù)類型
                    ;
            //為方法添加實現(xiàn)
            for(ViewBindInfo info : list){
                methodSpecBuilder.addStatement("activity.$L = activity.findViewById($L)", info.viewName, info.viewId);
            }

            //定義類結構
            TypeSpec typeSpec = TypeSpec.classBuilder(typeElement.getSimpleName().toString() + "_ViewBinder")
                    .addModifiers(Modifier.PUBLIC)//public修飾符
                    .addTypeVariable(TypeVariableName.get("T", TypeName.get(typeElement.asType())))//泛型聲明
                    .addSuperinterface(ParameterizedTypeName.get(viewBinder, t))
                    .addMethod(methodSpecBuilder.build())//方法
                    .build();

            //定義一個Java文件結構
            JavaFile javaFile = JavaFile.builder(pkgName, typeSpec).build();
            try {
                //寫入到filer中
                javaFile.writeTo(filer);
            }catch (Exception ex){
                ex.printStackTrace();
            }

        }

    }

    /**
     * 錯誤提示
     * @param element
     * @param msg
     * @param args
     */
    private void error(Element element, String msg, Object... args){
        //輸出錯誤提示
        messager.printMessage(Diagnostic.Kind.ERROR, String.format(msg, args));
    }

    /**
     * 信息提示
     * @param msg
     * @param args
     */
    private void info(String msg, Object... args) {
        messager.printMessage(
                Diagnostic.Kind.NOTE,
                String.format(msg, args));
    }

}

這里會使用一個ViewBindInfo用于存儲view的id, 名稱,和類型的對應關系,如下:

public class ViewBindInfo {

    public int viewId;
    public String viewName;
    public TypeMirror typeMirror;

    public ViewBindInfo(int viewId, String viewName, TypeMirror typeMirror){
        this.viewId = viewId;
        this.viewName = viewName;
        this.typeMirror = typeMirror;
    }

}

這樣就完成了編譯時注解的處理。接下來在MainActivity中使用注解


public class MainActivity extends AppCompatActivity {

    @BindView(R.id.tv_text)
    TextView tvText;
    @BindView(R.id.btn_text)
    Button btnText;

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

        BindHelper.bind(this);

        tvText.setText("Hello, BindView");
        btnText.setText("Hello, BindView");
    }
}

編譯后會生成一個MainActivity_ViewBinder.java文件。我們在看看BindHelper的實現(xiàn)


public class BindHelper {

    /**
     * 綁定方法
     * @param activity
     */
    public static void bind(Activity activity) {
        try {
            Class<?> viewBinderClazz = Class.forName(activity.getClass().getCanonicalName() + "_ViewBinder");
            ViewBinder viewBinder = (ViewBinder) viewBinderClazz.newInstance();
            viewBinder.bind(activity);
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (InstantiationException e) {
            e.printStackTrace();
        }

    }

}

這里就會通過反射生成MainActivity_ViewBinder的對象,調用bind方法作view的綁定處理。

例子項目

CompileAnnotation

?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內容

友情鏈接更多精彩內容